From 2295ce6c4e7ba805cc100ff961527bebc2cd89e5 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 4 Dec 2017 10:34:40 +0100 Subject: [PATCH] Add account avatar --- .../account-settings.component.html | 10 ++++- .../account-settings.component.scss | 21 ++++++++-- .../account-settings.component.ts | 4 ++ client/src/app/core/auth/auth.service.ts | 36 +++++++----------- client/src/app/menu/menu.component.html | 2 + client/src/app/menu/menu.component.scss | 8 +++- client/src/app/menu/menu.component.ts | 4 ++ client/src/app/shared/users/user.model.ts | 25 +++++------- client/src/assets/default-avatar.png | Bin 0 -> 1674 bytes client/src/sass/_mixins.scss | 5 +++ config/default.yaml | 1 + config/production.yaml.example | 1 + config/test-1.yaml | 1 + config/test-2.yaml | 1 + config/test-3.yaml | 1 + config/test-4.yaml | 1 + config/test-5.yaml | 1 + config/test-6.yaml | 1 + server/initializers/constants.ts | 7 +++- server/initializers/database.ts | 2 + .../migrations/0115-account-avatar.ts | 31 +++++++++++++++ server/models/account/account-interface.ts | 3 ++ server/models/account/account.ts | 27 ++++++++++++- server/models/account/user.ts | 5 +-- server/models/avatar/avatar-interface.ts | 16 ++++++++ server/models/avatar/avatar.ts | 24 ++++++++++++ server/models/avatar/index.ts | 1 + server/models/index.ts | 1 + shared/models/accounts/account.model.ts | 8 ++++ shared/models/avatars/avatar.model.ts | 5 +++ shared/models/users/user.model.ts | 8 ++-- 31 files changed, 207 insertions(+), 54 deletions(-) create mode 100644 client/src/assets/default-avatar.png create mode 100644 server/initializers/migrations/0115-account-avatar.ts create mode 100644 server/models/avatar/avatar-interface.ts create mode 100644 server/models/avatar/avatar.ts create mode 100644 server/models/avatar/index.ts create mode 100644 shared/models/avatars/avatar.model.ts diff --git a/client/src/app/account/account-settings/account-settings.component.html b/client/src/app/account/account-settings/account-settings.component.html index 2509eb5aa..9e9f688d2 100644 --- a/client/src/app/account/account-settings/account-settings.component.html +++ b/client/src/app/account/account-settings/account-settings.component.html @@ -1,7 +1,13 @@ -
- {{ user.username }} +
+ Avatar + +
+ diff --git a/client/src/app/account/account-settings/account-settings.component.scss b/client/src/app/account/account-settings/account-settings.component.scss index a0822631d..f514809b0 100644 --- a/client/src/app/account/account-settings/account-settings.component.scss +++ b/client/src/app/account/account-settings/account-settings.component.scss @@ -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 { diff --git a/client/src/app/account/account-settings/account-settings.component.ts b/client/src/app/account/account-settings/account-settings.component.ts index c3b670e02..cba251000 100644 --- a/client/src/app/account/account-settings/account-settings.component.ts +++ b/client/src/app/account/account-settings/account-settings.component.ts @@ -15,4 +15,8 @@ export class AccountSettingsComponent implements OnInit { ngOnInit () { this.user = this.authService.getUser() } + + getAvatarPath () { + return this.user.getAvatarPath() + } } diff --git a/client/src/app/core/auth/auth.service.ts b/client/src/app/core/auth/auth.service.ts index 9e6c6b888..fd2708c11 100644 --- a/client/src/app/core/auth/auth.service.ts +++ b/client/src/app/core/auth/auth.service.ts @@ -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[] } diff --git a/client/src/app/menu/menu.component.html b/client/src/app/menu/menu.component.html index 0ed8ec518..7a80fa4de 100644 --- a/client/src/app/menu/menu.component.html +++ b/client/src/app/menu/menu.component.html @@ -1,5 +1,7 @@
+ Avatar +
{{ user.username }}
{{ user.email }}
diff --git a/client/src/app/menu/menu.component.scss b/client/src/app/menu/menu.component.scss index 9d67ca66c..5d6fd61c6 100644 --- a/client/src/app/menu/menu.component.scss +++ b/client/src/app/menu/menu.component.scss @@ -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; diff --git a/client/src/app/menu/menu.component.ts b/client/src/app/menu/menu.component.ts index 4c35bb3a5..8b8b714a8 100644 --- a/client/src/app/menu/menu.component.ts +++ b/client/src/app/menu/menu.component.ts @@ -51,6 +51,10 @@ export class MenuComponent implements OnInit { ) } + getUserAvatarPath () { + return this.user.getAvatarPath() + } + isRegistrationAllowed () { return this.serverService.getConfig().signup.allowed } diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index b075ab717..83990d8b8 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts @@ -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' + } } diff --git a/client/src/assets/default-avatar.png b/client/src/assets/default-avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..4b7fd2c0a27ae12b4125e44a1eb80433206d4013 GIT binary patch literal 1674 zcmbtUdpHw%82)WD(}u00EtE=giDmAYPMaDw3nx0=8!@DgOH3omDk`T^x(E@KTSalE zb`n{cP*dbmvt((>lEN^|x%@Az5mmA`Pwg%Fg06|U~0|383r@C&A>l(}% z@HX6vX=uP|7YPG|g(o__Y{DqF^xBd4Z?772p2|F&^15U}O6S8%lpqWQagjR=H@s5M zRBkx>%iBjhyHK=A_GHqrT)PH^ieUcv*2W?y2n z_*^+M`Z3mGMBpeeo88SBUAF}?lF16pfP~CfX$Sh?X9g?i`KekMh6((F$?UlHIO^?U zq6Y2y;f~DO#V!zHnm!Rm$=g&a)auJy=}xsu<16Nw*in+#2++CMFt?$hQ_Yz90yhSG zx*1!fkAq5K%mNLhwD*)Lm=MjkXr;`g#xPb;mei=v_aXcI3*l6F|D|-O*UAk?Uv@!# z2I9bwW(9V>swu0sCT8#Ad|6&%l(HOoaJpF){sIghyBCjcJ}@}NgevuImQE#yG~CNN z;J7L24ZqRRxyeH_YHsM8sA>|3%X(cRcZQ!f&<*lU8hP4QxwwDcbhpBQv_H-#QWrd8 z7RzJH4d2H1x<<`f{!sVa^XKrgv4NLW*4H#g=Mp~s*7ITNm4iF6w7pk8Onp^8k*G5O zo%Jim4dn9mwuZ?+Fl=~jt-HQyu1`?vhhrS8*2I3Cd#>5pa1sSDdD&UjqMNa4o{{oU z>&s2M3Tdz28{G&hXn}xbwxl(BvALZV2egvPWC5!d#180w6xU=lE%4*-lWK#OeOaAD z}SX+lo8Jqwc<(pv+;{K2&+Uwl9Tcw zn8@XQzBlUgCR)IkcUzx9@BG9^a=$g&E*e9Wfc7POSmO85TU?c1*|%JG|7ylz-5Z3S zzhef|(_y|7cHqfDEUp4}6(D3+Ci{b~L&bIbKC!vk^)E{|_VDU)XJ>=a+)WNJX=4}O z+|S8d0Rrk4f#N`2O1~slXvKB=qV8&Xk-+@T_GG+5R({pV zvm+C~|8!aKs7@&LWn{?Bm^!;{OdGKZ4Vv}q#3^z@0FFC6k64`o1o0Pk<{uIDdggjX z%o(l#f^iA{3tw-xgO00-$62mE>mQAlk5%$TK1nnHBr4SQt~ToI?abXE>Y9EZS!dRq zJ&p=Jic-ChIg~0dHHNf)KL`LNDAGEIhsJd20}zEw8QmCyvxwaih&PUa0kG;a*`vta zkJ&SIyJP?Nr%ZpX!`=K=z^!QC7LC!oyH2mInyc7ypQ)qbm#QBt<^G|`1Kh^FFDk0? z1LjG0Jd3ovQKd-0|EH+@H9?%EQsYzurYP)E(IfRo3)1>ObUt)Rs*ml+Bgwx zYzajB%|vIC{TdR{&XQ('database.password') }, STORAGE: { + AVATARS_DIR: join(root(), config.get('storage.avatars')), LOG_DIR: join(root(), config.get('storage.logs')), VIDEOS_DIR: join(root(), config.get('storage.videos')), THUMBNAILS_DIR: join(root(), config.get('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, diff --git a/server/initializers/database.ts b/server/initializers/database.ts index 90dbba5b9..bb95992e1 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -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, Application?: ApplicationModel, + Avatar?: AvatarModel, Account?: AccountModel, Job?: JobModel, OAuthClient?: OAuthClientModel, diff --git a/server/initializers/migrations/0115-account-avatar.ts b/server/initializers/migrations/0115-account-avatar.ts new file mode 100644 index 000000000..e3531f5ce --- /dev/null +++ b/server/initializers/migrations/0115-account-avatar.ts @@ -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 { + 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 +} diff --git a/server/models/account/account-interface.ts b/server/models/account/account-interface.ts index b369766dc..46fe068e3 100644 --- a/server/models/account/account-interface.ts +++ b/server/models/account/account-interface.ts @@ -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 { @@ -68,6 +70,7 @@ export interface AccountInstance extends AccountClass, AccountAttributes, Sequel Server: ServerInstance VideoChannels: VideoChannelInstance[] + Avatar: AvatarInstance } export interface AccountModel extends AccountClass, Sequelize.Model {} diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 61a88524c..15be1126b 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -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 diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 8f7c9b013..3705947c0 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts @@ -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) { diff --git a/server/models/avatar/avatar-interface.ts b/server/models/avatar/avatar-interface.ts new file mode 100644 index 000000000..4af2b87b7 --- /dev/null +++ b/server/models/avatar/avatar-interface.ts @@ -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 { + createdAt: Date + updatedAt: Date +} + +export interface AvatarModel extends AvatarClass, Sequelize.Model {} diff --git a/server/models/avatar/avatar.ts b/server/models/avatar/avatar.ts new file mode 100644 index 000000000..3d329d888 --- /dev/null +++ b/server/models/avatar/avatar.ts @@ -0,0 +1,24 @@ +import * as Sequelize from 'sequelize' +import { addMethodsToModel } from '../utils' +import { AvatarAttributes, AvatarInstance, AvatarMethods } from './avatar-interface' + +let Avatar: Sequelize.Model + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + Avatar = sequelize.define('Avatar', + { + filename: { + type: DataTypes.STRING, + allowNull: false + } + }, + {} + ) + + const classMethods = [] + addMethodsToModel(Avatar, classMethods) + + return Avatar +} + +// ------------------------------ Statics ------------------------------ diff --git a/server/models/avatar/index.ts b/server/models/avatar/index.ts new file mode 100644 index 000000000..877aed1ce --- /dev/null +++ b/server/models/avatar/index.ts @@ -0,0 +1 @@ +export * from './avatar-interface' diff --git a/server/models/index.ts b/server/models/index.ts index 65faa5294..fedd97dd1 100644 --- a/server/models/index.ts +++ b/server/models/index.ts @@ -1,4 +1,5 @@ export * from './application' +export * from './avatar' export * from './job' export * from './oauth' export * from './server' diff --git a/shared/models/accounts/account.model.ts b/shared/models/accounts/account.model.ts index 338426dc7..d14701317 100644 --- a/shared/models/accounts/account.model.ts +++ b/shared/models/accounts/account.model.ts @@ -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 } diff --git a/shared/models/avatars/avatar.model.ts b/shared/models/avatars/avatar.model.ts new file mode 100644 index 000000000..301d00929 --- /dev/null +++ b/shared/models/avatars/avatar.model.ts @@ -0,0 +1,5 @@ +export interface Avatar { + path: string + createdAt: Date | string + updatedAt: Date | string +} diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index a8012734c..4b17881e5 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -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[] }