mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM-27936: Allow resizing the channel sidebar and right-hand sidebar (#23117)
* MM-27936: Resize sidebar * separate sidebar related constants into different file * reset style when sidebar expanded * change not to save user_id when saving to local_storage * prevent a separation between the rhs and the main center panel * change variable name * migrate to css-based paradigm * delete unnecessary files * update snapshot * increase the resizable range * fix transition * Made RHS and LHS widths not be saved scoped by team * scope cleanup * fix lhs header menu ellipsis/tolerances * fix lint * remove a ref from a dependency * return null instead of empty jsx * return null when disabled --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Caleb Roseland <caleb@calebroseland.com>
This commit is contained in:
parent
b84236d555
commit
61473efb93
@ -7,8 +7,38 @@ import {getCurrentRelativeTeamUrl} from 'mattermost-redux/selectors/entities/tea
|
|||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
import {LhsItemType} from 'types/store/lhs';
|
import {LhsItemType} from 'types/store/lhs';
|
||||||
import {ActionTypes} from 'utils/constants';
|
import Constants, {ActionTypes} from 'utils/constants';
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
|
export const setLhsSize = (sidebarSize?: SidebarSize) => {
|
||||||
|
let newSidebarSize = sidebarSize;
|
||||||
|
if (!sidebarSize) {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.SMALL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.MEDIUM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.LARGE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
newSidebarSize = SidebarSize.XLARGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: ActionTypes.SET_LHS_SIZE,
|
||||||
|
size: newSidebarSize,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const toggle = () => ({
|
export const toggle = () => ({
|
||||||
type: ActionTypes.TOGGLE_LHS,
|
type: ActionTypes.TOGGLE_LHS,
|
||||||
|
@ -33,6 +33,7 @@ import {GlobalState} from 'types/store';
|
|||||||
import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts';
|
import {getPostsByIds, getPost as fetchPost} from 'mattermost-redux/actions/posts';
|
||||||
|
|
||||||
import {getChannel} from 'mattermost-redux/actions/channels';
|
import {getChannel} from 'mattermost-redux/actions/channels';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) {
|
function selectPostFromRightHandSideSearchWithPreviousState(post: Post, previousRhsState?: RhsState) {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
@ -138,6 +139,35 @@ export function updateSearchTerms(terms: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setRhsSize(rhsSize?: SidebarSize) {
|
||||||
|
let newSidebarSize = rhsSize;
|
||||||
|
if (!newSidebarSize) {
|
||||||
|
const width = window.innerWidth;
|
||||||
|
|
||||||
|
switch (true) {
|
||||||
|
case width <= Constants.SMALL_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.SMALL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width > Constants.SMALL_SIDEBAR_BREAKPOINT && width <= Constants.MEDIUM_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.MEDIUM;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case width > Constants.MEDIUM_SIDEBAR_BREAKPOINT && width <= Constants.LARGE_SIDEBAR_BREAKPOINT: {
|
||||||
|
newSidebarSize = SidebarSize.LARGE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
newSidebarSize = SidebarSize.XLARGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: ActionTypes.SET_RHS_SIZE,
|
||||||
|
size: newSidebarSize,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function updateSearchTermsForShortcut() {
|
export function updateSearchTermsForShortcut() {
|
||||||
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
const currentChannelName = getCurrentChannelNameForSearchShortcut(getState());
|
const currentChannelName = getCurrentChannelNameForSearchShortcut(getState());
|
||||||
|
@ -129,6 +129,8 @@ interface FormattingBarProps {
|
|||||||
additionalControls?: React.ReactNodeArray;
|
additionalControls?: React.ReactNodeArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MIN_MODE_X_COORD = 55;
|
||||||
|
|
||||||
const FormattingBar = (props: FormattingBarProps): JSX.Element => {
|
const FormattingBar = (props: FormattingBarProps): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
applyMarkdown,
|
applyMarkdown,
|
||||||
@ -224,10 +226,12 @@ const FormattingBar = (props: FormattingBarProps): JSX.Element => {
|
|||||||
}
|
}
|
||||||
}, [getCurrentSelection, getCurrentMessage, applyMarkdown, showHiddenControls, toggleHiddenControls, disableControls]);
|
}, [getCurrentSelection, getCurrentMessage, applyMarkdown, showHiddenControls, toggleHiddenControls, disableControls]);
|
||||||
|
|
||||||
|
const leftPosition = wideMode === 'min' ? (x ?? 0) + DEFAULT_MIN_MODE_X_COORD : x ?? 0;
|
||||||
|
|
||||||
const hiddenControlsContainerStyles: React.CSSProperties = {
|
const hiddenControlsContainerStyles: React.CSSProperties = {
|
||||||
position: strategy,
|
position: strategy,
|
||||||
top: y ?? 0,
|
top: y ?? 0,
|
||||||
left: x ?? 0,
|
left: leftPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
const showSeparators = wideMode === 'wide';
|
const showSeparators = wideMode === 'wide';
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useEffect, useLayoutEffect, useState} from 'react';
|
import React, {useCallback, useEffect, useLayoutEffect, useState} from 'react';
|
||||||
import {Instance} from '@popperjs/core';
|
import {Instance} from '@popperjs/core';
|
||||||
|
|
||||||
import {debounce} from 'lodash';
|
import {debounce} from 'lodash';
|
||||||
|
|
||||||
import {MarkdownMode} from 'utils/markdown/apply_markdown';
|
import {MarkdownMode} from 'utils/markdown/apply_markdown';
|
||||||
|
|
||||||
type WideMode = 'wide' | 'normal' | 'narrow';
|
type WideMode = 'wide' | 'normal' | 'narrow' | 'min';
|
||||||
|
|
||||||
export function useGetLatest<T>(val: T) {
|
export function useGetLatest<T>(val: T) {
|
||||||
const ref = React.useRef<T>(val);
|
const ref = React.useRef<T>(val);
|
||||||
@ -18,23 +18,24 @@ export function useGetLatest<T>(val: T) {
|
|||||||
|
|
||||||
const useResponsiveFormattingBar = (ref: React.RefObject<HTMLDivElement>): WideMode => {
|
const useResponsiveFormattingBar = (ref: React.RefObject<HTMLDivElement>): WideMode => {
|
||||||
const [wideMode, setWideMode] = useState<WideMode>('wide');
|
const [wideMode, setWideMode] = useState<WideMode>('wide');
|
||||||
const handleResize = debounce(() => {
|
const handleResize = useCallback(debounce(() => {
|
||||||
if (ref.current?.clientWidth === undefined) {
|
if (ref.current?.clientWidth === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.current.clientWidth > 640) {
|
if (ref.current.clientWidth > 640) {
|
||||||
setWideMode('wide');
|
setWideMode('wide');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.current.clientWidth >= 424 && ref.current.clientWidth <= 640) {
|
if (ref.current.clientWidth >= 424 && ref.current.clientWidth <= 640) {
|
||||||
setWideMode('normal');
|
setWideMode('normal');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ref.current.clientWidth < 424) {
|
if (ref.current.clientWidth < 424) {
|
||||||
setWideMode('narrow');
|
setWideMode('narrow');
|
||||||
}
|
}
|
||||||
}, 10);
|
|
||||||
|
if (ref.current.clientWidth < 310) {
|
||||||
|
setWideMode('min');
|
||||||
|
}
|
||||||
|
}, 10), []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
@ -58,6 +59,7 @@ const MAP_WIDE_MODE_TO_CONTROLS_QUANTITY: {[key in WideMode]: number} = {
|
|||||||
wide: 9,
|
wide: 9,
|
||||||
normal: 5,
|
normal: 5,
|
||||||
narrow: 3,
|
narrow: 3,
|
||||||
|
min: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useFormattingBarControls = (
|
export const useFormattingBarControls = (
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export enum SidebarSize {
|
||||||
|
SMALL='small',
|
||||||
|
MEDIUM= 'medium',
|
||||||
|
LARGE= 'large',
|
||||||
|
XLARGE= 'xLarge',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ResizeDirection {
|
||||||
|
LEFT = 'left',
|
||||||
|
RIGHT = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CssVarKeyForResizable {
|
||||||
|
LHS = 'overrideLhsWidth',
|
||||||
|
RHS = 'overrideRhsWidth',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SIDEBAR_SNAP_SIZE = 16;
|
||||||
|
export const SIDEBAR_SNAP_SPEED_LIMIT = 5;
|
||||||
|
|
||||||
|
export const DEFAULT_LHS_WIDTH = 240;
|
||||||
|
|
||||||
|
export const RHS_MIN_MAX_WIDTH: { [size in SidebarSize]: { min: number; max: number; default: number}} = {
|
||||||
|
[SidebarSize.SMALL]: {
|
||||||
|
min: 400,
|
||||||
|
max: 400,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
[SidebarSize.MEDIUM]: {
|
||||||
|
min: 304,
|
||||||
|
max: 400,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
[SidebarSize.LARGE]: {
|
||||||
|
min: 304,
|
||||||
|
max: 464,
|
||||||
|
default: 400,
|
||||||
|
},
|
||||||
|
[SidebarSize.XLARGE]: {
|
||||||
|
min: 304,
|
||||||
|
max: 776,
|
||||||
|
default: 500,
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,267 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, {MouseEventHandler, RefObject, useEffect, useRef, useState} from 'react';
|
||||||
|
import styled, {createGlobalStyle, css} from 'styled-components';
|
||||||
|
import {CssVarKeyForResizable, ResizeDirection} from './constants';
|
||||||
|
import {useGlobalState} from 'stores/hooks';
|
||||||
|
import {isSizeLessThanSnapSize, isSnapableSpeed, shouldSnapWhenSizeGrown, shouldSnapWhenSizeShrunk} from './utils';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
defaultWidth: number;
|
||||||
|
globalCssVar: CssVarKeyForResizable;
|
||||||
|
dir: ResizeDirection;
|
||||||
|
containerRef: RefObject<HTMLElement>;
|
||||||
|
onResizeStart?: (startWidth: number) => void;
|
||||||
|
onResize?: (width: number, cssVarProperty: string, cssVarValue: string) => void;
|
||||||
|
onResizeEnd?: (finalWidth: number, cssVarProperty: string, cssVarValue: string) => void;
|
||||||
|
onDividerDoubleClick?: (prevWidth: number, cssVarProperty: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Divider = styled.div<{isActive: boolean}>`
|
||||||
|
position: absolute;
|
||||||
|
z-index: 50;
|
||||||
|
top: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 100%;
|
||||||
|
cursor: col-resize;
|
||||||
|
&.left {
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.right {
|
||||||
|
left: -8px;
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 6px;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: ${({isActive}) => (isActive ? 'var(--sidebar-text-active-border)' : 'transparent')};
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
&::after {
|
||||||
|
background-color: var(--sidebar-text-active-border);
|
||||||
|
transition: background-color 400ms step-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.snapped {
|
||||||
|
&::after {
|
||||||
|
animation: emphasis-sidebar-resize-line 800ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ResizableDividerGlobalStyle = createGlobalStyle<{active: boolean; varName: CssVarKeyForResizable; width: number | null}>`
|
||||||
|
${({active}) => active && css`
|
||||||
|
body {
|
||||||
|
cursor: col-resize;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
${({varName, width}) => width && css`
|
||||||
|
:root {
|
||||||
|
--${varName}: ${width}px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function ResizableDivider({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
dir,
|
||||||
|
containerRef,
|
||||||
|
defaultWidth,
|
||||||
|
onResize,
|
||||||
|
onResizeStart,
|
||||||
|
onResizeEnd,
|
||||||
|
onDividerDoubleClick,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const resizeLineRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const startWidth = useRef(0);
|
||||||
|
const lastWidth = useRef<number | null>(null);
|
||||||
|
|
||||||
|
const previousClientX = useRef(0);
|
||||||
|
|
||||||
|
const cssVarKey = `--${props.globalCssVar}`;
|
||||||
|
|
||||||
|
const currentUserID = useSelector(getCurrentUserId);
|
||||||
|
|
||||||
|
const [isActive, setIsActive] = useState(false);
|
||||||
|
const [width, setWidth] = useGlobalState<number | null>(null, `resizable_${name}:`, currentUserID);
|
||||||
|
|
||||||
|
const defaultOnResizeChange = (width: number, cssVarProp: string, cssVarValue: string) => {
|
||||||
|
containerRef.current?.style.setProperty(cssVarProp, cssVarValue);
|
||||||
|
lastWidth.current = width;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOnResize = (width: number, cssVarProp: string, cssVarValue: string) => {
|
||||||
|
if (onResize) {
|
||||||
|
onResize(width, cssVarProp, cssVarValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultOnResizeChange(width, cssVarProp, cssVarValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
startWidth.current = 0;
|
||||||
|
previousClientX.current = 0;
|
||||||
|
lastWidth.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseDown: MouseEventHandler<HTMLDivElement> = (e) => {
|
||||||
|
if (disabled || !containerRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
previousClientX.current = e.clientX;
|
||||||
|
startWidth.current = containerRef.current.getBoundingClientRect().width;
|
||||||
|
|
||||||
|
setIsActive(true);
|
||||||
|
|
||||||
|
if (onResizeStart) {
|
||||||
|
onResizeStart(startWidth.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnResize(startWidth.current, cssVarKey, `${startWidth.current}px`);
|
||||||
|
|
||||||
|
document.body.classList.add('layout-changing');
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive || disabled) {
|
||||||
|
reset();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const onMouseMove = (e: MouseEvent) => {
|
||||||
|
const resizeLine = resizeLineRef.current;
|
||||||
|
|
||||||
|
if (!isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resizeLine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const previousWidth = lastWidth.current ?? 0;
|
||||||
|
let widthDiff = 0;
|
||||||
|
|
||||||
|
switch (dir) {
|
||||||
|
case ResizeDirection.LEFT:
|
||||||
|
widthDiff = e.clientX - previousClientX.current;
|
||||||
|
break;
|
||||||
|
case ResizeDirection.RIGHT:
|
||||||
|
widthDiff = previousClientX.current - e.clientX;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newWidth = previousWidth + widthDiff;
|
||||||
|
|
||||||
|
if (resizeLine.classList.contains('snapped')) {
|
||||||
|
const diff = newWidth - defaultWidth;
|
||||||
|
|
||||||
|
if (isSizeLessThanSnapSize(diff)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnResize(newWidth, cssVarKey, `${newWidth}px`);
|
||||||
|
|
||||||
|
resizeLine.classList.remove('snapped');
|
||||||
|
previousClientX.current = e.clientX;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousClientX.current = e.clientX;
|
||||||
|
|
||||||
|
const shouldSnap = shouldSnapWhenSizeGrown(newWidth, previousWidth, defaultWidth) || shouldSnapWhenSizeShrunk(newWidth, previousWidth, defaultWidth);
|
||||||
|
|
||||||
|
if (isSnapableSpeed(newWidth - previousWidth) && shouldSnap) {
|
||||||
|
switch (dir) {
|
||||||
|
case ResizeDirection.LEFT:
|
||||||
|
previousClientX.current += defaultWidth - newWidth;
|
||||||
|
break;
|
||||||
|
case ResizeDirection.RIGHT:
|
||||||
|
previousClientX.current += newWidth - defaultWidth;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnResize(defaultWidth, cssVarKey, `${defaultWidth}px`);
|
||||||
|
|
||||||
|
resizeLine.classList.add('snapped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOnResize(newWidth, cssVarKey, `${newWidth}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = () => {
|
||||||
|
const finalWidth = containerRef.current?.getBoundingClientRect().width;
|
||||||
|
if (isActive && finalWidth) {
|
||||||
|
setWidth(finalWidth);
|
||||||
|
onResizeEnd?.(finalWidth, cssVarKey, `${finalWidth}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
containerRef.current?.style.removeProperty(cssVarKey);
|
||||||
|
document.body.classList.remove('layout-changing');
|
||||||
|
reset();
|
||||||
|
setIsActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', onMouseMove);
|
||||||
|
window.addEventListener('mouseup', onMouseUp);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('mousemove', onMouseMove);
|
||||||
|
window.removeEventListener('mouseup', onMouseUp);
|
||||||
|
};
|
||||||
|
}, [isActive, disabled]);
|
||||||
|
|
||||||
|
const onDoubleClick = () => {
|
||||||
|
onDividerDoubleClick?.(width ?? 0, cssVarKey);
|
||||||
|
reset();
|
||||||
|
setWidth(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider
|
||||||
|
id={id}
|
||||||
|
className={classNames(className, {
|
||||||
|
left: dir === ResizeDirection.LEFT,
|
||||||
|
right: dir === ResizeDirection.RIGHT,
|
||||||
|
})}
|
||||||
|
ref={resizeLineRef}
|
||||||
|
isActive={isActive}
|
||||||
|
onMouseDown={onMouseDown}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
/>
|
||||||
|
<ResizableDividerGlobalStyle
|
||||||
|
varName={props.globalCssVar}
|
||||||
|
width={width}
|
||||||
|
active={isActive}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResizableDivider;
|
@ -0,0 +1,41 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {HTMLAttributes, useRef} from 'react';
|
||||||
|
|
||||||
|
import {DEFAULT_LHS_WIDTH, CssVarKeyForResizable, ResizeDirection} from '../constants';
|
||||||
|
import ResizableDivider from '../resizable_divider';
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<'div'> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizableLhs({
|
||||||
|
children,
|
||||||
|
disabled,
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
}: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={className}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ResizableDivider
|
||||||
|
name={'lhsResizeHandle'}
|
||||||
|
globalCssVar={CssVarKeyForResizable.LHS}
|
||||||
|
disabled={disabled}
|
||||||
|
defaultWidth={DEFAULT_LHS_WIDTH}
|
||||||
|
dir={ResizeDirection.LEFT}
|
||||||
|
containerRef={containerRef}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResizableLhs;
|
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import React, {HTMLAttributes, useEffect, useRef, useState} from 'react';
|
||||||
|
import {useSelector} from 'react-redux';
|
||||||
|
|
||||||
|
import {getIsRhsExpanded, getRhsSize} from 'selectors/rhs';
|
||||||
|
|
||||||
|
import {shouldRhsOverlapChannelView} from '../utils';
|
||||||
|
import {CssVarKeyForResizable, RHS_MIN_MAX_WIDTH, ResizeDirection} from '../constants';
|
||||||
|
import ResizableDivider from '../resizable_divider';
|
||||||
|
|
||||||
|
interface Props extends HTMLAttributes<'div'> {
|
||||||
|
children: React.ReactNode;
|
||||||
|
rightWidthHolderRef: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ResizableRhs({
|
||||||
|
role,
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
className,
|
||||||
|
rightWidthHolderRef,
|
||||||
|
}: Props) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const rhsSize = useSelector(getRhsSize);
|
||||||
|
const isRhsExpanded = useSelector(getIsRhsExpanded);
|
||||||
|
|
||||||
|
const [previousRhsExpanded, setPreviousRhsExpanded] = useState(false);
|
||||||
|
|
||||||
|
const defaultWidth = RHS_MIN_MAX_WIDTH[rhsSize].default;
|
||||||
|
|
||||||
|
const shouldRhsOverlap = shouldRhsOverlapChannelView(rhsSize);
|
||||||
|
|
||||||
|
const handleResize = (_: number, cssVarProp: string, cssVarValue: string) => {
|
||||||
|
const rightWidthHolderRefElement = rightWidthHolderRef.current;
|
||||||
|
|
||||||
|
if (!rightWidthHolderRefElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shouldRhsOverlap) {
|
||||||
|
rightWidthHolderRefElement.style.setProperty(cssVarProp, cssVarValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResizeEnd = (_: number, cssVarProp: string) => {
|
||||||
|
const rightWidthHolderRefElement = rightWidthHolderRef.current;
|
||||||
|
|
||||||
|
if (!rightWidthHolderRefElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
rightWidthHolderRefElement.style.removeProperty(cssVarProp);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDividerDoubleClick = (_: number, cssVarProp: string) => {
|
||||||
|
handleResizeEnd(_, cssVarProp);
|
||||||
|
|
||||||
|
document.body.classList.add('layout-changing');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.classList.remove('layout-changing');
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// If max-width is applied immediately when expanded is collapsed, the transition will not work correctly.
|
||||||
|
useEffect(() => {
|
||||||
|
const containerRefElement = containerRef.current;
|
||||||
|
|
||||||
|
if (!containerRefElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreviousRhsExpanded(isRhsExpanded);
|
||||||
|
|
||||||
|
if (previousRhsExpanded && !isRhsExpanded) {
|
||||||
|
containerRefElement.classList.add('resize-disabled');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
containerRefElement.classList.remove('resize-disabled');
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}, [isRhsExpanded]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id={id}
|
||||||
|
className={className}
|
||||||
|
role={role}
|
||||||
|
ref={containerRef}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ResizableDivider
|
||||||
|
name='rhsResizeHandle'
|
||||||
|
globalCssVar={CssVarKeyForResizable.RHS}
|
||||||
|
defaultWidth={defaultWidth}
|
||||||
|
dir={ResizeDirection.RIGHT}
|
||||||
|
disabled={isRhsExpanded}
|
||||||
|
containerRef={containerRef}
|
||||||
|
onResize={handleResize}
|
||||||
|
onResizeEnd={handleResizeEnd}
|
||||||
|
onDividerDoubleClick={handleDividerDoubleClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResizableRhs;
|
29
webapp/channels/src/components/resizable_sidebar/utils.ts
Normal file
29
webapp/channels/src/components/resizable_sidebar/utils.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {SidebarSize, SIDEBAR_SNAP_SIZE, SIDEBAR_SNAP_SPEED_LIMIT} from './constants';
|
||||||
|
|
||||||
|
export const isSizeLessThanSnapSize = (size: number) => {
|
||||||
|
return Math.abs(size) <= SIDEBAR_SNAP_SIZE;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isSnapableSpeed = (speed: number) => {
|
||||||
|
return Math.abs(speed) <= SIDEBAR_SNAP_SPEED_LIMIT;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldSnapWhenSizeGrown = (newWidth: number, prevWidth: number, defaultWidth: number) => {
|
||||||
|
const diff = defaultWidth - newWidth;
|
||||||
|
const isGrowing = newWidth > prevWidth;
|
||||||
|
|
||||||
|
return diff >= 0 && diff <= SIDEBAR_SNAP_SIZE && isGrowing;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldSnapWhenSizeShrunk = (newWidth: number, prevWidth: number, defaultWidth: number) => {
|
||||||
|
const diff = newWidth - defaultWidth;
|
||||||
|
const isShrinking = newWidth < prevWidth;
|
||||||
|
|
||||||
|
return diff >= 0 && diff <= SIDEBAR_SNAP_SIZE && isShrinking;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const shouldRhsOverlapChannelView = (size: SidebarSize) => size === SidebarSize.MEDIUM;
|
||||||
|
|
@ -90,6 +90,7 @@ exports[`components/Root Routes Should mount public product routes 1`] = `
|
|||||||
<CompassThemeProvider
|
<CompassThemeProvider
|
||||||
theme={Object {}}
|
theme={Object {}}
|
||||||
>
|
>
|
||||||
|
<WindowSizeObserver />
|
||||||
<Connect(ModalController) />
|
<Connect(ModalController) />
|
||||||
<Connect(_class) />
|
<Connect(_class) />
|
||||||
<Connect(SystemNotice) />
|
<Connect(SystemNotice) />
|
||||||
|
@ -6,6 +6,8 @@ import {RouteComponentProps} from 'react-router-dom';
|
|||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
import rudderAnalytics from 'rudder-sdk-js';
|
import rudderAnalytics from 'rudder-sdk-js';
|
||||||
|
|
||||||
|
import matchMedia from 'tests/helpers/match_media.mock';
|
||||||
|
|
||||||
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
import {Theme} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
|
||||||
import {Client4} from 'mattermost-redux/client';
|
import {Client4} from 'mattermost-redux/client';
|
||||||
@ -14,7 +16,6 @@ import {GeneralTypes} from 'mattermost-redux/action_types';
|
|||||||
import Root from 'components/root/root';
|
import Root from 'components/root/root';
|
||||||
import * as GlobalActions from 'actions/global_actions';
|
import * as GlobalActions from 'actions/global_actions';
|
||||||
import Constants, {StoragePrefixes, WindowSizes} from 'utils/constants';
|
import Constants, {StoragePrefixes, WindowSizes} from 'utils/constants';
|
||||||
import matchMedia from 'tests/helpers/match_media.mock';
|
|
||||||
import {ProductComponent} from 'types/store/plugins';
|
import {ProductComponent} from 'types/store/plugins';
|
||||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||||
|
|
||||||
|
@ -91,6 +91,7 @@ import {applyLuxonDefaults} from './effects';
|
|||||||
|
|
||||||
import RootProvider from './root_provider';
|
import RootProvider from './root_provider';
|
||||||
import RootRedirect from './root_redirect';
|
import RootRedirect from './root_redirect';
|
||||||
|
import WindowSizeObserver from 'components/window_size_observer/WindowSizeObserver';
|
||||||
import {ServiceEnvironment} from '@mattermost/types/config';
|
import {ServiceEnvironment} from '@mattermost/types/config';
|
||||||
|
|
||||||
const CreateTeam = makeAsyncComponent('CreateTeam', LazyCreateTeam);
|
const CreateTeam = makeAsyncComponent('CreateTeam', LazyCreateTeam);
|
||||||
@ -644,6 +645,7 @@ export default class Root extends React.PureComponent<Props, State> {
|
|||||||
transitionDirection={Animations.Reasons.EnterFromBefore}
|
transitionDirection={Animations.Reasons.EnterFromBefore}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
<WindowSizeObserver/>
|
||||||
<ModalController/>
|
<ModalController/>
|
||||||
<AnnouncementBarController/>
|
<AnnouncementBarController/>
|
||||||
<SystemNotice/>
|
<SystemNotice/>
|
||||||
|
@ -3,8 +3,9 @@
|
|||||||
exports[`components/sidebar should match empty div snapshot when teamId is missing 1`] = `<div />`;
|
exports[`components/sidebar should match empty div snapshot when teamId is missing 1`] = `<div />`;
|
||||||
|
|
||||||
exports[`components/sidebar should match snapshot 1`] = `
|
exports[`components/sidebar should match snapshot 1`] = `
|
||||||
<div
|
<ResizableLhs
|
||||||
className=""
|
className=""
|
||||||
|
disabled={false}
|
||||||
id="SidebarContainer"
|
id="SidebarContainer"
|
||||||
>
|
>
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
@ -42,12 +43,13 @@ exports[`components/sidebar should match snapshot 1`] = `
|
|||||||
onDragStart={[Function]}
|
onDragStart={[Function]}
|
||||||
/>
|
/>
|
||||||
<Connect(DataPrefetch) />
|
<Connect(DataPrefetch) />
|
||||||
</div>
|
</ResizableLhs>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/sidebar should match snapshot when direct channels modal is open 1`] = `
|
exports[`components/sidebar should match snapshot when direct channels modal is open 1`] = `
|
||||||
<div
|
<ResizableLhs
|
||||||
className=""
|
className=""
|
||||||
|
disabled={false}
|
||||||
id="SidebarContainer"
|
id="SidebarContainer"
|
||||||
>
|
>
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
@ -89,12 +91,13 @@ exports[`components/sidebar should match snapshot when direct channels modal is
|
|||||||
isExistingChannel={false}
|
isExistingChannel={false}
|
||||||
onModalDismissed={[Function]}
|
onModalDismissed={[Function]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</ResizableLhs>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`components/sidebar should match snapshot when more channels modal is open 1`] = `
|
exports[`components/sidebar should match snapshot when more channels modal is open 1`] = `
|
||||||
<div
|
<ResizableLhs
|
||||||
className=""
|
className=""
|
||||||
|
disabled={false}
|
||||||
id="SidebarContainer"
|
id="SidebarContainer"
|
||||||
>
|
>
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
@ -132,5 +135,5 @@ exports[`components/sidebar should match snapshot when more channels modal is op
|
|||||||
onDragStart={[Function]}
|
onDragStart={[Function]}
|
||||||
/>
|
/>
|
||||||
<Connect(DataPrefetch) />
|
<Connect(DataPrefetch) />
|
||||||
</div>
|
</ResizableLhs>
|
||||||
`;
|
`;
|
||||||
|
@ -29,6 +29,7 @@ import ChannelNavigator from './channel_navigator';
|
|||||||
import SidebarList from './sidebar_list';
|
import SidebarList from './sidebar_list';
|
||||||
import SidebarHeader from './sidebar_header';
|
import SidebarHeader from './sidebar_header';
|
||||||
import MobileSidebarHeader from './mobile_sidebar_header';
|
import MobileSidebarHeader from './mobile_sidebar_header';
|
||||||
|
import ResizableLhs from 'components/resizable_sidebar/resizable_lhs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
@ -229,12 +230,14 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
const ariaLabel = Utils.localizeMessage('accessibility.sections.lhsNavigator', 'channel navigator region');
|
const ariaLabel = Utils.localizeMessage('accessibility.sections.lhsNavigator', 'channel navigator region');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<ResizableLhs
|
||||||
id='SidebarContainer'
|
id='SidebarContainer'
|
||||||
className={classNames({
|
className={classNames({
|
||||||
'move--right': this.props.isOpen && this.props.isMobileView,
|
'move--right': this.props.isOpen && this.props.isMobileView,
|
||||||
dragging: this.state.isDragging,
|
dragging: this.state.isDragging,
|
||||||
})}
|
})}
|
||||||
|
disabled={this.props.isMobileView}
|
||||||
|
|
||||||
>
|
>
|
||||||
{this.props.isMobileView ? <MobileSidebarHeader/> : (
|
{this.props.isMobileView ? <MobileSidebarHeader/> : (
|
||||||
<SidebarHeader
|
<SidebarHeader
|
||||||
@ -270,7 +273,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
/>
|
/>
|
||||||
<DataPrefetch/>
|
<DataPrefetch/>
|
||||||
{this.renderModals()}
|
{this.renderModals()}
|
||||||
</div>
|
</ResizableLhs>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ const SidebarHeaderContainer = styled(Flex).attrs(() => ({
|
|||||||
}))<SidebarHeaderContainerProps>`
|
}))<SidebarHeaderContainerProps>`
|
||||||
height: 52px;
|
height: 52px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -54,11 +55,6 @@ const SidebarHeaderContainer = styled(Flex).attrs(() => ({
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const HEADING_WIDTH = 200;
|
|
||||||
const CHEVRON_WIDTH = 26;
|
|
||||||
const ADD_CHANNEL_DROPDOWN_WIDTH = 28;
|
|
||||||
const TITLE_WIDTH = (HEADING_WIDTH - CHEVRON_WIDTH - ADD_CHANNEL_DROPDOWN_WIDTH).toString();
|
|
||||||
|
|
||||||
const SidebarHeading = styled(Heading).attrs(() => ({
|
const SidebarHeading = styled(Heading).attrs(() => ({
|
||||||
element: 'h1',
|
element: 'h1',
|
||||||
margin: 'none',
|
margin: 'none',
|
||||||
@ -69,7 +65,6 @@ const SidebarHeading = styled(Heading).attrs(() => ({
|
|||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
max-width: ${TITLE_WIDTH}px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -26,6 +26,7 @@ import PostEditHistory from 'components/post_edit_history';
|
|||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
|
|
||||||
import RhsPlugin from 'plugins/rhs_plugin';
|
import RhsPlugin from 'plugins/rhs_plugin';
|
||||||
|
import ResizableRhs from 'components/resizable_sidebar/resizable_rhs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
@ -65,13 +66,15 @@ type State = {
|
|||||||
|
|
||||||
export default class SidebarRight extends React.PureComponent<Props, State> {
|
export default class SidebarRight extends React.PureComponent<Props, State> {
|
||||||
sidebarRight: React.RefObject<HTMLDivElement>;
|
sidebarRight: React.RefObject<HTMLDivElement>;
|
||||||
|
sidebarRightWidthHolder: React.RefObject<HTMLDivElement>;
|
||||||
previous: Partial<Props> | undefined = undefined;
|
previous: Partial<Props> | undefined = undefined;
|
||||||
focusSearchBar?: () => void;
|
focusSearchBar?: () => void;
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.sidebarRight = React.createRef();
|
this.sidebarRightWidthHolder = React.createRef<HTMLDivElement>();
|
||||||
|
this.sidebarRight = React.createRef<HTMLDivElement>();
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpened: false,
|
isOpened: false,
|
||||||
};
|
};
|
||||||
@ -262,14 +265,20 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={'sidebar--right sidebar--right--width-holder'}/>
|
|
||||||
<div
|
<div
|
||||||
|
className={'sidebar--right sidebar--right--width-holder'}
|
||||||
|
ref={this.sidebarRightWidthHolder}
|
||||||
|
/>
|
||||||
|
<ResizableRhs
|
||||||
className={containerClassName}
|
className={containerClassName}
|
||||||
id='sidebar-right'
|
id='sidebar-right'
|
||||||
role='complementary'
|
role='complementary'
|
||||||
ref={this.sidebarRight}
|
rightWidthHolderRef={this.sidebarRightWidthHolder}
|
||||||
>
|
>
|
||||||
<div className='sidebar-right-container'>
|
<div
|
||||||
|
className='sidebar-right-container'
|
||||||
|
ref={this.sidebarRight}
|
||||||
|
>
|
||||||
{isRHSLoading ? (
|
{isRHSLoading ? (
|
||||||
<div className='sidebar-right__body'>
|
<div className='sidebar-right__body'>
|
||||||
{/* Sometimes the channel/team is not loaded yet, so we need to wait for it */}
|
{/* Sometimes the channel/team is not loaded yet, so we need to wait for it */}
|
||||||
@ -286,7 +295,7 @@ export default class SidebarRight extends React.PureComponent<Props, State> {
|
|||||||
</Search>
|
</Search>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ResizableRhs>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,93 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {useCallback, useEffect} from 'react';
|
||||||
|
|
||||||
|
import {useDispatch} from 'react-redux';
|
||||||
|
|
||||||
|
import throttle from 'lodash/throttle';
|
||||||
|
|
||||||
|
import {setLhsSize} from 'actions/views/lhs';
|
||||||
|
import {setRhsSize} from 'actions/views/rhs';
|
||||||
|
|
||||||
|
import Constants from 'utils/constants';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
|
const smallSidebarMediaQuery = window.matchMedia(`(max-width: ${Constants.SMALL_SIDEBAR_BREAKPOINT}px)`);
|
||||||
|
const mediumSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.SMALL_SIDEBAR_BREAKPOINT + 1}px) and (max-width: ${Constants.MEDIUM_SIDEBAR_BREAKPOINT}px)`);
|
||||||
|
const largeSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.MEDIUM_SIDEBAR_BREAKPOINT + 1}px) and (max-width: ${Constants.LARGE_SIDEBAR_BREAKPOINT}px)`);
|
||||||
|
const xLargeSidebarMediaQuery = window.matchMedia(`(min-width: ${Constants.LARGE_SIDEBAR_BREAKPOINT + 1}px)`);
|
||||||
|
|
||||||
|
function WindowSizeObserver() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const updateSidebarSize = useCallback(() => {
|
||||||
|
switch (true) {
|
||||||
|
case xLargeSidebarMediaQuery.matches:
|
||||||
|
dispatch(setLhsSize(SidebarSize.XLARGE));
|
||||||
|
dispatch(setRhsSize(SidebarSize.XLARGE));
|
||||||
|
break;
|
||||||
|
case largeSidebarMediaQuery.matches:
|
||||||
|
dispatch(setLhsSize(SidebarSize.LARGE));
|
||||||
|
dispatch(setRhsSize(SidebarSize.LARGE));
|
||||||
|
break;
|
||||||
|
case mediumSidebarMediaQuery.matches:
|
||||||
|
dispatch(setLhsSize(SidebarSize.MEDIUM));
|
||||||
|
dispatch(setRhsSize(SidebarSize.MEDIUM));
|
||||||
|
break;
|
||||||
|
case smallSidebarMediaQuery.matches:
|
||||||
|
dispatch(setLhsSize(SidebarSize.SMALL));
|
||||||
|
dispatch(setRhsSize(SidebarSize.SMALL));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const setSidebarSizeWhenWindowResized = useCallback(throttle(() => {
|
||||||
|
dispatch(setLhsSize());
|
||||||
|
dispatch(setRhsSize());
|
||||||
|
}, 100), []);
|
||||||
|
|
||||||
|
const handleSidebarMediaQueryChangeEvent = useCallback((e: MediaQueryListEvent) => {
|
||||||
|
if (e.matches) {
|
||||||
|
updateSidebarSize();
|
||||||
|
}
|
||||||
|
}, [updateSidebarSize]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateSidebarSize();
|
||||||
|
|
||||||
|
if (smallSidebarMediaQuery.addEventListener) {
|
||||||
|
xLargeSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
largeSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
mediumSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
smallSidebarMediaQuery.addEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
} else if (smallSidebarMediaQuery.addListener) {
|
||||||
|
xLargeSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
largeSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
mediumSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
smallSidebarMediaQuery.addListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
} else {
|
||||||
|
window.addEventListener('resize', setSidebarSizeWhenWindowResized);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (smallSidebarMediaQuery.removeEventListener) {
|
||||||
|
xLargeSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
largeSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
mediumSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
smallSidebarMediaQuery.removeEventListener('change', handleSidebarMediaQueryChangeEvent);
|
||||||
|
} else if (smallSidebarMediaQuery.removeListener) {
|
||||||
|
xLargeSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
largeSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
mediumSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
smallSidebarMediaQuery.removeListener(handleSidebarMediaQueryChangeEvent);
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('resize', setSidebarSizeWhenWindowResized);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [handleSidebarMediaQueryChangeEvent, setSidebarSizeWhenWindowResized, updateSidebarSize]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WindowSizeObserver;
|
@ -11,6 +11,7 @@ describe('Reducers.LHS', () => {
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
};
|
};
|
||||||
|
|
||||||
test('initial state', () => {
|
test('initial state', () => {
|
||||||
@ -18,6 +19,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{} as GenericAction,
|
{} as GenericAction,
|
||||||
);
|
);
|
||||||
@ -30,6 +32,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.TOGGLE_LHS,
|
type: ActionTypes.TOGGLE_LHS,
|
||||||
@ -47,6 +50,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.TOGGLE_LHS,
|
type: ActionTypes.TOGGLE_LHS,
|
||||||
@ -64,6 +68,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.OPEN_LHS,
|
type: ActionTypes.OPEN_LHS,
|
||||||
@ -81,6 +86,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ActionTypes.CLOSE_LHS,
|
type: ActionTypes.CLOSE_LHS,
|
||||||
@ -104,6 +110,7 @@ describe('Reducers.LHS', () => {
|
|||||||
{
|
{
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
currentStaticPageId: '',
|
currentStaticPageId: '',
|
||||||
|
size: 'medium',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: action,
|
type: action,
|
||||||
|
@ -6,6 +6,7 @@ import {combineReducers} from 'redux';
|
|||||||
import {TeamTypes, UserTypes} from 'mattermost-redux/action_types';
|
import {TeamTypes, UserTypes} from 'mattermost-redux/action_types';
|
||||||
import type {GenericAction} from 'mattermost-redux/types/actions';
|
import type {GenericAction} from 'mattermost-redux/types/actions';
|
||||||
import {ActionTypes} from 'utils/constants';
|
import {ActionTypes} from 'utils/constants';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
function isOpen(state = false, action: GenericAction) {
|
function isOpen(state = false, action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -29,6 +30,15 @@ function isOpen(state = false, action: GenericAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function size(state = SidebarSize.MEDIUM, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SET_LHS_SIZE:
|
||||||
|
return action.size;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function currentStaticPageId(state = '', action: GenericAction) {
|
function currentStaticPageId(state = '', action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.SELECT_STATIC_PAGE:
|
case ActionTypes.SELECT_STATIC_PAGE:
|
||||||
@ -42,6 +52,6 @@ function currentStaticPageId(state = '', action: GenericAction) {
|
|||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
isOpen,
|
isOpen,
|
||||||
|
size,
|
||||||
currentStaticPageId,
|
currentStaticPageId,
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,7 @@ describe('Reducers.RHS', () => {
|
|||||||
searchTerms: '',
|
searchTerms: '',
|
||||||
searchType: '',
|
searchType: '',
|
||||||
searchResultsTerms: '',
|
searchResultsTerms: '',
|
||||||
|
size: 'medium',
|
||||||
pluggableId: '',
|
pluggableId: '',
|
||||||
isSearchingFlaggedPost: false,
|
isSearchingFlaggedPost: false,
|
||||||
isSearchingPinnedPost: false,
|
isSearchingPinnedPost: false,
|
||||||
@ -609,6 +610,7 @@ describe('Reducers.RHS', () => {
|
|||||||
searchTerms: 'user_id',
|
searchTerms: 'user_id',
|
||||||
searchType: '',
|
searchType: '',
|
||||||
searchResultsTerms: 'user id',
|
searchResultsTerms: 'user id',
|
||||||
|
size: 'medium',
|
||||||
pluggableId: 'pluggable_id',
|
pluggableId: 'pluggable_id',
|
||||||
isSearchingFlaggedPost: true,
|
isSearchingFlaggedPost: true,
|
||||||
isSearchingPinnedPost: true,
|
isSearchingPinnedPost: true,
|
||||||
|
@ -14,6 +14,7 @@ import type {GenericAction} from 'mattermost-redux/types/actions';
|
|||||||
import type {RhsState} from 'types/store/rhs';
|
import type {RhsState} from 'types/store/rhs';
|
||||||
|
|
||||||
import {ActionTypes, RHSStates} from 'utils/constants';
|
import {ActionTypes, RHSStates} from 'utils/constants';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
function selectedPostId(state = '', action: GenericAction) {
|
function selectedPostId(state = '', action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
@ -182,6 +183,15 @@ function rhsState(state: RhsState = null, action: GenericAction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function size(state: SidebarSize = SidebarSize.MEDIUM, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ActionTypes.SET_RHS_SIZE:
|
||||||
|
return action.size;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function searchTerms(state = '', action: GenericAction) {
|
function searchTerms(state = '', action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case ActionTypes.UPDATE_RHS_SEARCH_TERMS:
|
case ActionTypes.UPDATE_RHS_SEARCH_TERMS:
|
||||||
@ -381,6 +391,7 @@ export default combineReducers({
|
|||||||
searchTerms,
|
searchTerms,
|
||||||
searchType,
|
searchType,
|
||||||
searchResultsTerms,
|
searchResultsTerms,
|
||||||
|
size,
|
||||||
pluggableId,
|
pluggableId,
|
||||||
isSearchingFlaggedPost,
|
isSearchingFlaggedPost,
|
||||||
isSearchingPinnedPost,
|
isSearchingPinnedPost,
|
||||||
|
@ -24,15 +24,31 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
|||||||
}
|
}
|
||||||
|
|
||||||
#SidebarContainer {
|
#SidebarContainer {
|
||||||
|
position: relative;
|
||||||
z-index: 16;
|
z-index: 16;
|
||||||
left: 0;
|
left: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 240px;
|
width: var(--overrideLhsWidth, 240px);
|
||||||
|
min-width: 240px;
|
||||||
|
max-width: 240px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
border-right: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
|
border-right: 1px solid rgba(var(--center-channel-color-rgb), 0.12);
|
||||||
background-color: var(--sidebar-bg);
|
background-color: var(--sidebar-bg);
|
||||||
|
|
||||||
|
@media screen and (min-width: 769px) {
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 264px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1201px) {
|
||||||
|
max-width: 304px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1681px) {
|
||||||
|
max-width: 440px;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3 {
|
h3 {
|
||||||
@ -376,11 +392,13 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
|||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SidebarHeaderMenuWrapper {
|
.SidebarHeaderMenuWrapper {
|
||||||
|
overflow: hidden;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
@ -1159,7 +1177,7 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
|||||||
.SidebarChannel .SidebarLink {
|
.SidebarChannel .SidebarLink {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 240px;
|
width: 100%;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 7px 16px 7px 19px;
|
padding: 7px 16px 7px 19px;
|
||||||
@ -1361,8 +1379,6 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
|||||||
|
|
||||||
.multi-teams {
|
.multi-teams {
|
||||||
#SidebarContainer {
|
#SidebarContainer {
|
||||||
left: 65px;
|
|
||||||
|
|
||||||
+.inner-wrap #app-content {
|
+.inner-wrap #app-content {
|
||||||
margin-left: 305px;
|
margin-left: 305px;
|
||||||
}
|
}
|
||||||
@ -1396,11 +1412,15 @@ $sidebarOpacityAnimationDuration: 0.15s;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Manual override of the global menu (and items) styling for the sidebar dropdown menu
|
// Manual override of the global menu (and items) styling for the sidebar dropdown menu
|
||||||
#sidebarDropdownMenu .MenuItem {
|
#sidebarDropdownMenu {
|
||||||
>button,
|
position: fixed;
|
||||||
>div,
|
|
||||||
>a {
|
.MenuItem {
|
||||||
padding: 0 32px 0 24px;
|
>button,
|
||||||
|
>div,
|
||||||
|
>a {
|
||||||
|
padding: 0 32px 0 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,18 +5,49 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
width: 400px;
|
body:not(.layout-changing) & {
|
||||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
width: var(--overrideRhsWidth, 400px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar--right {
|
.sidebar--right {
|
||||||
width: 400px;
|
width: var(--overrideRhsWidth, 400px);
|
||||||
|
|
||||||
|
&:not(.expanded):not(.resize-disabled) {
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 400px;
|
||||||
|
|
||||||
|
&.sidebar--right--width-holder {
|
||||||
|
@media screen and (min-width: 901px) and (max-width: 1200px) {
|
||||||
|
min-width: 304px;
|
||||||
|
max-width: 304px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 901px) {
|
||||||
|
min-width: 304px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1201px) {
|
||||||
|
max-width: 464px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1681px) {
|
||||||
|
max-width: 776px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
transform: translateX(100%);
|
transform: translateX(100%);
|
||||||
|
|
||||||
&.is-open {
|
&.is-open {
|
||||||
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
body:not(.layout-changing) & {
|
||||||
|
transition: width 0.25s 0s ease-in, z-index 0.25s 0s step-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.expanded {
|
&.expanded {
|
||||||
@ -27,7 +58,7 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-open {
|
body:not(.layout-changing) & {
|
||||||
transition: width 0.25s 0s ease-in, z-index 0.15s 0.1s step-start;
|
transition: width 0.25s 0s ease-in, z-index 0.15s 0.1s step-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
@media screen and (min-width: 1680px) {
|
@media screen and (min-width: 1680px) {
|
||||||
.sidebar--right,
|
.sidebar--right,
|
||||||
.sidebar--right--width-holder {
|
.sidebar--right--width-holder {
|
||||||
width: 500px;
|
width: var(--overrideRhsWidth, 500px);
|
||||||
|
|
||||||
.sidebar--right__title__channel {
|
.sidebar--right__title__channel {
|
||||||
max-width: 30rem;
|
max-width: 30rem;
|
||||||
|
@ -373,6 +373,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes emphasis-sidebar-resize-line {
|
||||||
|
50% {
|
||||||
|
transform: scaleX(1.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@mixin simple-in-and-out($classPrefix, $transition_time: 300ms) {
|
@mixin simple-in-and-out($classPrefix, $transition_time: 300ms) {
|
||||||
@include simple-in-and-out-before($classPrefix, $transition_time);
|
@include simple-in-and-out-before($classPrefix, $transition_time);
|
||||||
@include simple-in-and-out-after($classPrefix, $transition_time);
|
@include simple-in-and-out-after($classPrefix, $transition_time);
|
||||||
|
@ -9,11 +9,16 @@ import {makeGetDraftsCount} from 'selectors/drafts';
|
|||||||
import {
|
import {
|
||||||
isCollapsedThreadsEnabled,
|
isCollapsedThreadsEnabled,
|
||||||
} from 'mattermost-redux/selectors/entities/preferences';
|
} from 'mattermost-redux/selectors/entities/preferences';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
export function getIsLhsOpen(state: GlobalState): boolean {
|
export function getIsLhsOpen(state: GlobalState): boolean {
|
||||||
return state.views.lhs.isOpen;
|
return state.views.lhs.isOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getLhsSize(state: GlobalState): SidebarSize {
|
||||||
|
return state.views.lhs.size;
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentStaticPageId(state: GlobalState): string {
|
export function getCurrentStaticPageId(state: GlobalState): string {
|
||||||
return state.views.lhs.currentStaticPageId;
|
return state.views.lhs.currentStaticPageId;
|
||||||
}
|
}
|
||||||
|
@ -13,11 +13,16 @@ import {localizeMessage} from 'utils/utils';
|
|||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
import {RhsState, FakePost, SearchType} from 'types/store/rhs';
|
import {RhsState, FakePost, SearchType} from 'types/store/rhs';
|
||||||
import {PostDraft} from 'types/store/draft';
|
import {PostDraft} from 'types/store/draft';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
export function getSelectedPostId(state: GlobalState): Post['id'] {
|
export function getSelectedPostId(state: GlobalState): Post['id'] {
|
||||||
return state.views.rhs.selectedPostId;
|
return state.views.rhs.selectedPostId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getRhsSize(state: GlobalState): SidebarSize {
|
||||||
|
return state.views.rhs.size;
|
||||||
|
}
|
||||||
|
|
||||||
export function getSelectedPostFocussedAt(state: GlobalState): number {
|
export function getSelectedPostFocussedAt(state: GlobalState): number {
|
||||||
return state.views.rhs.selectedPostFocussedAt;
|
return state.views.rhs.selectedPostFocussedAt;
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
|||||||
import {makeGetGlobalItem} from 'selectors/storage';
|
import {makeGetGlobalItem} from 'selectors/storage';
|
||||||
import {setGlobalItem} from 'actions/storage';
|
import {setGlobalItem} from 'actions/storage';
|
||||||
|
|
||||||
export const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix', [
|
const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix', [
|
||||||
getCurrentUserId,
|
getCurrentUserId,
|
||||||
getCurrentTeamId,
|
getCurrentTeamId,
|
||||||
], (
|
], (
|
||||||
@ -21,14 +21,6 @@ export const currentUserAndTeamSuffix = createSelector('currentUserAndTeamSuffix
|
|||||||
return `:${userId}:${teamId}`;
|
return `:${userId}:${teamId}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const currentUserSuffix = createSelector('currentUserSuffix', [
|
|
||||||
getCurrentUserId,
|
|
||||||
], (
|
|
||||||
userId,
|
|
||||||
) => {
|
|
||||||
return `:${userId}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param initialValue
|
* @param initialValue
|
||||||
@ -38,10 +30,12 @@ export const currentUserSuffix = createSelector('currentUserSuffix', [
|
|||||||
export function useGlobalState<TVal>(
|
export function useGlobalState<TVal>(
|
||||||
initialValue: TVal,
|
initialValue: TVal,
|
||||||
name: string,
|
name: string,
|
||||||
|
suffix?: string,
|
||||||
): [TVal, (value: TVal) => ReturnType<typeof setGlobalItem>] {
|
): [TVal, (value: TVal) => ReturnType<typeof setGlobalItem>] {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const suffix = useSelector(currentUserAndTeamSuffix);
|
const defaultSuffix = useSelector(currentUserAndTeamSuffix);
|
||||||
const storedKey = `${name}${suffix}`;
|
const suffixToUse = suffix || defaultSuffix;
|
||||||
|
const storedKey = `${name}${suffixToUse}`;
|
||||||
|
|
||||||
const value = useSelector(makeGetGlobalItem(storedKey, initialValue), shallowEqual);
|
const value = useSelector(makeGetGlobalItem(storedKey, initialValue), shallowEqual);
|
||||||
const setValue = useCallback((newValue) => dispatch(setGlobalItem(storedKey, newValue)), [storedKey]);
|
const setValue = useCallback((newValue) => dispatch(setGlobalItem(storedKey, newValue)), [storedKey]);
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
export type LhsViewState = {
|
export type LhsViewState = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
||||||
// Static pages (e.g. Threads, etc.)
|
size: SidebarSize;
|
||||||
|
|
||||||
|
// Static pages (e.g. Threads, Insights, etc.)
|
||||||
currentStaticPageId: string;
|
currentStaticPageId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import {Channel} from '@mattermost/types/channels';
|
|||||||
import {UserProfile} from '@mattermost/types/users';
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
|
|
||||||
import {RHSStates} from 'utils/constants';
|
import {RHSStates} from 'utils/constants';
|
||||||
|
import {SidebarSize} from 'components/resizable_sidebar/constants';
|
||||||
|
|
||||||
export type SearchType = '' | 'files' | 'messages';
|
export type SearchType = '' | 'files' | 'messages';
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ export type RhsViewState = {
|
|||||||
isSidebarExpanded: boolean;
|
isSidebarExpanded: boolean;
|
||||||
isMenuOpen: boolean;
|
isMenuOpen: boolean;
|
||||||
editChannelMembers: boolean;
|
editChannelMembers: boolean;
|
||||||
|
size: SidebarSize;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RhsState = typeof RHSStates[keyof typeof RHSStates] | null;
|
export type RhsState = typeof RHSStates[keyof typeof RHSStates] | null;
|
||||||
|
@ -199,6 +199,8 @@ export const ActionTypes = keyMirror({
|
|||||||
UPDATE_RHS_SEARCH_TYPE: null,
|
UPDATE_RHS_SEARCH_TYPE: null,
|
||||||
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
UPDATE_RHS_SEARCH_RESULTS_TERMS: null,
|
||||||
|
|
||||||
|
SET_RHS_SIZE: null,
|
||||||
|
|
||||||
RHS_GO_BACK: null,
|
RHS_GO_BACK: null,
|
||||||
|
|
||||||
SET_RHS_EXPANDED: null,
|
SET_RHS_EXPANDED: null,
|
||||||
@ -255,6 +257,7 @@ export const ActionTypes = keyMirror({
|
|||||||
TOGGLE_LHS: null,
|
TOGGLE_LHS: null,
|
||||||
OPEN_LHS: null,
|
OPEN_LHS: null,
|
||||||
CLOSE_LHS: null,
|
CLOSE_LHS: null,
|
||||||
|
SET_LHS_SIZE: null,
|
||||||
SELECT_STATIC_PAGE: null,
|
SELECT_STATIC_PAGE: null,
|
||||||
|
|
||||||
SET_SHOW_PREVIEW_ON_CREATE_COMMENT: null,
|
SET_SHOW_PREVIEW_ON_CREATE_COMMENT: null,
|
||||||
@ -1491,6 +1494,10 @@ export const Constants = {
|
|||||||
TABLET_SCREEN_WIDTH: 1020,
|
TABLET_SCREEN_WIDTH: 1020,
|
||||||
MOBILE_SCREEN_WIDTH: 768,
|
MOBILE_SCREEN_WIDTH: 768,
|
||||||
|
|
||||||
|
SMALL_SIDEBAR_BREAKPOINT: 900,
|
||||||
|
MEDIUM_SIDEBAR_BREAKPOINT: 1200,
|
||||||
|
LARGE_SIDEBAR_BREAKPOINT: 1680,
|
||||||
|
|
||||||
POST_MODAL_PADDING: 170,
|
POST_MODAL_PADDING: 170,
|
||||||
SCROLL_DELAY: 2000,
|
SCROLL_DELAY: 2000,
|
||||||
SCROLL_PAGE_FRACTION: 3,
|
SCROLL_PAGE_FRACTION: 3,
|
||||||
|
Loading…
Reference in New Issue
Block a user