| /** |
| * @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-model/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; |
| } |
| } |