From: Chocobozzz Date: Mon, 4 Dec 2017 09:34:40 +0000 (+0100) Subject: Add account avatar X-Git-Tag: v0.0.1-alpha~149^2~42 X-Git-Url: https://git.librecmc.org/?a=commitdiff_plain;h=2295ce6c4e7ba805cc100ff961527bebc2cd89e5;p=oweals%2Fpeertube.git Add account avatar --- 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,30 +1,25 @@ +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 { AuthStatus } from './auth-status.model' -import { AuthUser } from './auth-user.model' -import { - OAuthClientLocal, - UserRole, - UserRefreshToken, - VideoChannel, - User as UserServerModel -} from '../../../../../shared' +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 { UserLogin } from '../../../../../shared/models/users/user-login.model' import { UserConstructorHash } from '../../shared/users/user.model' +import { AuthStatus } from './auth-status.model' +import { AuthUser } from './auth-user.model' + interface UserLoginWithUsername extends UserLogin { access_token: string refresh_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 000000000..4b7fd2c0a Binary files /dev/null and b/client/src/assets/default-avatar.png differ diff --git a/client/src/sass/_mixins.scss b/client/src/sass/_mixins.scss index 5798b8f6e..e44cf064d 100644 --- a/client/src/sass/_mixins.scss +++ b/client/src/sass/_mixins.scss @@ -39,3 +39,8 @@ @include peertube-button; @include disable-default-a-behaviour; } + +@mixin avatar ($size) { + width: $size; + height: $size; +} diff --git a/config/default.yaml b/config/default.yaml index b53fa0d5b..2c1043067 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -16,6 +16,7 @@ database: # From the project root directory storage: + avatars: 'avatars/' certs: 'certs/' videos: 'videos/' logs: 'logs/' diff --git a/config/production.yaml.example b/config/production.yaml.example index 1af20a9e4..404d35c16 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -17,6 +17,7 @@ database: # From the project root directory storage: + avatars: 'avatars/' certs: 'certs/' videos: 'videos/' logs: 'logs/' diff --git a/config/test-1.yaml b/config/test-1.yaml index d9b4d2b1a..49fbebf04 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test1/avatars/' certs: 'test1/certs/' videos: 'test1/videos/' logs: 'test1/logs/' diff --git a/config/test-2.yaml b/config/test-2.yaml index 236dcb10d..ff0df5962 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test2/avatars/' certs: 'test2/certs/' videos: 'test2/videos/' logs: 'test2/logs/' diff --git a/config/test-3.yaml b/config/test-3.yaml index 291b43edc..4fbb00050 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test3/avatars/' certs: 'test3/certs/' videos: 'test3/videos/' logs: 'test3/logs/' diff --git a/config/test-4.yaml b/config/test-4.yaml index 6f80939fc..e4f0f2691 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test4/avatars/' certs: 'test4/certs/' videos: 'test4/videos/' logs: 'test4/logs/' diff --git a/config/test-5.yaml b/config/test-5.yaml index 0b5eab72e..610f523c8 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test5/avatars/' certs: 'test5/certs/' videos: 'test5/videos/' logs: 'test5/logs/' diff --git a/config/test-6.yaml b/config/test-6.yaml index 5d33e45b9..088b55c17 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -10,6 +10,7 @@ database: # From the project root directory storage: + avatars: 'test6/avatars/' certs: 'test6/certs/' videos: 'test6/videos/' logs: 'test6/logs/' diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e3d779456..144a4edbf 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -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('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[] }