Files
mattermostest/src/renderer/dropdown.tsx
2021-09-02 14:05:54 -04:00

367 lines
16 KiB
TypeScript

// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {Team, TeamWithTabs} from 'types/config';
import {
CLOSE_TEAMS_DROPDOWN,
REQUEST_TEAMS_DROPDOWN_INFO,
SEND_DROPDOWN_MENU_SIZE,
SHOW_NEW_SERVER_MODAL,
SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL,
SWITCH_SERVER, UPDATE_TEAMS,
UPDATE_TEAMS_DROPDOWN,
} from 'common/communication';
import {getTabViewName} from 'common/tabs/TabView';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
import './css/dropdown.scss';
import './css/compass-icons.css';
type State = {
teams?: TeamWithTabs[];
orderedTeams?: TeamWithTabs[];
activeTeam?: string;
darkMode?: boolean;
enableServerManagement?: boolean;
unreads?: Map<string, boolean>;
mentions?: Map<string, number>;
expired?: Map<string, boolean>;
hasGPOTeams?: boolean;
isAnyDragging: boolean;
windowBounds?: Electron.Rectangle;
}
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
if (style?.transform) {
const axisLockY = `translate(0px${style.transform.slice(style.transform.indexOf(','), style.transform.length)}`;
return {
...style,
transform: axisLockY,
};
}
return style;
}
class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
buttonRefs: Map<number, HTMLButtonElement>;
addServerRef: React.RefObject<HTMLButtonElement>;
focusedIndex: number | null;
constructor(props: Record<string, never>) {
super(props);
this.state = {
isAnyDragging: false,
};
this.focusedIndex = null;
this.buttonRefs = new Map();
this.addServerRef = React.createRef();
window.addEventListener('message', this.handleMessageEvent);
}
handleMessageEvent = (event: MessageEvent) => {
if (event.data.type === UPDATE_TEAMS_DROPDOWN) {
const {teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, unreads, mentions, expired, windowBounds} = event.data.data;
this.setState({
teams,
orderedTeams: teams.concat().sort((a: TeamWithTabs, b: TeamWithTabs) => a.order - b.order),
activeTeam,
darkMode,
enableServerManagement,
hasGPOTeams,
unreads,
mentions,
expired,
windowBounds,
});
}
}
selectServer = (team: Team) => {
return () => {
window.postMessage({type: SWITCH_SERVER, data: team.name}, window.location.href);
this.closeMenu();
};
}
closeMenu = () => {
if (!this.state.isAnyDragging) {
(document.activeElement as HTMLElement).blur();
window.postMessage({type: CLOSE_TEAMS_DROPDOWN}, window.location.href);
}
}
preventPropogation = (event: React.MouseEvent<HTMLDivElement>) => {
event.stopPropagation();
}
addServer = () => {
window.postMessage({type: SHOW_NEW_SERVER_MODAL}, window.location.href);
this.closeMenu();
}
isActiveTeam = (team: Team) => {
return team.name === this.state.activeTeam;
}
onDragStart = () => {
this.setState({isAnyDragging: true});
}
onDragEnd = (result: DropResult) => {
const removedIndex = result.source.index;
const addedIndex = result.destination?.index;
if (addedIndex === undefined || removedIndex === addedIndex) {
this.setState({isAnyDragging: false});
return;
}
if (!this.state.teams) {
throw new Error('No config');
}
const teams = this.state.teams.concat();
const tabOrder = teams.map((team, index) => {
return {
index,
order: team.order,
};
}).sort((a, b) => (a.order - b.order));
const team = tabOrder.splice(removedIndex, 1);
const newOrder = addedIndex < this.state.teams.length ? addedIndex : this.state.teams.length - 1;
tabOrder.splice(newOrder, 0, team[0]);
tabOrder.forEach((t, order) => {
teams[t.index].order = order;
});
this.setState({teams, orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order), isAnyDragging: false});
window.postMessage({type: UPDATE_TEAMS, data: teams}, window.location.href);
}
componentDidMount() {
window.postMessage({type: REQUEST_TEAMS_DROPDOWN_INFO}, window.location.href);
window.addEventListener('click', this.closeMenu);
window.addEventListener('keydown', this.handleKeyboardShortcuts);
}
componentDidUpdate() {
window.postMessage({type: SEND_DROPDOWN_MENU_SIZE, data: {width: document.body.scrollWidth, height: document.body.scrollHeight}}, window.location.href);
}
componentWillUnmount() {
window.removeEventListener('click', this.closeMenu);
window.removeEventListener('keydown', this.handleKeyboardShortcuts);
}
setButtonRef = (teamIndex: number, refMethod?: (element: HTMLButtonElement) => any) => {
return (ref: HTMLButtonElement) => {
this.addButtonRef(teamIndex, ref);
refMethod?.(ref);
};
}
addButtonRef = (teamIndex: number, ref: HTMLButtonElement | null) => {
if (ref) {
this.buttonRefs.set(teamIndex, ref);
ref.addEventListener('focusin', () => {
this.focusedIndex = teamIndex;
});
ref.addEventListener('blur', () => {
this.focusedIndex = null;
});
}
}
handleKeyboardShortcuts = (event: KeyboardEvent) => {
if (event.key === 'ArrowDown') {
if (this.focusedIndex === null) {
this.focusedIndex = 0;
} else {
this.focusedIndex = (this.focusedIndex + 1) % this.buttonRefs.size;
}
this.buttonRefs.get(this.focusedIndex)?.focus();
}
if (event.key === 'ArrowUp') {
if (this.focusedIndex === null || this.focusedIndex === 0) {
this.focusedIndex = this.buttonRefs.size - 1;
} else {
this.focusedIndex = (this.focusedIndex - 1) % this.buttonRefs.size;
}
this.buttonRefs.get(this.focusedIndex)?.focus();
}
if (event.key === 'Escape') {
this.closeMenu();
}
this.buttonRefs.forEach((button, index) => {
if (event.key === String(index + 1)) {
button.focus();
}
});
}
handleClickOnDragHandle = (event: React.MouseEvent<HTMLDivElement>) => {
if (this.state.isAnyDragging) {
event.stopPropagation();
}
}
editServer = (team: string) => {
return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
window.postMessage({type: SHOW_EDIT_SERVER_MODAL, data: {name: team}}, window.location.href);
this.closeMenu();
};
}
removeServer = (team: string) => {
return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
window.postMessage({type: SHOW_REMOVE_SERVER_MODAL, data: {name: team}}, window.location.href);
this.closeMenu();
};
}
render() {
return (
<div
onClick={this.preventPropogation}
className={classNames('TeamDropdown', {
darkMode: this.state.darkMode,
})}
style={{
maxHeight: this.state.windowBounds ? (this.state.windowBounds.height - TAB_BAR_HEIGHT - 16) : undefined,
maxWidth: this.state.windowBounds ? (this.state.windowBounds.width - THREE_DOT_MENU_WIDTH_MAC) : undefined,
}}
>
<div className='TeamDropdown__header'>
<span>{'Servers'}</span>
</div>
<hr className='TeamDropdown__divider'/>
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<Droppable
isDropDisabled={this.state.hasGPOTeams}
droppableId='TeamDropdown__droppable'
>
{(provided) => (
<div
className='TeamDropdown__droppable'
ref={provided.innerRef}
{...provided.droppableProps}
>
{this.state.orderedTeams?.map((team, orderedIndex) => {
const index = this.state.teams?.indexOf(team);
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => {
const tabName = getTabViewName(team.name, tab.name);
counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired;
counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
return counts;
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
let badgeDiv: React.ReactNode;
if (sessionExpired) {
badgeDiv = (
<div className='TeamDropdown__badge-expired'>
<i className='icon-alert-circle-outline'/>
</div>
);
} else if (mentionCount && mentionCount > 0) {
badgeDiv = (
<div className='TeamDropdown__badge-count'>
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
</div>
);
} else if (hasUnreads) {
badgeDiv = (
<div className='TeamDropdown__badge-dot'/>
);
}
return (
<Draggable
key={index}
draggableId={`TeamDropdown__draggable-${index}`}
index={orderedIndex}
disableInteractiveElementBlocking={true}
>
{(provided, snapshot) => (
<button
className={classNames('TeamDropdown__button', {
dragging: snapshot.isDragging,
anyDragging: this.state.isAnyDragging,
active: this.isActiveTeam(team),
})}
ref={this.setButtonRef(orderedIndex, provided.innerRef)}
{...provided.draggableProps}
onClick={this.selectServer(team)}
style={getStyle(provided.draggableProps.style)}
>
<div
className={classNames('TeamDropdown__draggable-handle', {
dragging: snapshot.isDragging,
})}
{...provided.dragHandleProps}
onClick={this.handleClickOnDragHandle}
>
<i className='icon-drag-vertical'/>
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
<span>{team.name}</span>
</div>
<div className='TeamDropdown__indicators'>
<button
className='TeamDropdown__button-edit'
onClick={this.editServer(team.name)}
>
<i className='icon-pencil-outline'/>
</button>
<button
className='TeamDropdown__button-remove'
onClick={this.removeServer(team.name)}
>
<i className='icon-trash-can-outline'/>
</button>
{badgeDiv && <div className='TeamDropdown__badge'>
{badgeDiv}
</div>}
</div>
</button>
)}
</Draggable>
);
})}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
<hr className='TeamDropdown__divider'/>
{this.state.enableServerManagement &&
<button
ref={(ref) => {
this.addButtonRef(this.state.orderedTeams?.length || 0, ref);
}}
className='TeamDropdown__button addServer'
onClick={this.addServer}
>
<i className='icon-plus'/>
<span>{'Add a server'}</span>
</button>
}
</div>
);
}
}
ReactDOM.render(
<TeamDropdown/>,
document.getElementById('app'),
);