
* Rename MattermostTeam -> UniqueServer, MattermostTab -> UniqueView * Rename 'team' to 'server' * Some further cleanup * Rename weirdly named function * Rename 'tab' to 'view' in most instances * Fix i18n * PR feedback
379 lines
16 KiB
TypeScript
379 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 {FormattedMessage} from 'react-intl';
|
|
import classNames from 'classnames';
|
|
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
|
|
|
|
import {UniqueServer} from 'types/config';
|
|
|
|
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
|
|
|
|
import './css/dropdown.scss';
|
|
|
|
import IntlProvider from './intl_provider';
|
|
|
|
type State = {
|
|
servers?: UniqueServer[];
|
|
serverOrder?: string[];
|
|
orderedServers?: UniqueServer[];
|
|
activeServer?: string;
|
|
darkMode?: boolean;
|
|
enableServerManagement?: boolean;
|
|
unreads?: Map<string, boolean>;
|
|
mentions?: Map<string, number>;
|
|
expired?: Map<string, boolean>;
|
|
hasGPOServers?: 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 ServerDropdown 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.desktop.serverDropdown.onUpdateServerDropdown(this.handleUpdate);
|
|
}
|
|
|
|
handleUpdate = (
|
|
servers: UniqueServer[],
|
|
darkMode: boolean,
|
|
windowBounds: Electron.Rectangle,
|
|
activeServer?: string,
|
|
enableServerManagement?: boolean,
|
|
hasGPOServers?: boolean,
|
|
expired?: Map<string, boolean>,
|
|
mentions?: Map<string, number>,
|
|
unreads?: Map<string, boolean>,
|
|
) => {
|
|
this.setState({
|
|
servers,
|
|
activeServer,
|
|
darkMode,
|
|
enableServerManagement,
|
|
hasGPOServers,
|
|
unreads,
|
|
mentions,
|
|
expired,
|
|
windowBounds,
|
|
});
|
|
}
|
|
|
|
selectServer = (server: UniqueServer) => {
|
|
return () => {
|
|
if (!server.id) {
|
|
return;
|
|
}
|
|
window.desktop.serverDropdown.switchServer(server.id);
|
|
this.closeMenu();
|
|
};
|
|
}
|
|
|
|
closeMenu = () => {
|
|
if (!this.state.isAnyDragging) {
|
|
(document.activeElement as HTMLElement).blur();
|
|
window.desktop.closeServersDropdown();
|
|
}
|
|
}
|
|
|
|
preventPropagation = (event: React.MouseEvent<HTMLDivElement>) => {
|
|
event.stopPropagation();
|
|
}
|
|
|
|
addServer = () => {
|
|
window.desktop.serverDropdown.showNewServerModal();
|
|
this.closeMenu();
|
|
}
|
|
|
|
isActiveServer = (server: UniqueServer) => {
|
|
return server.id === this.state.activeServer;
|
|
}
|
|
|
|
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.servers) {
|
|
throw new Error('No config');
|
|
}
|
|
const serversCopy = this.state.servers.concat();
|
|
|
|
const server = serversCopy.splice(removedIndex, 1);
|
|
const newOrder = addedIndex < this.state.servers.length ? addedIndex : this.state.servers.length - 1;
|
|
serversCopy.splice(newOrder, 0, server[0]);
|
|
|
|
this.setState({servers: serversCopy, isAnyDragging: false});
|
|
window.desktop.updateServerOrder(serversCopy.map((server) => server.id!));
|
|
}
|
|
|
|
componentDidMount() {
|
|
window.desktop.serverDropdown.requestInfo();
|
|
window.addEventListener('click', this.closeMenu);
|
|
window.addEventListener('keydown', this.handleKeyboardShortcuts);
|
|
}
|
|
|
|
componentDidUpdate() {
|
|
window.desktop.serverDropdown.sendSize(document.body.scrollWidth, document.body.scrollHeight);
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('click', this.closeMenu);
|
|
window.removeEventListener('keydown', this.handleKeyboardShortcuts);
|
|
}
|
|
|
|
setButtonRef = (serverIndex: number, refMethod?: (element: HTMLButtonElement) => unknown) => {
|
|
return (ref: HTMLButtonElement) => {
|
|
this.addButtonRef(serverIndex, ref);
|
|
refMethod?.(ref);
|
|
};
|
|
}
|
|
|
|
addButtonRef = (serverIndex: number, ref: HTMLButtonElement | null) => {
|
|
if (ref) {
|
|
this.buttonRefs.set(serverIndex, ref);
|
|
ref.addEventListener('focusin', () => {
|
|
this.focusedIndex = serverIndex;
|
|
});
|
|
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 = (serverId: string) => {
|
|
if (this.serverIsPredefined(serverId)) {
|
|
return () => {};
|
|
}
|
|
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
window.desktop.serverDropdown.showEditServerModal(serverId);
|
|
this.closeMenu();
|
|
};
|
|
}
|
|
|
|
removeServer = (serverId: string) => {
|
|
if (this.serverIsPredefined(serverId)) {
|
|
return () => {};
|
|
}
|
|
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
|
event.stopPropagation();
|
|
window.desktop.serverDropdown.showRemoveServerModal(serverId);
|
|
this.closeMenu();
|
|
};
|
|
}
|
|
|
|
serverIsPredefined = (serverId: string) => {
|
|
return this.state.servers?.some((server) => server.id === serverId && server.isPredefined);
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<IntlProvider>
|
|
<div
|
|
onClick={this.preventPropagation}
|
|
className={classNames('ServerDropdown', {
|
|
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='ServerDropdown__header'>
|
|
<span className='ServerDropdown__servers'>
|
|
<FormattedMessage
|
|
id='renderer.dropdown.servers'
|
|
defaultMessage='Servers'
|
|
/>
|
|
</span>
|
|
<span className='ServerDropdown__keyboardShortcut'>
|
|
{window.process.platform === 'darwin' ? '⌃⌘S' : 'Ctrl + Shift + S'}
|
|
</span>
|
|
</div>
|
|
<hr className='ServerDropdown__divider'/>
|
|
<DragDropContext
|
|
onDragStart={this.onDragStart}
|
|
onDragEnd={this.onDragEnd}
|
|
>
|
|
<Droppable
|
|
isDropDisabled={this.state.hasGPOServers}
|
|
droppableId='ServerDropdown__droppable'
|
|
>
|
|
{(provided) => (
|
|
<div
|
|
className='ServerDropdown__droppable'
|
|
ref={provided.innerRef}
|
|
{...provided.droppableProps}
|
|
>
|
|
{this.state.servers?.map((server, orderedIndex) => {
|
|
const index = this.state.servers?.indexOf(server);
|
|
const sessionExpired = this.state.expired?.get(server.id!);
|
|
const hasUnreads = this.state.unreads?.get(server.id!);
|
|
const mentionCount = this.state.mentions?.get(server.id!);
|
|
|
|
let badgeDiv: React.ReactNode;
|
|
if (sessionExpired) {
|
|
badgeDiv = (
|
|
<div className='ServerDropdown__badge-expired'>
|
|
<i className='icon-alert-circle-outline'/>
|
|
</div>
|
|
);
|
|
} else if (mentionCount && mentionCount > 0) {
|
|
badgeDiv = (
|
|
<div className='ServerDropdown__badge-count'>
|
|
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
|
|
</div>
|
|
);
|
|
} else if (hasUnreads) {
|
|
badgeDiv = (
|
|
<div className='ServerDropdown__badge-dot'/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Draggable
|
|
key={index}
|
|
draggableId={`ServerDropdown__draggable-${index}`}
|
|
index={orderedIndex}
|
|
disableInteractiveElementBlocking={true}
|
|
>
|
|
{(provided, snapshot) => (
|
|
<button
|
|
className={classNames('ServerDropdown__button', {
|
|
dragging: snapshot.isDragging,
|
|
anyDragging: this.state.isAnyDragging,
|
|
active: this.isActiveServer(server),
|
|
})}
|
|
ref={this.setButtonRef(orderedIndex, provided.innerRef)}
|
|
{...provided.draggableProps}
|
|
onClick={this.selectServer(server)}
|
|
style={getStyle(provided.draggableProps.style)}
|
|
>
|
|
<div
|
|
className={classNames('ServerDropdown__draggable-handle', {
|
|
dragging: snapshot.isDragging,
|
|
})}
|
|
{...provided.dragHandleProps}
|
|
onClick={this.handleClickOnDragHandle}
|
|
>
|
|
<i className='icon-drag-vertical'/>
|
|
{this.isActiveServer(server) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
|
|
<span>{server.name}</span>
|
|
</div>
|
|
{!server.isPredefined && <div className='ServerDropdown__indicators'>
|
|
<button
|
|
className='ServerDropdown__button-edit'
|
|
onClick={this.editServer(server.id!)}
|
|
>
|
|
<i className='icon-pencil-outline'/>
|
|
</button>
|
|
<button
|
|
className='ServerDropdown__button-remove'
|
|
onClick={this.removeServer(server.id!)}
|
|
>
|
|
<i className='icon-trash-can-outline'/>
|
|
</button>
|
|
{badgeDiv && <div className='ServerDropdown__badge'>
|
|
{badgeDiv}
|
|
</div>}
|
|
</div>}
|
|
</button>
|
|
)}
|
|
</Draggable>
|
|
);
|
|
})}
|
|
{provided.placeholder}
|
|
</div>
|
|
)}
|
|
</Droppable>
|
|
</DragDropContext>
|
|
<hr className='ServerDropdown__divider'/>
|
|
{this.state.enableServerManagement &&
|
|
<button
|
|
ref={(ref) => {
|
|
this.addButtonRef(this.state.servers?.length || 0, ref);
|
|
}}
|
|
className='ServerDropdown__button addServer'
|
|
onClick={this.addServer}
|
|
>
|
|
<i className='icon-plus'/>
|
|
<FormattedMessage
|
|
id='renderer.dropdown.addAServer'
|
|
defaultMessage='Add a server'
|
|
/>
|
|
</button>
|
|
}
|
|
</div>
|
|
</IntlProvider>
|
|
);
|
|
}
|
|
}
|
|
|
|
ReactDOM.render(
|
|
<ServerDropdown/>,
|
|
document.getElementById('app'),
|
|
);
|