Skip to content

Commit

Permalink
refactor: React participant-avatar (#9828)
Browse files Browse the repository at this point in the history
  • Loading branch information
AndyLnd authored Oct 28, 2020
1 parent dd70307 commit c6dc0d7
Show file tree
Hide file tree
Showing 45 changed files with 1,380 additions and 674 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-redux": "7.2.1",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-transition-group": "4.4.1",
"redux": "4.0.5",
"redux-logdown": "1.0.4",
"redux-thunk": "2.3.0",
Expand Down Expand Up @@ -80,6 +81,7 @@
"@types/react-redux": "7.1.9",
"@types/react-router": "5.1.8",
"@types/react-router-dom": "5.1.6",
"@types/react-transition-group": "4.4.0",
"@types/redux-mock-store": "1.0.2",
"@types/sdp-transform": "2.4.4",
"@types/simplebar": "5.1.1",
Expand Down
2 changes: 1 addition & 1 deletion src/page/template/content/connect-requests.htm
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<div class="connect-request" data-bind="attr: {'data-uie-uid': id}" data-uie-name="connect-request">
<div class="connect-request-name ellipsis" data-bind="text: name()"></div>
<div class="connect-request-username label-username" data-bind="text: username()"></div>
<participant-avatar class="connect-request-avatar avatar-no-badge avatar-no-filter cursor-default" params="participant: $data, size: $parent.ParticipantAvatar.SIZE.X_LARGE"></participant-avatar>
<participant-avatar class="connect-request-avatar avatar-no-filter cursor-default" params="participant: $data, size: $parent.AVATAR_SIZE.X_LARGE, noBadge: true, noFilter: true"></participant-avatar>
<div class="button-group">
<div class="button button-inverted" data-bind="click: $parent.clickOnIgnore, text: t('connectionRequestIgnore')" data-uie-name="do-ignore"></div>
<div class="button" data-bind="click: $parent.clickOnAccept, text: t('connectionRequestConnect')" data-uie-name="do-accept"></div>
Expand Down
4 changes: 2 additions & 2 deletions src/page/template/content/preferences-account.htm
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@

<!-- ko if: isActivatedAccount() -->
<label class="preferences-account-picture-button" for="self-upload-file-input" data-bind="attr: {title: t('tooltipPreferencesPicture')}">
<participant-avatar params="participant: selfUser, size: ParticipantAvatar.SIZE.X_LARGE"></participant-avatar>
<participant-avatar class="see-through" params="participant: selfUser, size: AVATAR_SIZE.X_LARGE"></participant-avatar>
<input id="self-upload-file-input"
type="file"
data-bind="attr: {accept: Config.PROFILE_IMAGE.FILE_TYPES.join(',')}, file_select: clickOnChangePicture"
Expand All @@ -108,7 +108,7 @@

<!-- ko ifnot: isActivatedAccount() -->
<div>
<participant-avatar params="participant: selfUser, size: ParticipantAvatar.SIZE.X_LARGE"></participant-avatar>
<participant-avatar params="participant: selfUser, size: AVATAR_SIZE.X_LARGE"></participant-avatar>
</div>
<!-- /ko -->
</section>
Expand Down
99 changes: 99 additions & 0 deletions src/script/components/ParticipantAvatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* Wire
* Copyright (C) 2020 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import ParticipantAvatar, {ParticipantAvatarProps} from './ParticipantAvatar';
import TestPage from 'Util/test/TestPage';
import {User} from '../entity/User';
import {AssetRepository} from '../assets/AssetRepository';

class ParticipantAvatarPage extends TestPage<ParticipantAvatarProps> {
constructor(props?: ParticipantAvatarProps) {
super(ParticipantAvatar, props);
}

getUserParticpantAvatar = () => this.get('div[data-uie-name="element-avatar-user"]');
getServiceParticipantAvatar = () => this.get('div[data-uie-name="service-avatar"]');
getTemporaryGuestAvatar = () => this.get('div[data-uie-name="element-avatar-temporary-guest"]');
getServiceAvatar = () => this.get('div[data-uie-name="element-avatar-service"]');
getUserAvatar = () => this.get('div[data-uie-name="element-avatar-user"]');

clickUserAvatar = () => this.click(this.getUserParticpantAvatar());
}

describe('ParticipantAvatar', () => {
it('executes onClick with current participant', async () => {
const assetRepoSpy = (jasmine.createSpy() as unknown) as AssetRepository;
const participant = new User('id');
participant.name('Anton Bertha');

const participantAvatar = new ParticipantAvatarPage({
assetRepository: assetRepoSpy,
clickHandler: jasmine.createSpy(),
participant,
});

participantAvatar.clickUserAvatar();

expect(participantAvatar.getProps().clickHandler).toHaveBeenCalledWith(
participantAvatar.getProps().participant,
jasmine.anything(),
);
});

it('renders temporary guest avatar', async () => {
const assetRepoSpy = (jasmine.createSpy() as unknown) as AssetRepository;
const participant = new User('id');
participant.name('Anton Bertha');
participant.isTemporaryGuest(true);

const participantAvatar = new ParticipantAvatarPage({
assetRepository: assetRepoSpy,
participant,
});

expect(participantAvatar.getTemporaryGuestAvatar().exists()).toBe(true);
});

it('renders service avatar', async () => {
const assetRepoSpy = (jasmine.createSpy() as unknown) as AssetRepository;
const participant = new User('id');
participant.name('Anton Bertha');
participant.isService = true;

const participantAvatar = new ParticipantAvatarPage({
assetRepository: assetRepoSpy,
participant,
});

expect(participantAvatar.getServiceAvatar().exists()).toBe(true);
});

it('renders user avatar', async () => {
const assetRepoSpy = (jasmine.createSpy() as unknown) as AssetRepository;
const participant = new User('id');
participant.name('Anton Bertha');

const participantAvatar = new ParticipantAvatarPage({
assetRepository: assetRepoSpy,
participant,
});

expect(participantAvatar.getUserAvatar().exists()).toBe(true);
});
});
156 changes: 156 additions & 0 deletions src/script/components/ParticipantAvatar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Wire
* Copyright (C) 2020 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import React from 'react';

import {User} from '../entity/User';
import {ServiceEntity} from '../integration/ServiceEntity';
import {AssetRepository} from '../assets/AssetRepository';
import {registerReactComponent} from 'Util/ComponentUtil';

import UserAvatar from './participantAvatar/UserAvatar';
import ServiceAvatar from './participantAvatar/ServiceAvatar';
import TemporaryGuestAvatar from './participantAvatar/TemporaryGuestAvatar';

export enum AVATAR_SIZE {
LARGE = 'avatar-l',
MEDIUM = 'avatar-m',
SMALL = 'avatar-s',
X_LARGE = 'avatar-xl',
X_SMALL = 'avatar-xs',
XX_SMALL = 'avatar-xxs',
XXX_SMALL = 'avatar-xxxs',
}

export enum STATE {
BLOCKED = 'blocked',
IGNORED = 'ignored',
NONE = '',
PENDING = 'pending',
SELECTED = 'selected',
SELF = 'self',
UNKNOWN = 'unknown',
}

export const DIAMETER = {
[AVATAR_SIZE.LARGE]: 72,
[AVATAR_SIZE.MEDIUM]: 40,
[AVATAR_SIZE.SMALL]: 28,
[AVATAR_SIZE.X_LARGE]: 200,
[AVATAR_SIZE.X_SMALL]: 24,
[AVATAR_SIZE.XX_SMALL]: 20,
[AVATAR_SIZE.XXX_SMALL]: 16,
};

export const INITIALS_SIZE = {
[AVATAR_SIZE.LARGE]: '24px',
[AVATAR_SIZE.MEDIUM]: '16px',
[AVATAR_SIZE.SMALL]: '11px',
[AVATAR_SIZE.X_LARGE]: '32px',
[AVATAR_SIZE.X_SMALL]: '11px',
[AVATAR_SIZE.XX_SMALL]: '11px',
[AVATAR_SIZE.XXX_SMALL]: '8px',
};

export interface ParticipantAvatarProps {
assetRepository: AssetRepository;
clickHandler?: (participant: User, target: Node) => void;
noBadge?: boolean;
noFilter?: boolean;
participant: User;
size?: AVATAR_SIZE;
}

const ParticipantAvatar: React.FunctionComponent<ParticipantAvatarProps> = ({
assetRepository,
participant,
clickHandler,
noBadge = false,
noFilter = false,
size = AVATAR_SIZE.LARGE,
}) => {
const isUser = participant instanceof User && !participant.isService && !participant.isTemporaryGuest();
const isService = participant instanceof ServiceEntity || participant.isService;
const isTemporaryGuest = !isService && participant.isTemporaryGuest();

const avatarState = (() => {
switch (true) {
case isService:
return STATE.NONE;
case participant.isMe:
return STATE.SELF;
case participant.isTeamMember():
return STATE.NONE;
case participant.isBlocked():
return STATE.BLOCKED;
case participant.isRequest():
return STATE.PENDING;
case participant.isIgnored():
return STATE.IGNORED;
case participant.isCanceled() || participant.isUnknown():
return STATE.UNKNOWN;
default:
return STATE.NONE;
}
})();

const onClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
if (typeof clickHandler === 'function') {
clickHandler(participant, (event.currentTarget as Node).parentNode);
}
};

if (isUser) {
return (
<UserAvatar
size={size}
assetRepository={assetRepository}
noBadge={noBadge}
noFilter={noFilter}
participant={participant}
state={avatarState}
onClick={onClick}
/>
);
}

if (isTemporaryGuest) {
return (
<TemporaryGuestAvatar
noBadge={noBadge}
participant={participant}
state={avatarState}
size={size}
onClick={onClick}
/>
);
}

return <ServiceAvatar assetRepository={assetRepository} size={size} participant={participant} onClick={onClick} />;
};

