Add account avatar

This commit is contained in:
Chocobozzz 2017-12-04 10:34:40 +01:00
parent 202f6b6c9d
commit 2295ce6c4e
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
31 changed files with 207 additions and 54 deletions

View File

@ -1,7 +1,13 @@
<div class="user-info"> <div class="user">
{{ user.username }} <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>
<div class="account-title">Account settings</div> <div class="account-title">Account settings</div>
<my-account-change-password></my-account-change-password> <my-account-change-password></my-account-change-password>

View File

@ -1,6 +1,21 @@
.user-info { .user {
font-size: 20px; display: flex;
font-weight: $font-bold;
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 { .account-title {

View File

@ -15,4 +15,8 @@ export class AccountSettingsComponent implements OnInit {
ngOnInit () { ngOnInit () {
this.user = this.authService.getUser() this.user = this.authService.getUser()
} }
getAvatarPath () {
return this.user.getAvatarPath()
}
} }

View File

@ -1,29 +1,24 @@
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { Observable } from 'rxjs/Observable'
import { Subject } from 'rxjs/Subject' import { NotificationsService } from 'angular2-notifications'
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http' import 'rxjs/add/observable/throw'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import 'rxjs/add/operator/do' import 'rxjs/add/operator/do'
import 'rxjs/add/operator/map' import 'rxjs/add/operator/map'
import 'rxjs/add/operator/mergeMap' import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/observable/throw' import { Observable } from 'rxjs/Observable'
import { ReplaySubject } from 'rxjs/ReplaySubject'
import { NotificationsService } from 'angular2-notifications' 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 { AuthStatus } from './auth-status.model'
import { AuthUser } from './auth-user.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 { interface UserLoginWithUsername extends UserLogin {
access_token: string access_token: string
@ -42,10 +37,7 @@ interface UserLoginWithUserInformation extends UserLogin {
displayNSFW: boolean displayNSFW: boolean
email: string email: string
videoQuota: number videoQuota: number
account: { account: Account
id: number
uuid: string
}
videoChannels: VideoChannel[] videoChannels: VideoChannel[]
} }

View File

@ -1,5 +1,7 @@
<menu> <menu>
<div *ngIf="isLoggedIn" class="logged-in-block"> <div *ngIf="isLoggedIn" class="logged-in-block">
<img [src]="getUserAvatarPath()" alt="Avatar" />
<div class="logged-in-info"> <div class="logged-in-info">
<a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a> <a routerLink="/account/settings" class="logged-in-username">{{ user.username }}</a>
<div class="logged-in-email">{{ user.email }}</div> <div class="logged-in-email">{{ user.email }}</div>

View File

@ -21,9 +21,15 @@ menu {
justify-content: center; justify-content: center;
margin-bottom: 35px; margin-bottom: 35px;
img {
margin-left: 20px;
margin-right: 10px;
@include avatar(34px);
}
.logged-in-info { .logged-in-info {
flex-grow: 1; flex-grow: 1;
margin-left: 40px;
.logged-in-username { .logged-in-username {
font-size: 16px; font-size: 16px;

View File

@ -51,6 +51,10 @@ export class MenuComponent implements OnInit {
) )
} }
getUserAvatarPath () {
return this.user.getAvatarPath()
}
isRegistrationAllowed () { isRegistrationAllowed () {
return this.serverService.getConfig().signup.allowed return this.serverService.getConfig().signup.allowed
} }

View File

@ -1,10 +1,5 @@
import { import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared'
User as UserServerModel, import { Account } from '../../../../../shared/models/accounts'
UserRole,
VideoChannel,
UserRight,
hasUserRight
} from '../../../../../shared'
export type UserConstructorHash = { export type UserConstructorHash = {
id: number, id: number,
@ -14,10 +9,7 @@ export type UserConstructorHash = {
videoQuota?: number, videoQuota?: number,
displayNSFW?: boolean, displayNSFW?: boolean,
createdAt?: Date, createdAt?: Date,
account?: { account?: Account,
id: number
uuid: string
},
videoChannels?: VideoChannel[] videoChannels?: VideoChannel[]
} }
export class User implements UserServerModel { export class User implements UserServerModel {
@ -27,10 +19,7 @@ export class User implements UserServerModel {
role: UserRole role: UserRole
displayNSFW: boolean displayNSFW: boolean
videoQuota: number videoQuota: number
account: { account: Account
id: number
uuid: string
}
videoChannels: VideoChannel[] videoChannels: VideoChannel[]
createdAt: Date createdAt: Date
@ -61,4 +50,10 @@ export class User implements UserServerModel {
hasRight (right: UserRight) { hasRight (right: UserRight) {
return hasUserRight(this.role, right) return hasUserRight(this.role, right)
} }
getAvatarPath () {
if (this.account && this.account.avatar) return this.account.avatar.path
return '/assets/default-avatar.png'
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -39,3 +39,8 @@
@include peertube-button; @include peertube-button;
@include disable-default-a-behaviour; @include disable-default-a-behaviour;
} }
@mixin avatar ($size) {
width: $size;
height: $size;
}

View File

@ -16,6 +16,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'avatars/'
certs: 'certs/' certs: 'certs/'
videos: 'videos/' videos: 'videos/'
logs: 'logs/' logs: 'logs/'

View File

@ -17,6 +17,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'avatars/'
certs: 'certs/' certs: 'certs/'
videos: 'videos/' videos: 'videos/'
logs: 'logs/' logs: 'logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test1/avatars/'
certs: 'test1/certs/' certs: 'test1/certs/'
videos: 'test1/videos/' videos: 'test1/videos/'
logs: 'test1/logs/' logs: 'test1/logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test2/avatars/'
certs: 'test2/certs/' certs: 'test2/certs/'
videos: 'test2/videos/' videos: 'test2/videos/'
logs: 'test2/logs/' logs: 'test2/logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test3/avatars/'
certs: 'test3/certs/' certs: 'test3/certs/'
videos: 'test3/videos/' videos: 'test3/videos/'
logs: 'test3/logs/' logs: 'test3/logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test4/avatars/'
certs: 'test4/certs/' certs: 'test4/certs/'
videos: 'test4/videos/' videos: 'test4/videos/'
logs: 'test4/logs/' logs: 'test4/logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test5/avatars/'
certs: 'test5/certs/' certs: 'test5/certs/'
videos: 'test5/videos/' videos: 'test5/videos/'
logs: 'test5/logs/' logs: 'test5/logs/'

View File

@ -10,6 +10,7 @@ database:
# From the project root directory # From the project root directory
storage: storage:
avatars: 'test6/avatars/'
certs: 'test6/certs/' certs: 'test6/certs/'
videos: 'test6/videos/' videos: 'test6/videos/'
logs: 'test6/logs/' logs: 'test6/logs/'

View File

@ -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') PASSWORD: config.get<string>('database.password')
}, },
STORAGE: { STORAGE: {
AVATARS_DIR: join(root(), config.get<string>('storage.avatars')),
LOG_DIR: join(root(), config.get<string>('storage.logs')), LOG_DIR: join(root(), config.get<string>('storage.logs')),
VIDEOS_DIR: join(root(), config.get<string>('storage.videos')), VIDEOS_DIR: join(root(), config.get<string>('storage.videos')),
THUMBNAILS_DIR: join(root(), config.get<string>('storage.thumbnails')), 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.URL = CONFIG.WEBSERVER.SCHEME + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT
CONFIG.WEBSERVER.HOST = 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 = { const CONSTRAINTS_FIELDS = {
@ -356,6 +360,7 @@ export {
PREVIEWS_SIZE, PREVIEWS_SIZE,
REMOTE_SCHEME, REMOTE_SCHEME,
FOLLOW_STATES, FOLLOW_STATES,
AVATARS_DIR,
SEARCHABLE_COLUMNS, SEARCHABLE_COLUMNS,
SERVER_ACCOUNT_NAME, SERVER_ACCOUNT_NAME,
PRIVATE_RSA_KEY_SIZE, PRIVATE_RSA_KEY_SIZE,

View File

@ -2,6 +2,7 @@ import { join } from 'path'
import { flattenDepth } from 'lodash' import { flattenDepth } from 'lodash'
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { AvatarModel } from '../models/avatar'
import { CONFIG } from './constants' import { CONFIG } from './constants'
// Do not use barrel, we need to load database first // Do not use barrel, we need to load database first
@ -36,6 +37,7 @@ export type PeerTubeDatabase = {
init?: (silent: boolean) => Promise<void>, init?: (silent: boolean) => Promise<void>,
Application?: ApplicationModel, Application?: ApplicationModel,
Avatar?: AvatarModel,
Account?: AccountModel, Account?: AccountModel,
Job?: JobModel, Job?: JobModel,
OAuthClient?: OAuthClientModel, OAuthClient?: OAuthClientModel,

View 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
}

View File

@ -1,6 +1,7 @@
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { Account as FormattedAccount, ActivityPubActor } from '../../../shared' import { Account as FormattedAccount, ActivityPubActor } from '../../../shared'
import { AvatarInstance } from '../avatar'
import { ServerInstance } from '../server/server-interface' import { ServerInstance } from '../server/server-interface'
import { VideoChannelInstance } from '../video/video-channel-interface' import { VideoChannelInstance } from '../video/video-channel-interface'
@ -51,6 +52,7 @@ export interface AccountAttributes {
serverId?: number serverId?: number
userId?: number userId?: number
applicationId?: number applicationId?: number
avatarId?: number
} }
export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> { export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
@ -68,6 +70,7 @@ export interface AccountInstance extends AccountClass, AccountAttributes, Sequel
Server: ServerInstance Server: ServerInstance
VideoChannels: VideoChannelInstance[] VideoChannels: VideoChannelInstance[]
Avatar: AvatarInstance
} }
export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {} export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}

View File

@ -1,4 +1,6 @@
import { join } from 'path'
import * as Sequelize from 'sequelize' import * as Sequelize from 'sequelize'
import { Avatar } from '../../../shared/models/avatars/avatar.model'
import { import {
activityPubContextify, activityPubContextify,
isAccountFollowersCountValid, isAccountFollowersCountValid,
@ -8,8 +10,10 @@ import {
isUserUsernameValid isUserUsernameValid
} from '../../helpers' } from '../../helpers'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { AVATARS_DIR } from '../../initializers'
import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants' import { CONFIG, CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete' import { sendDeleteAccount } from '../../lib/activitypub/send/send-delete'
import { AvatarModel } from '../avatar'
import { addMethodsToModel } from '../utils' import { addMethodsToModel } from '../utils'
import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface' import { AccountAttributes, AccountInstance, AccountMethods } from './account-interface'
@ -252,6 +256,14 @@ function associate (models) {
as: 'followers', as: 'followers',
onDelete: 'cascade' onDelete: 'cascade'
}) })
Account.hasOne(models.Avatar, {
foreignKey: {
name: 'avatarId',
allowNull: true
},
onDelete: 'cascade'
})
} }
function afterDestroy (account: AccountInstance) { function afterDestroy (account: AccountInstance) {
@ -265,6 +277,15 @@ function afterDestroy (account: AccountInstance) {
toFormattedJSON = function (this: AccountInstance) { toFormattedJSON = function (this: AccountInstance) {
let host = CONFIG.WEBSERVER.HOST let host = CONFIG.WEBSERVER.HOST
let score: number 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) { if (this.Server) {
host = this.Server.host host = this.Server.host
@ -273,11 +294,15 @@ toFormattedJSON = function (this: AccountInstance) {
const json = { const json = {
id: this.id, id: this.id,
uuid: this.uuid,
host, host,
score, score,
name: this.name, name: this.name,
followingCount: this.followingCount,
followersCount: this.followersCount,
createdAt: this.createdAt, createdAt: this.createdAt,
updatedAt: this.updatedAt updatedAt: this.updatedAt,
avatar
} }
return json return json

View File

@ -157,10 +157,7 @@ toFormattedJSON = function (this: UserInstance) {
roleLabel: USER_ROLE_LABELS[this.role], roleLabel: USER_ROLE_LABELS[this.role],
videoQuota: this.videoQuota, videoQuota: this.videoQuota,
createdAt: this.createdAt, createdAt: this.createdAt,
account: { account: this.Account.toFormattedJSON()
id: this.Account.id,
uuid: this.Account.uuid
}
} }
if (Array.isArray(this.Account.VideoChannels) === true) { if (Array.isArray(this.Account.VideoChannels) === true) {

View 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> {}

View 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 ------------------------------

View File

@ -0,0 +1 @@
export * from './avatar-interface'

View File

@ -1,4 +1,5 @@
export * from './application' export * from './application'
export * from './avatar'
export * from './job' export * from './job'
export * from './oauth' export * from './oauth'
export * from './server' export * from './server'

View File

@ -1,5 +1,13 @@
import { Avatar } from '../avatars/avatar.model'
export interface Account { export interface Account {
id: number id: number
uuid: string
name: string name: string
host: string host: string
followingCount: number
followersCount: number
createdAt: Date
updatedAt: Date
avatar: Avatar
} }

View File

@ -0,0 +1,5 @@
export interface Avatar {
path: string
createdAt: Date | string
updatedAt: Date | string
}

View File

@ -1,3 +1,4 @@
import { Account } from '../accounts'
import { VideoChannel } from '../videos/video-channel.model' import { VideoChannel } from '../videos/video-channel.model'
import { UserRole } from './user-role' import { UserRole } from './user-role'
@ -8,10 +9,7 @@ export interface User {
displayNSFW: boolean displayNSFW: boolean
role: UserRole role: UserRole
videoQuota: number videoQuota: number
createdAt: Date, createdAt: Date
account: { account: Account
id: number
uuid: string
}
videoChannels?: VideoChannel[] videoChannels?: VideoChannel[]
} }