blob: b445f757a72557e69553d3b999221f0a23fa0020 [file] [log] [blame]
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import './gr-avatar';
import '../gr-hovercard-account/gr-hovercard-account';
import {AccountInfo, ServerInfo} from '../../../types/common';
import {LitElement, PropertyValues, css, html} from 'lit';
import {customElement, property, state} from 'lit/decorators.js';
import {
isDetailedAccount,
uniqueAccountId,
uniqueDefinedAvatar,
} from '../../../utils/account-util';
import {resolve} from '../../../models/dependency';
import {configModelToken} from '../../../models/config/config-model';
import {subscribe} from '../../lit/subscription-controller';
import {getDisplayName} from '../../../utils/display-name-util';
import {accountsModelToken} from '../../../models/accounts/accounts-model';
import {isDefined} from '../../../types/types';
import {when} from 'lit/directives/when.js';
/**
* This elements draws stack of avatars overlapped with each other.
*
* If accounts is empty or contains accounts with more than MAX_STACK unique
* avatars the fallback slot is rendered instead.
*
* Style parameters:
* --avatar-size: size of the individual avatars. (Default: 16px)
* --stack-border-color: border of individual avatars in stack.
* (Default: #ffffff)
*/
@customElement('gr-avatar-stack')
export class GrAvatarStack extends LitElement {
static readonly MAX_STACK = 4;
@property({type: Array})
accounts: AccountInfo[] = [];
@state()
detailedAccounts: AccountInfo[] = [];
/**
* The size of requested image in px.
*
* By default this also controls avatarSize.
*/
@property({type: Number})
imageSize = 16;
/**
* Whether a hover-card should be shown for each avatar when hovered
*/
@property({type: Boolean})
enableHover = false;
/**
* In gr-app, gr-account-chip is in charge of loading a full account, so
* avatars will be set. However, code-owners will create gr-avatars with a
* bare account-id. To enable fetching of those avatars, a flag is added to
* gr-avatar-stack that will fetch the accounts on demand
*/
@property({type: Boolean})
forceFetch = false;
private readonly getAccountsModel = resolve(this, accountsModelToken);
@state() config?: ServerInfo;
static override get styles() {
return [
css`
gr-avatar {
box-sizing: border-box;
vertical-align: top;
height: var(--avatar-size, 16px);
width: var(--avatar-size, 16px);
border: solid 1px var(--stack-border-color, transparent);
}
gr-avatar:not(:first-child) {
margin-left: calc((var(--avatar-size, 16px) / -2));
}
`,
];
}
private readonly getConfigModel = resolve(this, configModelToken);
constructor() {
super();
subscribe(
this,
() => this.getConfigModel().serverConfig$,
config => (this.config = config)
);
}
override updated(changedProperties: PropertyValues) {
if (changedProperties.has('accounts')) {
if (
this.forceFetch &&
this.accounts.length > 0 &&
this.accounts.some(a => !isDetailedAccount(a))
) {
Promise.all(
this.accounts.map(account =>
this.getAccountsModel().fillDetails(account)
)
).then(accounts => {
// Only keep the detailed accounts as only those will be shown.
// It is possible for the server to return an empty account with just an account-id.
// This could be due to the fact that the user does not have permission to see this account.
this.detailedAccounts = accounts.filter(
a => isDefined(a) && isDetailedAccount(a)
);
});
} else {
this.detailedAccounts = this.accounts;
}
}
}
override render() {
const uniqueAvatarAccounts = this.forceFetch
? this.detailedAccounts.filter(uniqueAccountId)
: this.detailedAccounts
.filter(account => !!account?.avatars?.[0]?.url)
.filter(uniqueDefinedAvatar);
const hasAvatars = this.config?.plugin?.has_avatars ?? false;
if (
!hasAvatars ||
uniqueAvatarAccounts.length === 0 ||
uniqueAvatarAccounts.length > GrAvatarStack.MAX_STACK
) {
return html`<slot name="fallback"></slot>`;
}
return uniqueAvatarAccounts.map(
account =>
html`<gr-avatar
.account=${account}
.imageSize=${this.imageSize}
aria-label=${getDisplayName(this.config, account)}
>
${when(
this.enableHover,
() =>
html`<gr-hovercard-account
.account=${account}
></gr-hovercard-account>`
)}
</gr-avatar>`
);
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-avatar-stack': GrAvatarStack;
}
}