export default ParticipantAvatar;

registerReactComponent('participant-avatar', {
component: ParticipantAvatar,
injected: {assetRepository: AssetRepository},
optionalParams: ['size', 'click', 'noBadge', 'noFilter'],
template:
'<span data-bind="react: {assetRepository, participant: ko.unwrap(participant), size, clickHandler: click, noBadge, noFilter}"></span>',
});
8 changes: 4 additions & 4 deletions src/script/components/fullSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {debounce, escape} from 'underscore';
import {isScrolledBottom} from 'Util/scroll-helpers';
import {formatDateShort} from 'Util/TimeUtil';

import {ParticipantAvatar} from 'Components/participantAvatar';
import {AVATAR_SIZE} from 'Components/ParticipantAvatar';
import {getSearchRegex} from '../search/FullTextSearch';
import type {Message} from '../entity/message/Message';
import type {ContentMessage} from '../entity/message/ContentMessage';
Expand All @@ -40,7 +40,7 @@ class FullSearch {
inputSubscription: ko.Subscription;
messageEntities: Message[];
params: FullSearchParams;
ParticipantAvatar: typeof ParticipantAvatar;
AVATAR_SIZE: typeof AVATAR_SIZE;
searchProvider: (query: string) => Promise<{messageEntities: Message[]; query: string}>;
showNoResultsText: ko.Observable<boolean>;
visibleMessageEntities: ko.ObservableArray<Message>;
Expand All @@ -56,7 +56,7 @@ class FullSearch {

constructor(params: FullSearchParams) {
this.searchProvider = params.search_provider;
this.ParticipantAvatar = ParticipantAvatar;
this.AVATAR_SIZE = AVATAR_SIZE;
this.params = params;

this.messageEntities = [];
Expand Down Expand Up @@ -178,7 +178,7 @@ ko.components.register('full-search', {
<div class="full-search-list" data-bind="foreach: {data: visibleMessageEntities, as: 'messageEntity', noChildContext: true}" data-uie-name="full-search-list">
<div class="full-search-item" data-bind="click: () => clickOnMessage(messageEntity)" data-uie-name="full-search-item">
<div class="full-search-item-avatar">
<participant-avatar params="participant: messageEntity.user, size: ParticipantAvatar.SIZE.X_SMALL"></participant-avatar>
<participant-avatar params="participant: messageEntity.user, size: AVATAR_SIZE.X_SMALL"></participant-avatar>
</div>
<div class="full-search-item-content">
<div class="full-search-item-content-text ellipsis" data-bind="html: htmlFormatResult(messageEntity)" data-uie-name="full-search-item-text"></div>
Expand Down
8 changes: 4 additions & 4 deletions src/script/components/groupList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import ko from 'knockout';

import {ParticipantAvatar} from 'Components/participantAvatar';
import {AVATAR_SIZE} from 'Components/ParticipantAvatar';
import type {Conversation} from '../entity/Conversation';
import {generateConversationUrl} from '../router/routeGenerator';

Expand All @@ -31,13 +31,13 @@ interface GroupListViewModelParams {
class GroupListViewModel {
groups: ko.ObservableArray<Conversation[]>;
onSelect: (group: Conversation) => void;
ParticipantAvatar: typeof ParticipantAvatar;
AVATAR_SIZE: typeof AVATAR_SIZE;
readonly getConversationUrl: (conversationId: string) => string;

constructor(params: GroupListViewModelParams) {
this.groups = params.groups;
this.onSelect = params.click;
this.ParticipantAvatar = ParticipantAvatar;
this.AVATAR_SIZE = AVATAR_SIZE;
this.getConversationUrl = generateConversationUrl;
}
}
Expand All @@ -49,7 +49,7 @@ ko.components.register('group-list', {
<div class="search-list-item" data-bind="link_to: getConversationUrl(group.id), click: () => onSelect(group), attr: {'data-uie-uid': group.id, 'data-uie-value': group.display_name}" data-uie-name="item-group">
<div class="search-list-item-image">
<!-- ko if: group.is1to1() -->
<participant-avatar params="participant: group.participating_user_ets()[0], size: ParticipantAvatar.SIZE.SMALL"></participant-avatar>
<participant-avatar params="participant: group.participating_user_ets()[0], size: AVATAR_SIZE.SMALL"></participant-avatar>
<!-- /ko -->
<!-- ko ifnot: group.is1to1() -->
<group-avatar params="users: group.participating_user_ets()"></group-avatar>
Expand Down
Loading

0 comments on commit c6dc0d7

Please sign in to comment.