mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2024-11-22 08:46:54 -06:00
Add account avatar
This commit is contained in:
parent
202f6b6c9d
commit
2295ce6c4e
@ -1,7 +1,13 @@
|
||||
<div class="user-info">
|
||||
{{ user.username }}
|
||||
<div class="user">
|
||||
<img [src]="getAvatarPath()" alt="Avatar" />
|
||||
|
||||
<div class="user-info">
|
||||
<div class="user-info-username">{{ user.username }}</div>
|
||||
<div class="user-info-followers">{{ user.account.followersCount }} subscribers</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="account-title">Account settings</div>
|
||||
<my-account-change-password></my-account-change-password>
|
||||
|
||||
|
@ -1,6 +1,21 @@
|
||||
.user-info {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
.user {
|
||||
display: flex;
|
||||
|
||||
img {
|
||||
@include avatar(50px);
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
.user-info-username {
|
||||
font-size: 20px;
|
||||
font-weight: $font-bold;
|
||||
}
|
||||
|
||||
.user-info-followers {
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.account-title {
|
||||
|
@ -15,4 +15,8 @@ export class AccountSettingsComponent implements OnInit {
|
||||
ngOnInit () {
|
||||
this.user = this.authService.getUser()
|
||||
}
|
||||
|
||||
getAvatarPath () {
|
||||
return this.user.getAvatarPath()
|
||||
}
|
||||
}
|
||||
|
@ -1,29 +1,24 @@
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject'
|
||||
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import 'rxjs/add/observable/throw'
|
||||
import 'rxjs/add/operator/do'
|
||||
import 'rxjs/add/operator/map'
|
||||
import 'rxjs/add/operator/mergeMap'
|
||||
import 'rxjs/add/observable/throw'
|
||||
|
||||
import { NotificationsService } from 'angular2-notifications'
|
||||
import { Observable } from 'rxjs/Observable'
|
||||
import { ReplaySubject } from 'rxjs/ReplaySubject'
|
||||
import { Subject } from 'rxjs/Subject'
|
||||
import { OAuthClientLocal, User as UserServerModel, UserRefreshToken, UserRole, VideoChannel } from '../../../../../shared'
|
||||
import { Account } from '../../../../../shared/models/accounts'
|
||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||
// Do not use the barrel (dependency loop)
|
||||
import { RestExtractor } from '../../shared/rest'
|
||||
import { UserConstructorHash } from '../../shared/users/user.model'
|
||||
|
||||
import { AuthStatus } from './auth-status.model'
|
||||
import { AuthUser } from './auth-user.model'
|
||||
import {
|
||||
OAuthClientLocal,
|
||||
UserRole,
|
||||
UserRefreshToken,
|
||||
VideoChannel,
|
||||
User as UserServerModel
|
||||
} from '../../../../../shared'
|
||||
// Do not use the barrel (dependency loop)
|
||||
import { RestExtractor } from '../../shared/rest'
|
||||
import { UserLogin } from '../../../../../shared/models/users/user-login.model'
|
||||
import { UserConstructorHash } from '../../shared/users/user.model'
|
||||
|
||||
interface UserLoginWithUsername extends UserLogin {
|
||||
access_token: string
|
||||
@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin {
|
||||
displayNSFW: boolean
|
||||
email: string
|
||||
videoQuota: number
|
||||
account: {
|
||||
id: number
|
||||
uuid: string
|
||||
}
|
||||
account: Account
|
||||
videoChannels: VideoChannel[]
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
<menu>
|
||||
<div *ngIf="isLoggedIn" class="logged-in-block">
|
||||
<img [src]="getUserAvatarPath()" alt="Avatar" />
|
||||
|
||||
<div class="logged-in-info">
|
||||
<a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
|
||||
<div class="logged-in-email">{{ user.email }}</div>
|
||||
|
@ -21,9 +21,15 @@ menu {
|
||||
justify-content: center;
|
||||
margin-bottom: 35px;
|
||||
|
||||
img {
|
||||
margin-left: 20px;
|
||||
margin-right: 10px;
|
||||
|
||||
@include avatar(34px);
|
||||
}
|
||||
|
||||
.logged-in-info {
|
||||
flex-grow: 1;
|
||||
margin-left: 40px;
|
||||
|
||||
.logged-in-username {
|
||||
font-size: 16px;
|
||||
|
@ -51,6 +51,10 @@ export class MenuComponent implements OnInit {
|
||||
)
|
||||
}
|
||||
|
||||
getUserAvatarPath () {
|
||||
return this.user.getAvatarPath()
|
||||
}
|
||||
|
||||
isRegistrationAllowed () {
|
||||
return this.serverService.getConfig().signup.allowed
|
||||
}
|
||||
|
@ -1,10 +1,5 @@
|
||||
import {
|
||||
User as UserServerModel,
|
||||
UserRole,
|
||||
VideoChannel,
|
||||
UserRight,
|
||||
hasUserRight
|
||||
} from '../../../../../shared'
|
||||
import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared'
|
||||
import { Account } from '../../../../../shared/models/accounts'
|
||||
|
||||
export type UserConstructorHash = {
|
||||
id: number,
|
||||
@ -14,10 +9,7 @@ export type UserConstructorHash = {
|
||||
videoQuota?: number,
|
||||
displayNSFW?: boolean,
|
||||
createdAt?: Date,
|
||||
account?: {
|
||||
id: number
|
||||
uuid: string
|
||||
},
|
||||
account?: Account,
|
||||
videoChannels?: VideoChannel[]
|
||||
}
|
||||
export class User implements UserServerModel {
|
||||
@ -27,10 +19,7 @@ export class User implements UserServerModel {
|
||||
role: UserRole
|
||||
displayNSFW: boolean
|
||||
videoQuota: number
|
||||
account: {
|
||||
id: number
|
||||
uuid: string
|
||||
}
|
||||
account: Account
|
||||
videoChannels: VideoChannel[]
|
||||
createdAt: Date
|
||||
|
||||
@ -61,4 +50,10 @@ export class User implements UserServerModel {
|
||||
hasRight (right: UserRight) {
|
||||
return hasUserRight(this.role, right)
|
||||
}
|
||||
|
||||
getAvatarPath () {
|
||||
if (this.account && this.account.avatar) return this.account.avatar.path
|
||||
|
||||
return '/assets/default-avatar.png'
|
||||
}
|
||||
}
|
||||
|
BIN
client/src/assets/default-avatar.png
Normal file
BIN
client/src/assets/default-avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
@ -39,3 +39,8 @@
|
||||
@include peertube-button;
|
||||
@include disable-default-a-behaviour;
|
||||
}
|
||||
|
||||
@mixin avatar ($size) {
|
||||
width: $size;
|
||||
height: $size;
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'avatars/'
|
||||
certs: 'certs/'
|
||||
videos: 'videos/'
|
||||
logs: 'logs/'
|
||||
|
@ -17,6 +17,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'avatars/'
|
||||
certs: 'certs/'
|
||||
videos: 'videos/'
|
||||
logs: 'logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test1/avatars/'
|
||||
certs: 'test1/certs/'
|
||||
videos: 'test1/videos/'
|
||||
logs: 'test1/logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test2/avatars/'
|
||||
certs: 'test2/certs/'
|
||||
videos: 'test2/videos/'
|
||||
logs: 'test2/logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test3/avatars/'
|
||||
certs: 'test3/certs/'
|
||||
videos: 'test3/videos/'
|
||||
logs: 'test3/logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test4/avatars/'
|
||||
certs: 'test4/certs/'
|
||||
videos: 'test4/videos/'
|
||||
logs: 'test4/logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test5/avatars/'
|
||||
certs: 'test5/certs/'
|
||||
videos: 'test5/videos/'
|
||||
logs: 'test5/logs/'
|
||||
|
@ -10,6 +10,7 @@ database:
|
||||
|
||||
# From the project root directory
|
||||
storage:
|
||||
avatars: 'test6/avatars/'
|
||||
certs: 'test6/certs/'
|
||||
videos: 'test6/videos/'
|
||||
logs: 'test6/logs/'
|
||||
|
@ -14,7 +14,7 @@ import { FollowState } from '../../shared/models/accounts/follow.model'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const LAST_MIGRATION_VERSION = 110
|
||||
const LAST_MIGRATION_VERSION = 115
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -60,6 +60,7 @@ const CONFIG = {
|
||||
PASSWORD: config.get<string>('database.password')
|
||||
},
|
||||
STORAGE: {
|
||||
AVATARS_DIR: join(root(), config.get<string>('storage.avatars')),
|
||||
LOG_DIR: join(root(), config.get<string>('storage.logs')),
|
||||
VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
|
||||
THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')),
|
||||
@ -105,6 +106,9 @@ const CONFIG = {
|
||||
CONFIG.WEBSERVER.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
||||
CONFIG.WEBSERVER.HOST = CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
|
||||
|
||||
const AVATARS_DIR = {
|
||||
ACCOUNT: join(CONFIG.STORAGE.AVATARS_DIR, 'account')
|
||||
}
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const CONSTRAINTS_FIELDS = {
|
||||
@ -356,6 +360,7 @@ export {
|
||||
PREVIEWS_SIZE,
|
||||
REMOTE_SCHEME,
|
||||
FOLLOW_STATES,
|
||||
AVATARS_DIR,
|
||||
SEARCHABLE_COLUMNS,
|
||||
SERVER_ACCOUNT_NAME,
|
||||
PRIVATE_RSA_KEY_SIZE,
|
||||
|
@ -2,6 +2,7 @@ import { join } from 'path'
|
||||
import { flattenDepth } from 'lodash'
|
||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { AvatarModel } from '../models/avatar'
|
||||
|
||||
import { CONFIG } from './constants'
|
||||
// Do not use barrel, we need to load database first
|
||||
@ -36,6 +37,7 @@ export type PeerTubeDatabase = {
|
||||
init?: (silent: boolean) => Promise<void>,
|
||||
|
||||
Application?: ApplicationModel,
|
||||
Avatar?: AvatarModel,
|
||||
Account?: AccountModel,
|
||||
Job?: JobModel,
|
||||
OAuthClient?: OAuthClientModel,
|
||||
|
31
server/initializers/migrations/0115-account-avatar.ts
Normal file
31
server/initializers/migrations/0115-account-avatar.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { PeerTubeDatabase } from '../database'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction,
|
||||
queryInterface: Sequelize.QueryInterface,
|
||||
sequelize: Sequelize.Sequelize,
|
||||
db: PeerTubeDatabase
|
||||
}): Promise<void> {
|
||||
await db.Avatar.sync()
|
||||
|
||||
const data = {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'Avatars',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
}
|
||||
await utils.queryInterface.addColumn('Accounts', 'avatarId', data)
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
up,
|
||||
down
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import * as Bluebird from 'bluebird'
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
|
||||
import { AvatarInstance } from '../avatar'
|
||||
import { ServerInstance } from '../server/server-interface'
|
||||
import { VideoChannelInstance } from '../video/video-channel-interface'
|
||||
|
||||
@ -51,6 +52,7 @@ export interface AccountAttributes {
|
||||
serverId?: number
|
||||
userId?: number
|
||||
applicationId?: number
|
||||
avatarId?: number
|
||||
}
|
||||
|
||||
export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
|
||||
@ -68,6 +70,7 @@ export interface AccountInstance extends AccountClass, AccountAttributes, Sequel
|
||||
|
||||
Server: ServerInstance
|
||||
VideoChannels: VideoChannelInstance[]
|
||||
Avatar: AvatarInstance
|
||||
}
|
||||
|
||||
export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { join } from 'path'
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { Avatar } from '../../../shared/models/avatars/avatar.model'
|
||||
import {
|
||||
activityPubContextify,
|
||||
isAccountFollowersCountValid,
|
||||
@ -8,8 +10,10 @@ import {
|
||||
isUserUsernameValid
|
||||
} from '../../helpers'
|
||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||
import { AVATARS_DIR } from '../../initializers'
|
||||
import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
||||
import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete'
|
||||
import { AvatarModel } from '../avatar'
|
||||
import { addMethodsToModel } from '../utils'
|
||||
import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface'
|
||||
|
||||
@ -252,6 +256,14 @@ function associate (models) {
|
||||
as: 'followers',
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
|
||||
Account.hasOne(models.Avatar, {
|
||||
foreignKey: {
|
||||
name: 'avatarId',
|
||||
allowNull: true
|
||||
},
|
||||
onDelete: 'cascade'
|
||||
})
|
||||
}
|
||||
|
||||
function afterDestroy (account: AccountInstance) {
|
||||
@ -265,6 +277,15 @@ function afterDestroy (account: AccountInstance) {
|
||||
toFormattedJSON = function (this: AccountInstance) {
|
||||
let host = CONFIG.WEBSERVER.HOST
|
||||
let score: number
|
||||
let avatar: Avatar = null
|
||||
|
||||
if (this.Avatar) {
|
||||
avatar = {
|
||||
path: join(AVATARS_DIR.ACCOUNT, this.Avatar.filename),
|
||||
createdAt: this.Avatar.createdAt,
|
||||
updatedAt: this.Avatar.updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
if (this.Server) {
|
||||
host = this.Server.host
|
||||
@ -273,11 +294,15 @@ toFormattedJSON = function (this: AccountInstance) {
|
||||
|
||||
const json = {
|
||||
id: this.id,
|
||||
uuid: this.uuid,
|
||||
host,
|
||||
score,
|
||||
name: this.name,
|
||||
followingCount: this.followingCount,
|
||||
followersCount: this.followersCount,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt
|
||||
updatedAt: this.updatedAt,
|
||||
avatar
|
||||
}
|
||||
|
||||
return json
|
||||
|
@ -157,10 +157,7 @@ toFormattedJSON = function (this: UserInstance) {
|
||||
roleLabel: USER_ROLE_LABELS[this.role],
|
||||
videoQuota: this.videoQuota,
|
||||
createdAt: this.createdAt,
|
||||
account: {
|
||||
id: this.Account.id,
|
||||
uuid: this.Account.uuid
|
||||
}
|
||||
account: this.Account.toFormattedJSON()
|
||||
}
|
||||
|
||||
if (Array.isArray(this.Account.VideoChannels) === true) {
|
||||
|
16
server/models/avatar/avatar-interface.ts
Normal file
16
server/models/avatar/avatar-interface.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
export namespace AvatarMethods {}
|
||||
|
||||
export interface AvatarClass {}
|
||||
|
||||
export interface AvatarAttributes {
|
||||
filename: string
|
||||
}
|
||||
|
||||
export interface AvatarInstance extends AvatarClass, AvatarAttributes, Sequelize.Instance<AvatarAttributes> {
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface AvatarModel extends AvatarClass, Sequelize.Model<AvatarInstance, AvatarAttributes> {}
|
24
server/models/avatar/avatar.ts
Normal file
24
server/models/avatar/avatar.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
import { addMethodsToModel } from '../utils'
|
||||
import { AvatarAttributes, AvatarInstance, AvatarMethods } from './avatar-interface'
|
||||
|
||||
let Avatar: Sequelize.Model<AvatarInstance, AvatarAttributes>
|
||||
|
||||
export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) {
|
||||
Avatar = sequelize.define<AvatarInstance, AvatarAttributes>('Avatar',
|
||||
{
|
||||
filename: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
const classMethods = []
|
||||
addMethodsToModel(Avatar, classMethods)
|
||||
|
||||
return Avatar
|
||||
}
|
||||
|
||||
// ------------------------------ Statics ------------------------------
|
1
server/models/avatar/index.ts
Normal file
1
server/models/avatar/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './avatar-interface'
|
@ -1,4 +1,5 @@
|
||||
export * from './application'
|
||||
export * from './avatar'
|
||||
export * from './job'
|
||||
export * from './oauth'
|
||||
export * from './server'
|
||||
|
@ -1,5 +1,13 @@
|
||||
import { Avatar } from '../avatars/avatar.model'
|
||||
|
||||
export interface Account {
|
||||
id: number
|
||||
uuid: string
|
||||
name: string
|
||||
host: string
|
||||
followingCount: number
|
||||
followersCount: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
avatar: Avatar
|
||||
}
|
||||
|
5
shared/models/avatars/avatar.model.ts
Normal file
5
shared/models/avatars/avatar.model.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Avatar {
|
||||
path: string
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { Account } from '../accounts'
|
||||
import { VideoChannel } from '../videos/video-channel.model'
|
||||
import { UserRole } from './user-role'
|
||||
|
||||
@ -8,10 +9,7 @@ export interface User {
|
||||
displayNSFW: boolean
|
||||
role: UserRole
|
||||
videoQuota: number
|
||||
createdAt: Date,
|
||||
account: {
|
||||
id: number
|
||||
uuid: string
|
||||
}
|
||||
createdAt: Date
|
||||
account: Account
|
||||
videoChannels?: VideoChannel[]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user