Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
73
webapp/channels/src/components/status_dropdown/index.ts
Обычный файл
73
webapp/channels/src/components/status_dropdown/index.ts
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {connect} from 'react-redux';
|
||||
import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {savePreferences} from 'mattermost-redux/actions/preferences';
|
||||
|
||||
import {setStatus, unsetCustomStatus} from 'mattermost-redux/actions/users';
|
||||
import {Client4} from 'mattermost-redux/client';
|
||||
import {Preferences} from 'mattermost-redux/constants';
|
||||
|
||||
import {get, getBool, getInt} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import {openModal} from 'actions/views/modals';
|
||||
import {setStatusDropdown} from 'actions/views/status_dropdown';
|
||||
|
||||
import {getCurrentUserTimezone} from 'selectors/general';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, showStatusDropdownPulsatingDot, isCustomStatusExpired} from 'selectors/views/custom_status';
|
||||
import {isStatusDropdownOpen} from 'selectors/views/status_dropdown';
|
||||
import {GenericAction} from 'mattermost-redux/types/actions';
|
||||
import {GlobalState} from 'types/store';
|
||||
import {
|
||||
OnboardingTaskCategory,
|
||||
OnboardingTasksName,
|
||||
TaskNameMapToSteps,
|
||||
} from 'components/onboarding_tasks';
|
||||
|
||||
import StatusDropdown from './status_dropdown';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getCustomStatus = makeGetCustomStatus();
|
||||
|
||||
return function mapStateToProps(state: GlobalState) {
|
||||
const currentUser = getCurrentUser(state);
|
||||
|
||||
const userId = currentUser?.id;
|
||||
const customStatus = getCustomStatus(state, userId);
|
||||
const isMilitaryTime = getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, Preferences.USE_MILITARY_TIME, false);
|
||||
const step = getInt(state, OnboardingTaskCategory, OnboardingTasksName.COMPLETE_YOUR_PROFILE, 0);
|
||||
const showCompleteYourProfileTour = step === TaskNameMapToSteps[OnboardingTasksName.COMPLETE_YOUR_PROFILE].STARTED;
|
||||
return {
|
||||
userId,
|
||||
profilePicture: Client4.getProfilePictureUrl(userId, currentUser?.last_picture_update),
|
||||
autoResetPref: get(state, Preferences.CATEGORY_AUTO_RESET_MANUAL_STATUS, userId, ''),
|
||||
status: getStatusForUserId(state, userId),
|
||||
customStatus,
|
||||
currentUser,
|
||||
isCustomStatusEnabled: isCustomStatusEnabled(state),
|
||||
isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
|
||||
isMilitaryTime,
|
||||
isStatusDropdownOpen: isStatusDropdownOpen(state),
|
||||
showCustomStatusPulsatingDot: showStatusDropdownPulsatingDot(state),
|
||||
showCompleteYourProfileTour,
|
||||
timezone: getCurrentUserTimezone(state),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: Dispatch<GenericAction>) {
|
||||
return {
|
||||
actions: bindActionCreators({
|
||||
openModal,
|
||||
setStatus,
|
||||
unsetCustomStatus,
|
||||
setStatusDropdown,
|
||||
savePreferences,
|
||||
}, dispatch),
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(makeMapStateToProps, mapDispatchToProps)(StatusDropdown);
|
||||
345
webapp/channels/src/components/status_dropdown/status_dropdown.scss
Обычный файл
345
webapp/channels/src/components/status_dropdown/status_dropdown.scss
Обычный файл
@@ -0,0 +1,345 @@
|
||||
#statusDropdownMenu {
|
||||
.icon {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0 8px 0 0;
|
||||
|
||||
span {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
#status-menu-dnd {
|
||||
padding: 4px 16px;
|
||||
|
||||
.dropdown-menu {
|
||||
margin: auto;
|
||||
|
||||
#dndSubMenu-header {
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.SubMenu__icon-right {
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#status-menu-dnd_menuitem {
|
||||
.SubMenuItem.MenuItem {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.SubMenuItemContainer {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: unset;
|
||||
}
|
||||
}
|
||||
|
||||
#dndTime-tomorrow .dndTime-tomorrow_timestamp {
|
||||
&::before {
|
||||
content: "\00a0";
|
||||
}
|
||||
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
button.custom_status__row {
|
||||
position: relative;
|
||||
height: auto;
|
||||
font-size: inherit;
|
||||
|
||||
&.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.custom-status-emoji {
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.custom_status {
|
||||
&__container {
|
||||
display: flex;
|
||||
padding: 7px 0;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
overflow: hidden;
|
||||
max-width: 73%;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__expiry {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
opacity: 0.7;
|
||||
|
||||
&.padded {
|
||||
display: block;
|
||||
padding-left: 28px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#custom_status__clear {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 10px;
|
||||
width: auto;
|
||||
padding: 0;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.52);
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.custom-status {
|
||||
&-emoji {
|
||||
fill: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
}
|
||||
|
||||
&-suggestion-emoji > span:first-child {
|
||||
background-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-dropdown-menu-global-header {
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
|
||||
.icon > i:not(.status-icon) {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
font-family: "compass-icons";
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--sidebar-text-08);
|
||||
cursor: pointer;
|
||||
|
||||
.status {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--global-header-text-rgb), 0.08),
|
||||
rgba(var(--global-header-text-rgb), 0.08)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: rgba(var(--global-header-text-rgb), 0.16);
|
||||
|
||||
.status {
|
||||
background-image:
|
||||
linear-gradient(
|
||||
0deg,
|
||||
rgba(var(--global-header-text-rgb), 0.16),
|
||||
rgba(var(--global-header-text-rgb), 0.16)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.Menu__content {
|
||||
right: -1px;
|
||||
left: inherit;
|
||||
width: 264px;
|
||||
}
|
||||
|
||||
.custom_status__row > #custom_status__clear {
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.custom_status__container {
|
||||
align-items: center;
|
||||
line-height: 14px;
|
||||
}
|
||||
|
||||
.username-wrapper {
|
||||
padding: 0 12px;
|
||||
|
||||
> p {
|
||||
overflow: hidden;
|
||||
max-width: 150px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&.bold {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MenuHeader {
|
||||
height: 50px;
|
||||
padding: 0 16px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.MenuItem > button,
|
||||
#status-menu-dnd {
|
||||
padding: 1px 16px;
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.MenuItem__primary-text,
|
||||
.grid {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: auto 1fr auto auto auto;
|
||||
|
||||
.SubMenu__icon-right {
|
||||
padding-top: 0;
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
> .MenuItem__help-text {
|
||||
padding-left: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
#statusDropdownMenu,
|
||||
#statusDropdownMenu #status-menu-dnd {
|
||||
.icon {
|
||||
top: unset;
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 0 12px 0 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.SubMenu__icon-right {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dnd--icon {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.status-wrapper {
|
||||
display: flex;
|
||||
height: auto;
|
||||
align-items: center;
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: auto;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
display: inline-flex;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--global-header-background);
|
||||
color: var(--global-header-text);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
svg {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse1 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
80% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse2 {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
30% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: scale(2.5);
|
||||
}
|
||||
}
|
||||
|
||||
.status-dropdpwn-menu .status-dropdown-menu__clear-container .input-clear {
|
||||
position: absolute;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.72);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-dropdpwn-account-settings {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#accountSettings {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.account-settings-complete {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {CustomStatusDuration, UserProfile} from '@mattermost/types/users';
|
||||
|
||||
import {fakeDate} from 'tests/helpers/date';
|
||||
|
||||
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
|
||||
|
||||
import StatusDropdown from './status_dropdown';
|
||||
|
||||
describe('components/StatusDropdown', () => {
|
||||
let resetFakeDate: () => void;
|
||||
|
||||
beforeEach(() => {
|
||||
resetFakeDate = fakeDate(new Date('2021-11-02T22:48:57Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetFakeDate();
|
||||
});
|
||||
|
||||
const actions = {
|
||||
openModal: jest.fn(),
|
||||
setStatus: jest.fn(),
|
||||
unsetCustomStatus: jest.fn(),
|
||||
setStatusDropdown: jest.fn(),
|
||||
savePreferences: jest.fn(),
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
actions,
|
||||
userId: '',
|
||||
currentUser: {
|
||||
id: 'user_id',
|
||||
first_name: 'Nev',
|
||||
last_name: 'Aa',
|
||||
} as UserProfile,
|
||||
userTimezone: {
|
||||
useAutomaticTimezone: 'true',
|
||||
automaticTimezone: 'America/New_York',
|
||||
manualTimezone: '',
|
||||
},
|
||||
status: 'away',
|
||||
isTimezoneEnabled: true,
|
||||
isMilitaryTime: false,
|
||||
isCustomStatusEnabled: false,
|
||||
isCustomStatusExpired: false,
|
||||
isStatusDropdownOpen: false,
|
||||
showCustomStatusPulsatingDot: false,
|
||||
showCompleteYourProfileTour: false,
|
||||
};
|
||||
|
||||
test('should match snapshot in default state', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...baseProps}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with profile picture URL', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
profilePicture: 'http://localhost:8065/api/v4/users/jsx5jmdiyjyuzp9rzwfaf5pwjo/image?_=1590519110944',
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with status dropdown open', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status enabled', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status pulsating dot enabled', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
showCustomStatusPulsatingDot: true,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status and expiry', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
};
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status expired', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
};
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
isCustomStatusExpired: true,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should show clear status button when custom status is set', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.TODAY,
|
||||
expires_at: '2021-05-03T23:59:59.000Z',
|
||||
};
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
isCustomStatusExpired: false,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.status-dropdown-menu__clear-container').exists()).toBe(true);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should not show clear status button when custom status is not set', () => {
|
||||
const customStatus = undefined;
|
||||
const props = {
|
||||
...baseProps,
|
||||
isStatusDropdownOpen: true,
|
||||
isCustomStatusEnabled: true,
|
||||
isCustomStatusExpired: false,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
const wrapper = shallowWithIntl(
|
||||
<StatusDropdown {...props}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.find('.status-dropdown-menu__clear-container').exists()).toBe(false);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
609
webapp/channels/src/components/status_dropdown/status_dropdown.tsx
Обычный файл
609
webapp/channels/src/components/status_dropdown/status_dropdown.tsx
Обычный файл
@@ -0,0 +1,609 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {ReactNode} from 'react';
|
||||
import {IntlShape, injectIntl, FormattedDate, FormattedMessage, FormattedTime} from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import StatusIcon from '@mattermost/compass-components/components/status-icon';
|
||||
import Text from '@mattermost/compass-components/components/text';
|
||||
import Icon from '@mattermost/compass-components/foundations/icon/Icon';
|
||||
import {TUserStatus} from '@mattermost/compass-components/shared';
|
||||
|
||||
import {ActionFunc} from 'mattermost-redux/types/actions';
|
||||
|
||||
import * as GlobalActions from 'actions/global_actions';
|
||||
import CustomStatusEmoji from 'components/custom_status/custom_status_emoji';
|
||||
import CustomStatusModal from 'components/custom_status/custom_status_modal';
|
||||
import CustomStatusText from 'components/custom_status/custom_status_text';
|
||||
import ExpiryTime from 'components/custom_status/expiry_time';
|
||||
import DndCustomTimePicker from 'components/dnd_custom_time_picker_modal';
|
||||
import OverlayTrigger from 'components/overlay_trigger';
|
||||
import ResetStatusModal from 'components/reset_status_modal';
|
||||
import UserSettingsModal from 'components/user_settings/modal';
|
||||
import EmojiIcon from 'components/widgets/icons/emoji_icon';
|
||||
import Menu from 'components/widgets/menu/menu';
|
||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||
import Avatar, {TAvatarSizeToken} from 'components/widgets/users/avatar/avatar';
|
||||
import {OnboardingTaskCategory, OnboardingTasksName, TaskNameMapToSteps, CompleteYourProfileTour} from 'components/onboarding_tasks';
|
||||
import Tooltip from 'components/tooltip';
|
||||
|
||||
import {ModalData} from 'types/actions';
|
||||
|
||||
import {Constants, ModalIdentifiers, UserStatuses} from 'utils/constants';
|
||||
import {t} from 'utils/i18n';
|
||||
import {getCurrentDateTimeForTimezone, getCurrentMomentForTimezone} from 'utils/timezone';
|
||||
import {localizeMessage} from 'utils/utils';
|
||||
|
||||
import {CustomStatusDuration, UserCustomStatus, UserProfile, UserStatus} from '@mattermost/types/users';
|
||||
import {PulsatingDot} from '@mattermost/components';
|
||||
import {PreferenceType} from '@mattermost/types/preferences';
|
||||
|
||||
import './status_dropdown.scss';
|
||||
|
||||
type Props = {
|
||||
intl: IntlShape;
|
||||
status?: string;
|
||||
userId: string;
|
||||
profilePicture?: string;
|
||||
autoResetPref?: string;
|
||||
actions: {
|
||||
openModal: <P>(modalData: ModalData<P>) => void;
|
||||
setStatus: (status: UserStatus) => ActionFunc;
|
||||
unsetCustomStatus: () => ActionFunc;
|
||||
savePreferences: (userId: string, preferences: PreferenceType[]) => void;
|
||||
setStatusDropdown: (open: boolean) => void;
|
||||
};
|
||||
customStatus?: UserCustomStatus;
|
||||
currentUser: UserProfile;
|
||||
isCustomStatusEnabled: boolean;
|
||||
isCustomStatusExpired: boolean;
|
||||
isMilitaryTime: boolean;
|
||||
isStatusDropdownOpen: boolean;
|
||||
showCompleteYourProfileTour: boolean;
|
||||
showCustomStatusPulsatingDot: boolean;
|
||||
timezone?: string;
|
||||
}
|
||||
|
||||
type State = {
|
||||
openUp: boolean;
|
||||
width: number;
|
||||
isStatusSet: boolean;
|
||||
};
|
||||
|
||||
export class StatusDropdown extends React.PureComponent<Props, State> {
|
||||
dndTimes = [
|
||||
{id: 'thirty_minutes', label: t('status_dropdown.dnd_sub_menu_item.thirty_minutes'), labelDefault: '30 mins'},
|
||||
{id: 'one_hour', label: t('status_dropdown.dnd_sub_menu_item.one_hour'), labelDefault: '1 hour'},
|
||||
{id: 'two_hours', label: t('status_dropdown.dnd_sub_menu_item.two_hours'), labelDefault: '2 hours'},
|
||||
{id: 'tomorrow', label: t('status_dropdown.dnd_sub_menu_item.tomorrow'), labelDefault: 'Tomorrow'},
|
||||
{id: 'custom', label: t('status_dropdown.dnd_sub_menu_item.custom'), labelDefault: 'Custom'},
|
||||
];
|
||||
static defaultProps = {
|
||||
userId: '',
|
||||
profilePicture: '',
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
openUp: false,
|
||||
width: 0,
|
||||
isStatusSet: false,
|
||||
};
|
||||
}
|
||||
|
||||
openProfileModal = (): void => {
|
||||
this.props.actions.openModal({
|
||||
modalId: ModalIdentifiers.USER_SETTINGS,
|
||||
dialogType: UserSettingsModal,
|
||||
dialogProps: {isContentProductSettings: false},
|
||||
});
|
||||
}
|
||||
|
||||
setStatus = (status: string, dndEndTime?: number): void => {
|
||||
this.props.actions.setStatus({
|
||||
user_id: this.props.userId,
|
||||
status,
|
||||
dnd_end_time: dndEndTime,
|
||||
});
|
||||
}
|
||||
|
||||
isUserOutOfOffice = (): boolean => {
|
||||
return this.props.status === UserStatuses.OUT_OF_OFFICE;
|
||||
}
|
||||
|
||||
setOnline = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
this.setStatus(UserStatuses.ONLINE);
|
||||
}
|
||||
|
||||
setOffline = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
this.setStatus(UserStatuses.OFFLINE);
|
||||
}
|
||||
|
||||
setAway = (event: Event): void => {
|
||||
event.preventDefault();
|
||||
this.setStatus(UserStatuses.AWAY);
|
||||
}
|
||||
|
||||
setDnd = (index: number): void => {
|
||||
const currentDate = getCurrentMomentForTimezone(this.props.timezone);
|
||||
let endTime = currentDate;
|
||||
switch (index) {
|
||||
case 0:
|
||||
// add 30 minutes in current time
|
||||
endTime = currentDate.add(30, 'minutes');
|
||||
break;
|
||||
case 1:
|
||||
// add 1 hour in current time
|
||||
endTime = currentDate.add(1, 'hour');
|
||||
break;
|
||||
case 2:
|
||||
// add 2 hours in current time
|
||||
endTime = currentDate.add(2, 'hours');
|
||||
break;
|
||||
case 3:
|
||||
// add one day in current date
|
||||
endTime = currentDate.add(1, 'day');
|
||||
break;
|
||||
}
|
||||
|
||||
this.setStatus(UserStatuses.DND, endTime.utc().unix());
|
||||
}
|
||||
|
||||
setCustomTimedDnd = (): void => {
|
||||
const dndCustomTimePicker = {
|
||||
modalId: ModalIdentifiers.DND_CUSTOM_TIME_PICKER,
|
||||
dialogType: DndCustomTimePicker,
|
||||
dialogProps: {
|
||||
currentDate: this.props.timezone ? getCurrentDateTimeForTimezone(this.props.timezone) : new Date(),
|
||||
},
|
||||
};
|
||||
|
||||
this.props.actions.openModal(dndCustomTimePicker);
|
||||
}
|
||||
|
||||
showStatusChangeConfirmation = (status: string): void => {
|
||||
const resetStatusModalData = {
|
||||
modalId: ModalIdentifiers.RESET_STATUS,
|
||||
dialogType: ResetStatusModal,
|
||||
dialogProps: {newStatus: status},
|
||||
};
|
||||
|
||||
this.props.actions.openModal(resetStatusModalData);
|
||||
};
|
||||
|
||||
renderProfilePicture = (size: TAvatarSizeToken): ReactNode => {
|
||||
if (!this.props.profilePicture) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Avatar
|
||||
size={size}
|
||||
url={this.props.profilePicture}
|
||||
tabIndex={undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleClearStatus = (e: React.MouseEvent<HTMLButtonElement> | React.MouseEvent<HTMLDivElement> | React.TouchEvent): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.props.actions.unsetCustomStatus();
|
||||
};
|
||||
|
||||
handleEmitUserLoggedOutEvent = (): void => {
|
||||
GlobalActions.emitUserLoggedOutEvent();
|
||||
}
|
||||
|
||||
onToggle = (open: boolean): void => {
|
||||
this.props.actions.setStatusDropdown(open);
|
||||
}
|
||||
|
||||
handleCompleteYourProfileTask = (): void => {
|
||||
const taskName = OnboardingTasksName.COMPLETE_YOUR_PROFILE;
|
||||
const steps = TaskNameMapToSteps[taskName];
|
||||
const currentUserId = this.props.currentUser.id;
|
||||
const preferences = [
|
||||
{
|
||||
user_id: currentUserId,
|
||||
category: OnboardingTaskCategory,
|
||||
name: taskName,
|
||||
value: steps.FINISHED.toString(),
|
||||
},
|
||||
];
|
||||
this.props.actions.savePreferences(currentUserId, preferences);
|
||||
}
|
||||
|
||||
handleCustomStatusEmojiClick = (event: React.MouseEvent): void => {
|
||||
event.stopPropagation();
|
||||
const customStatusInputModalData = {
|
||||
modalId: ModalIdentifiers.CUSTOM_STATUS,
|
||||
dialogType: CustomStatusModal,
|
||||
};
|
||||
this.props.actions.openModal(customStatusInputModalData);
|
||||
}
|
||||
|
||||
renderCustomStatus = (isStatusSet: boolean | undefined): ReactNode => {
|
||||
if (!this.props.isCustomStatusEnabled) {
|
||||
return null;
|
||||
}
|
||||
const {customStatus} = this.props;
|
||||
|
||||
let customStatusText;
|
||||
let customStatusHelpText;
|
||||
switch (true) {
|
||||
case isStatusSet && customStatus?.text && customStatus.text.length > 0:
|
||||
customStatusText = customStatus?.text;
|
||||
break;
|
||||
case isStatusSet && !customStatus?.text && customStatus?.duration === CustomStatusDuration.DONT_CLEAR:
|
||||
customStatusHelpText = localizeMessage('status_dropdown.set_custom_text', 'Set Custom Status Text...');
|
||||
break;
|
||||
case isStatusSet && !customStatus?.text && customStatus?.duration !== CustomStatusDuration.DONT_CLEAR:
|
||||
customStatusText = '';
|
||||
break;
|
||||
case !isStatusSet:
|
||||
customStatusHelpText = localizeMessage('status_dropdown.set_custom', 'Set a Custom Status');
|
||||
}
|
||||
|
||||
const customStatusEmoji = isStatusSet ? (
|
||||
<span className='d-flex'>
|
||||
<CustomStatusEmoji
|
||||
showTooltip={false}
|
||||
emojiStyle={{marginLeft: 0}}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<EmojiIcon className={'custom-status-emoji'}/>
|
||||
);
|
||||
|
||||
const pulsatingDot = !isStatusSet && this.props.showCustomStatusPulsatingDot && (
|
||||
<PulsatingDot/>
|
||||
);
|
||||
|
||||
const clearableTooltip = (
|
||||
<Tooltip id={'InputClearTooltip'}>
|
||||
<FormattedMessage
|
||||
id={'input.clear'}
|
||||
defaultMessage='Clear'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const clearButton = isStatusSet && !pulsatingDot && (
|
||||
<div
|
||||
className={classNames('status-dropdown-menu__clear-container', 'input-clear visible')}
|
||||
onClick={this.handleClearStatus}
|
||||
onTouchEnd={this.handleClearStatus}
|
||||
>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement={'left'}
|
||||
overlay={clearableTooltip}
|
||||
>
|
||||
<span
|
||||
className='input-clear-x'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<i className='icon icon-close-circle'/>
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
);
|
||||
|
||||
const expiryTime = isStatusSet && customStatus?.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR &&
|
||||
(
|
||||
<ExpiryTime
|
||||
time={customStatus.expires_at}
|
||||
timezone={this.props.timezone}
|
||||
className={classNames('custom_status__expiry', {
|
||||
padded: customStatus?.text.length > 0,
|
||||
})}
|
||||
withinBrackets={true}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Group>
|
||||
<Menu.ItemToggleModalRedux
|
||||
ariaLabel={customStatusText || customStatusHelpText}
|
||||
modalId={ModalIdentifiers.CUSTOM_STATUS}
|
||||
dialogType={CustomStatusModal}
|
||||
className={classNames('MenuItem__primary-text custom_status__row', {
|
||||
flex: customStatus?.text.length === 0,
|
||||
})}
|
||||
id={'status-menu-custom-status'}
|
||||
>
|
||||
<span className='custom_status__container'>
|
||||
<span className='custom_status__icon'>
|
||||
{customStatusEmoji}
|
||||
</span>
|
||||
<CustomStatusText
|
||||
text={customStatusText}
|
||||
className='custom_status__text'
|
||||
/>
|
||||
<Text
|
||||
margin='none'
|
||||
color='disabled'
|
||||
>
|
||||
{customStatusHelpText}
|
||||
</Text>
|
||||
{clearButton}
|
||||
{pulsatingDot}
|
||||
</span>
|
||||
{expiryTime}
|
||||
</Menu.ItemToggleModalRedux>
|
||||
</Menu.Group>
|
||||
);
|
||||
}
|
||||
|
||||
render = (): JSX.Element => {
|
||||
const {intl} = this.props;
|
||||
const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === '';
|
||||
const {status, customStatus, isCustomStatusExpired, currentUser} = this.props;
|
||||
const isStatusSet = customStatus && (customStatus.text.length > 0 || customStatus.emoji.length > 0) && !isCustomStatusExpired;
|
||||
|
||||
const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline;
|
||||
const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd;
|
||||
const setAway = needsConfirm ? () => this.showStatusChangeConfirmation('away') : this.setAway;
|
||||
const setOffline = needsConfirm ? () => this.showStatusChangeConfirmation('offline') : this.setOffline;
|
||||
const setCustomTimedDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setCustomTimedDnd;
|
||||
|
||||
const selectedIndicator = (
|
||||
<Icon
|
||||
glyph={'check'}
|
||||
size={16}
|
||||
color={'success'}
|
||||
/>
|
||||
);
|
||||
|
||||
const dndSubMenuItems = [
|
||||
{
|
||||
id: 'dndSubMenu-header',
|
||||
direction: 'right',
|
||||
text: localizeMessage('status_dropdown.dnd_sub_menu_header', 'Disable notifications until:'),
|
||||
isHeader: true,
|
||||
} as any,
|
||||
].concat(
|
||||
this.dndTimes.map(({id, label, labelDefault}, index) => {
|
||||
let text: React.ReactNode = localizeMessage(label, labelDefault);
|
||||
if (index === 3) {
|
||||
const tomorrow = getCurrentMomentForTimezone(this.props.timezone).add(1, 'day').toDate();
|
||||
text = (
|
||||
<>
|
||||
{text}
|
||||
<span className={`dndTime-${id}_timestamp`}>
|
||||
<FormattedDate
|
||||
value={tomorrow}
|
||||
weekday='short'
|
||||
timeZone={this.props.timezone}
|
||||
/>
|
||||
{', '}
|
||||
<FormattedTime
|
||||
value={tomorrow}
|
||||
timeStyle='short'
|
||||
hour12={!this.props.isMilitaryTime}
|
||||
timeZone={this.props.timezone}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return {
|
||||
id: `dndTime-${id}`,
|
||||
direction: 'right',
|
||||
text,
|
||||
action:
|
||||
index === 4 ? () => setCustomTimedDnd() : () => setDnd(index),
|
||||
} as any;
|
||||
}),
|
||||
);
|
||||
|
||||
const customStatusComponent = this.renderCustomStatus(isStatusSet);
|
||||
|
||||
let menuAriaLabeltext;
|
||||
switch (this.props.status) {
|
||||
case UserStatuses.AWAY:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label.away',
|
||||
defaultMessage: 'Current status: Away. Select to open profile and status menu.',
|
||||
});
|
||||
break;
|
||||
case UserStatuses.DND:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label.dnd',
|
||||
defaultMessage: 'Current status: Do not disturb. Select to open profile and status menu.',
|
||||
});
|
||||
break;
|
||||
case UserStatuses.OFFLINE:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label.offline',
|
||||
defaultMessage: 'Current status: Offline. Select to open profile and status menu.',
|
||||
});
|
||||
break;
|
||||
case UserStatuses.ONLINE:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label.online',
|
||||
defaultMessage: 'Current status: Online. Select to open profile and status menu.',
|
||||
});
|
||||
break;
|
||||
case UserStatuses.OUT_OF_OFFICE:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label.ooo',
|
||||
defaultMessage: 'Current status: Out of office. Select to open profile and status menu.',
|
||||
});
|
||||
break;
|
||||
default:
|
||||
menuAriaLabeltext = intl.formatMessage({
|
||||
id: 'status_dropdown.profile_button_label',
|
||||
defaultMessage: 'Select to open profile and status menu.',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuWrapper
|
||||
onToggle={this.onToggle}
|
||||
open={this.props.isStatusDropdownOpen}
|
||||
className={classNames('status-dropdown-menu status-dropdown-menu-global-header', {
|
||||
active: this.props.isStatusDropdownOpen || isStatusSet,
|
||||
})}
|
||||
>
|
||||
<button
|
||||
className='status-wrapper style--none'
|
||||
aria-label={menuAriaLabeltext}
|
||||
aria-expanded={this.props.isStatusDropdownOpen}
|
||||
aria-controls='statusDropdownMenu'
|
||||
>
|
||||
<CustomStatusEmoji
|
||||
showTooltip={true}
|
||||
tooltipDirection={'bottom'}
|
||||
emojiStyle={{marginRight: '6px'}}
|
||||
onClick={this.handleCustomStatusEmojiClick as () => void}
|
||||
/>
|
||||
{this.renderProfilePicture('sm')}
|
||||
<div
|
||||
className='status'
|
||||
>
|
||||
<StatusIcon
|
||||
size={'sm'}
|
||||
status={(this.props.status || 'offline') as TUserStatus}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<Menu
|
||||
ariaLabel={localizeMessage('status_dropdown.menuAriaLabel', 'Set a status')}
|
||||
id={'statusDropdownMenu'}
|
||||
listId={'status-drop-down-menu-list'}
|
||||
>
|
||||
{currentUser && (
|
||||
<Menu.Header onClick={this.openProfileModal}>
|
||||
{this.renderProfilePicture('lg')}
|
||||
<div className={'username-wrapper'}>
|
||||
<Text
|
||||
className={'bold'}
|
||||
margin={'none'}
|
||||
>{`${currentUser.first_name} ${currentUser.last_name}`}</Text>
|
||||
<Text
|
||||
margin={'none'}
|
||||
className={!currentUser.first_name && !currentUser.last_name ? 'bold' : ''}
|
||||
color={!currentUser.first_name && !currentUser.last_name ? undefined : 'disabled'}
|
||||
>
|
||||
{'@' + currentUser.username}
|
||||
</Text>
|
||||
</div>
|
||||
</Menu.Header>
|
||||
)}
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
show={this.isUserOutOfOffice()}
|
||||
onClick={() => null}
|
||||
ariaLabel={localizeMessage('status_dropdown.set_ooo', 'Out of office').toLowerCase()}
|
||||
text={localizeMessage('status_dropdown.set_ooo', 'Out of office')}
|
||||
extraText={localizeMessage('status_dropdown.set_ooo.extra', 'Automatic Replies are enabled')}
|
||||
/>
|
||||
</Menu.Group>
|
||||
{customStatusComponent}
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
onClick={setOnline}
|
||||
ariaLabel={localizeMessage('status_dropdown.set_online', 'Online').toLowerCase()}
|
||||
text={localizeMessage('status_dropdown.set_online', 'Online')}
|
||||
icon={(
|
||||
<StatusIcon
|
||||
status={'online'}
|
||||
className={'status-icon'}
|
||||
/>
|
||||
)}
|
||||
rightDecorator={status === 'online' && selectedIndicator}
|
||||
id={'status-menu-online'}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
onClick={setAway}
|
||||
ariaLabel={localizeMessage('status_dropdown.set_away', 'Away').toLowerCase()}
|
||||
text={localizeMessage('status_dropdown.set_away', 'Away')}
|
||||
icon={(
|
||||
<StatusIcon
|
||||
status={'away'}
|
||||
className={'status-icon'}
|
||||
/>
|
||||
)}
|
||||
rightDecorator={status === 'away' && selectedIndicator}
|
||||
id={'status-menu-away'}
|
||||
/>
|
||||
<Menu.ItemSubMenu
|
||||
subMenu={dndSubMenuItems}
|
||||
ariaLabel={`${localizeMessage('status_dropdown.set_dnd', 'Do not disturb').toLowerCase()}. ${localizeMessage('status_dropdown.set_dnd.extra', 'Disables desktop, email and push notifications').toLowerCase()}`}
|
||||
text={localizeMessage('status_dropdown.set_dnd', 'Do not disturb')}
|
||||
extraText={localizeMessage('status_dropdown.set_dnd.extra', 'Disables all notifications')}
|
||||
icon={(
|
||||
<StatusIcon
|
||||
status={'dnd'}
|
||||
className={'status-icon'}
|
||||
/>
|
||||
)}
|
||||
rightDecorator={status === 'dnd' && selectedIndicator}
|
||||
direction={'left'}
|
||||
openUp={this.state.openUp}
|
||||
id={'status-menu-dnd'}
|
||||
/>
|
||||
<Menu.ItemAction
|
||||
onClick={setOffline}
|
||||
ariaLabel={localizeMessage('status_dropdown.set_offline', 'Offline').toLowerCase()}
|
||||
text={localizeMessage('status_dropdown.set_offline', 'Offline')}
|
||||
icon={(
|
||||
<StatusIcon
|
||||
status={'offline'}
|
||||
className={'status-icon'}
|
||||
/>
|
||||
)}
|
||||
rightDecorator={status === 'offline' && selectedIndicator}
|
||||
id={'status-menu-offline'}
|
||||
/>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemToggleModalRedux
|
||||
id='accountSettings'
|
||||
ariaLabel='Profile'
|
||||
modalId={ModalIdentifiers.USER_SETTINGS}
|
||||
dialogType={UserSettingsModal}
|
||||
dialogProps={{isContentProductSettings: false}}
|
||||
text={localizeMessage('navbar_dropdown.profileSettings', 'Profile')}
|
||||
icon={(
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'account-outline'}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{this.props.showCompleteYourProfileTour && (
|
||||
<div
|
||||
onClick={this.handleCompleteYourProfileTask}
|
||||
className={'account-settings-complete'}
|
||||
>
|
||||
<CompleteYourProfileTour/>
|
||||
</div>
|
||||
)}
|
||||
</Menu.ItemToggleModalRedux>
|
||||
</Menu.Group>
|
||||
<Menu.Group>
|
||||
<Menu.ItemAction
|
||||
id='logout'
|
||||
onClick={this.handleEmitUserLoggedOutEvent}
|
||||
text={localizeMessage('navbar_dropdown.logout', 'Log Out')}
|
||||
icon={(
|
||||
<Icon
|
||||
size={16}
|
||||
glyph={'exit-to-app'}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Menu.Group>
|
||||
</Menu>
|
||||
</MenuWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default injectIntl(StatusDropdown);
|
||||
Ссылка в новой задаче
Block a user