-<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>
-.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 {
ngOnInit () {
this.user = this.authService.getUser()
}
+
+ getAvatarPath () {
+ return this.user.getAvatarPath()
+ }
}
+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
displayNSFW: boolean
email: string
videoQuota: number
- account: {
- id: number
- uuid: string
- }
+ account: Account
videoChannels: VideoChannel[]
}
<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>
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;
)
}
+ getUserAvatarPath () {
+ return this.user.getAvatarPath()
+ }
+
isRegistrationAllowed () {
return this.serverService.getConfig().signup.allowed
}
-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,
videoQuota?: number,
displayNSFW?: boolean,
createdAt?: Date,
- account?: {
- id: number
- uuid: string
- },
+ account?: Account,
videoChannels?: VideoChannel[]
}
export class User implements UserServerModel {
role: UserRole
displayNSFW: boolean
videoQuota: number
- account: {
- id: number
- uuid: string
- }
+ account: Account
videoChannels: VideoChannel[]
createdAt: Date
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'
+ }
}
@include peertube-button;
@include disable-default-a-behaviour;
}
+
+@mixin avatar ($size) {
+ width: $size;
+ height: $size;
+}
# From the project root directory
storage:
+ avatars: 'avatars/'
certs: 'certs/'
videos: 'videos/'
logs: 'logs/'
# From the project root directory
storage:
+ avatars: 'avatars/'
certs: 'certs/'
videos: 'videos/'
logs: 'logs/'
# From the project root directory
storage:
+ avatars: 'test1/avatars/'
certs: 'test1/certs/'
videos: 'test1/videos/'
logs: 'test1/logs/'
# From the project root directory
storage:
+ avatars: 'test2/avatars/'
certs: 'test2/certs/'
videos: 'test2/videos/'
logs: 'test2/logs/'
# From the project root directory
storage:
+ avatars: 'test3/avatars/'
certs: 'test3/certs/'
videos: 'test3/videos/'
logs: 'test3/logs/'
# From the project root directory
storage:
+ avatars: 'test4/avatars/'
certs: 'test4/certs/'
videos: 'test4/videos/'
logs: 'test4/logs/'
# From the project root directory
storage:
+ avatars: 'test5/avatars/'
certs: 'test5/certs/'
videos: 'test5/videos/'
logs: 'test5/logs/'
# From the project root directory
storage:
+ avatars: 'test6/avatars/'
certs: 'test6/certs/'
videos: 'test6/videos/'
logs: 'test6/logs/'
// ---------------------------------------------------------------------------
-const LAST_MIGRATION_VERSION = 110
+const LAST_MIGRATION_VERSION = 115
// ---------------------------------------------------------------------------
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')),
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 = {
PREVIEWS_SIZE,
REMOTE_SCHEME,
FOLLOW_STATES,
+ AVATARS_DIR,
SEARCHABLE_COLUMNS,
SERVER_ACCOUNT_NAME,
PRIVATE_RSA_KEY_SIZE,
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
init?: (silent: boolean) => Promise<void>,
Application?: ApplicationModel,
+ Avatar?: AvatarModel,
Account?: AccountModel,
Job?: JobModel,
OAuthClient?: OAuthClientModel,
--- /dev/null
+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
+}
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'
serverId?: number
userId?: number
applicationId?: number
+ avatarId?: number
}
export interface AccountInstance extends AccountClass, AccountAttributes, Sequelize.Instance<AccountAttributes> {
Server: ServerInstance
VideoChannels: VideoChannelInstance[]
+ Avatar: AvatarInstance
}
export interface AccountModel extends AccountClass, Sequelize.Model<AccountInstance, AccountAttributes> {}
+import { join } from 'path'
import * as Sequelize from 'sequelize'
+import { Avatar } from '../../../shared/models/avatars/avatar.model'
import {
activityPubContextify,
isAccountFollowersCountValid,
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'
as: 'followers',
onDelete: 'cascade'
})
+
+ Account.hasOne(models.Avatar, {
+ foreignKey: {
+ name: 'avatarId',
+ allowNull: true
+ },
+ onDelete: 'cascade'
+ })
}
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
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
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) {
--- /dev/null
+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> {}
--- /dev/null
+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 ------------------------------
--- /dev/null
+export * from './avatar-interface'
export * from './application'
+export * from './avatar'
export * from './job'
export * from './oauth'
export * from './server'
+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
}
--- /dev/null
+export interface Avatar {
+ path: string
+ createdAt: Date | string
+ updatedAt: Date | string
+}
+import { Account } from '../accounts'
import { VideoChannel } from '../videos/video-channel.model'
import { UserRole } from './user-role'
displayNSFW: boolean
role: UserRole
videoQuota: number
- createdAt: Date,
- account: {
- id: number
- uuid: string
- }
+ createdAt: Date
+ account: Account
videoChannels?: VideoChannel[]
}