+
{recentCustomStatuses.length > 0 && recentStatuses}
@@ -199,8 +290,6 @@ const CustomStatusModal: React.FC
= (props: Props) => {
);
- const showSuggestions = !isStatusSet || (currentCustomStatus?.emoji === emoji && text && currentCustomStatus?.text === text);
-
return (
= (props: Props) => {
= (props: Props) => {
/>
{showSuggestions && suggestion}
+ {showDateAndTimeField && (
+
+ )}
+ {isStatusSet && (
+
+ )}
-
+
);
};
diff --git a/components/custom_status/custom_status_suggestion.test.tsx b/components/custom_status/custom_status_suggestion.test.tsx
index d11078cb14c2..f770842bccbf 100644
--- a/components/custom_status/custom_status_suggestion.test.tsx
+++ b/components/custom_status/custom_status_suggestion.test.tsx
@@ -1,15 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {shallow} from 'enzyme';
+
import React from 'react';
+import {CustomStatusDuration} from 'mattermost-redux/types/users';
+
import CustomStatusSuggestion from './custom_status_suggestion';
describe('components/custom_status/custom_status_emoji', () => {
const baseProps = {
handleSuggestionClick: jest.fn(),
- emoji: '',
- text: '',
+ status: {
+ emoji: '',
+ text: '',
+ duration: CustomStatusDuration.DONT_CLEAR,
+ },
handleClear: jest.fn(),
};
@@ -21,6 +27,21 @@ describe('components/custom_status/custom_status_emoji', () => {
expect(wrapper).toMatchSnapshot();
});
+ it('should match snapshot with duration', () => {
+ const props = {
+ ...baseProps,
+ status: {
+ ...baseProps.status,
+ duration: CustomStatusDuration.TODAY,
+ },
+ };
+ const wrapper = shallow(
+
,
+ );
+
+ expect(wrapper).toMatchSnapshot();
+ });
+
it('should call handleSuggestionClick when click occurs on div', () => {
const wrapper = shallow(
,
diff --git a/components/custom_status/custom_status_suggestion.tsx b/components/custom_status/custom_status_suggestion.tsx
index f61737629094..804cf2a7a10d 100644
--- a/components/custom_status/custom_status_suggestion.tsx
+++ b/components/custom_status/custom_status_suggestion.tsx
@@ -2,12 +2,14 @@
// See LICENSE.txt for license information.
import React, {useState} from 'react';
import {Tooltip} from 'react-bootstrap';
+import {FormattedMessage} from 'react-intl';
+import classNames from 'classnames';
import {UserCustomStatus} from 'mattermost-redux/types/users';
import OverlayTrigger from 'components/overlay_trigger';
-import Constants from 'utils/constants';
import RenderEmoji from 'components/emoji/render_emoji';
+import Constants, {durationValues} from 'utils/constants';
import CustomStatusText from './custom_status_text';
@@ -15,13 +17,13 @@ import './custom_status.scss';
type Props = {
handleSuggestionClick: (status: UserCustomStatus) => void;
- emoji: string;
- text: string;
handleClear?: (status: UserCustomStatus) => void;
+ status: UserCustomStatus;
};
const CustomStatusSuggestion: React.FC
= (props: Props) => {
- const {handleSuggestionClick, emoji, text, handleClear} = props;
+ const {handleSuggestionClick, handleClear, status} = props;
+ const {emoji, text, duration} = status;
const [show, setShow] = useState(false);
const showClearButton = () => setShow(true);
@@ -32,10 +34,7 @@ const CustomStatusSuggestion: React.FC = (props: Props) => {
event.stopPropagation();
event.preventDefault();
if (handleClear) {
- handleClear({
- emoji,
- text,
- });
+ handleClear(status);
}
};
@@ -68,7 +67,7 @@ const CustomStatusSuggestion: React.FC = (props: Props) => {
className='statusSuggestion__row cursor--pointer'
onMouseEnter={showClearButton}
onMouseLeave={hideClearButton}
- onClick={() => handleSuggestionClick({emoji, text})}
+ onClick={() => handleSuggestionClick(status)}
>
= (props: Props) => {
+ {duration && (
+
+
+
+ )}
{show && clearButton}
);
diff --git a/components/custom_status/date_time_input.test.tsx b/components/custom_status/date_time_input.test.tsx
new file mode 100644
index 000000000000..c50f5aaa17ec
--- /dev/null
+++ b/components/custom_status/date_time_input.test.tsx
@@ -0,0 +1,35 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import {shallow} from 'enzyme';
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import {Provider} from 'react-redux';
+import moment from 'moment-timezone';
+
+import {General} from 'mattermost-redux/constants';
+import * as i18Selectors from 'selectors/i18n';
+
+import DateTimeInput from './date_time_input';
+
+jest.mock('selectors/i18n');
+
+describe('components/custom_status/date_time_input', () => {
+ const mockStore = configureStore();
+ const store = mockStore({});
+
+ (i18Selectors.getCurrentLocale as jest.Mock).mockReturnValue(General.DEFAULT_LOCALE);
+ const baseProps = {
+ time: moment('2021-05-03T14:53:39.127Z'),
+ handleChange: jest.fn(),
+ timezone: 'Australia/Sydney',
+ };
+
+ it('should match snapshot', () => {
+ const wrapper = shallow(
+
+
+ ,
+ );
+ expect(wrapper.dive()).toMatchSnapshot();
+ });
+});
diff --git a/components/custom_status/date_time_input.tsx b/components/custom_status/date_time_input.tsx
new file mode 100644
index 000000000000..d772d414f195
--- /dev/null
+++ b/components/custom_status/date_time_input.tsx
@@ -0,0 +1,208 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React, {useEffect, useState, useCallback} from 'react';
+import {useSelector} from 'react-redux';
+import DayPickerInput from 'react-day-picker/DayPickerInput';
+import {DayModifiers, NavbarElementProps} from 'react-day-picker';
+import {useIntl} from 'react-intl';
+
+import moment, {Moment} from 'moment-timezone';
+
+import MenuWrapper from 'components/widgets/menu/menu_wrapper';
+import Menu from 'components/widgets/menu/menu';
+import Timestamp from 'components/timestamp';
+import {getCurrentLocale} from 'selectors/i18n';
+import {getCurrentMomentForTimezone} from 'utils/timezone';
+
+const CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES = 30;
+
+const Navbar: React.FC> = (navbarProps: Partial) => {
+ const {
+ onPreviousClick,
+ onNextClick,
+ className,
+ } = navbarProps;
+ const styleLeft: React.CSSProperties = {
+ float: 'left',
+ fontSize: 18,
+ };
+ const styleRight: React.CSSProperties = {
+ float: 'right',
+ fontSize: 18,
+ };
+
+ return (
+
+
+
+
+ );
+};
+
+export function getRoundedTime(value: Moment) {
+ const roundedTo = CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES;
+ const start = moment(value);
+ const diff = start.minute() % roundedTo;
+ if (diff === 0) {
+ return value;
+ }
+ const remainder = roundedTo - diff;
+ return start.add(remainder, 'm').seconds(0).milliseconds(0);
+}
+
+const getTimeInIntervals = (startTime: Moment): Date[] => {
+ const interval = CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES;
+ let time = moment(startTime);
+ const nextDay = moment(startTime).add(1, 'days').startOf('day');
+ const intervals: Date[] = [];
+ while (time.isBefore(nextDay)) {
+ intervals.push(time.toDate());
+ time = time.add(interval, 'minutes').seconds(0).milliseconds(0);
+ }
+
+ return intervals;
+};
+
+type Props = {
+ time: Moment;
+ handleChange: (date: Moment) => void;
+ timezone?: string;
+}
+
+const DateTimeInputContainer: React.FC = (props: Props) => {
+ const locale = useSelector(getCurrentLocale);
+ const {time, handleChange, timezone} = props;
+ const [timeOptions, setTimeOptions] = useState([]);
+ const {formatMessage} = useIntl();
+
+ const setTimeAndOptions = () => {
+ const currentTime = getCurrentMomentForTimezone(timezone);
+ let startTime = moment(time).startOf('day');
+ if (time.date() === currentTime.date()) {
+ startTime = getRoundedTime(currentTime);
+ }
+ setTimeOptions(getTimeInIntervals(startTime));
+ };
+
+ useEffect(setTimeAndOptions, [time]);
+
+ const handleDayChange = (day: Date, modifiers: DayModifiers) => {
+ if (modifiers.today) {
+ const currentTime = getCurrentMomentForTimezone(timezone);
+ const roundedTime = getRoundedTime(currentTime);
+ handleChange(roundedTime);
+ } else {
+ const dayWithTimezone = timezone ? moment.tz(day, timezone) : moment(day);
+ handleChange(dayWithTimezone.startOf('day'));
+ }
+ };
+
+ const handleTimeChange = useCallback((time: Date, e: React.MouseEvent) => {
+ e.preventDefault();
+ handleChange(moment(time));
+ }, [handleChange]);
+
+ const currentTime = getCurrentMomentForTimezone(timezone).toDate();
+ const modifiers = {
+ today: currentTime,
+ };
+
+ return (
+
+
+ {formatMessage({id: 'custom_status.expiry.date_picker.title', defaultMessage: 'Date'})}
+
+
+
+ ,
+ fromMonth: currentTime,
+ modifiers,
+ locale: locale.toLowerCase(),
+ disabledDays: {
+ before: currentTime,
+ },
+ }}
+ />
+
+
+
+
+
{formatMessage({id: 'custom_status.expiry.time_picker.title', defaultMessage: 'Time'})}
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default DateTimeInputContainer;
diff --git a/components/custom_status/expiry_menu.test.tsx b/components/custom_status/expiry_menu.test.tsx
new file mode 100644
index 000000000000..94c0c32e4406
--- /dev/null
+++ b/components/custom_status/expiry_menu.test.tsx
@@ -0,0 +1,27 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+
+import {mountWithIntl} from 'tests/helpers/intl-test-helper';
+
+import {CustomStatusDuration} from 'mattermost-redux/types/users';
+
+import ExpiryMenu from './expiry_menu';
+
+describe('components/custom_status/expiry_menu', () => {
+ const baseProps = {
+ duration: CustomStatusDuration.DONT_CLEAR,
+ handleDurationChange: jest.fn(),
+ };
+
+ it('should match snapshot', () => {
+ const wrapper = mountWithIntl();
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ it('should match snapshot with different props', () => {
+ baseProps.duration = CustomStatusDuration.DATE_AND_TIME;
+ const wrapper = mountWithIntl();
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/components/custom_status/expiry_menu.tsx b/components/custom_status/expiry_menu.tsx
new file mode 100644
index 000000000000..a63569c585e3
--- /dev/null
+++ b/components/custom_status/expiry_menu.tsx
@@ -0,0 +1,131 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React, {useState, useEffect} from 'react';
+import {FormattedMessage, useIntl} from 'react-intl';
+
+import MenuWrapper from 'components/widgets/menu/menu_wrapper';
+import Menu from 'components/widgets/menu/menu';
+import {CustomStatusDuration} from 'mattermost-redux/types/users';
+import {durationValues} from 'utils/constants';
+
+import ExpiryTime from './expiry_time';
+
+type ExpiryMenuItem = {
+ text: string;
+ value: string;
+}
+
+type Props = {
+ duration: CustomStatusDuration;
+ expiryTime?: string;
+ handleDurationChange: (expiryValue: CustomStatusDuration) => void;
+}
+
+const {
+ DONT_CLEAR,
+ THIRTY_MINUTES,
+ ONE_HOUR,
+ FOUR_HOURS,
+ TODAY,
+ THIS_WEEK,
+ DATE_AND_TIME,
+ CUSTOM_DATE_TIME,
+} = CustomStatusDuration;
+
+const ExpiryMenu: React.FC = (props: Props) => {
+ const {duration, handleDurationChange, expiryTime} = props;
+ const {formatMessage} = useIntl();
+ const [menuItems, setMenuItems] = useState([]);
+
+ const expiryMenuItems: { [key in CustomStatusDuration]?: ExpiryMenuItem } = {
+ [DONT_CLEAR]: {
+ text: formatMessage(durationValues[DONT_CLEAR]),
+ value: formatMessage(durationValues[DONT_CLEAR]),
+ },
+ [THIRTY_MINUTES]: {
+ text: formatMessage(durationValues[THIRTY_MINUTES]),
+ value: formatMessage(durationValues[THIRTY_MINUTES]),
+ },
+ [ONE_HOUR]: {
+ text: formatMessage(durationValues[ONE_HOUR]),
+ value: formatMessage(durationValues[ONE_HOUR]),
+ },
+ [FOUR_HOURS]: {
+ text: formatMessage(durationValues[FOUR_HOURS]),
+ value: formatMessage(durationValues[FOUR_HOURS]),
+ },
+ [TODAY]: {
+ text: formatMessage(durationValues[TODAY]),
+ value: formatMessage(durationValues[TODAY]),
+ },
+ [THIS_WEEK]: {
+ text: formatMessage(durationValues[THIS_WEEK]),
+ value: formatMessage(durationValues[THIS_WEEK]),
+ },
+ [CUSTOM_DATE_TIME]: {
+ text: formatMessage({id: 'custom_status.expiry_dropdown.choose_date_and_time', defaultMessage: 'Choose date and time'}),
+ value: formatMessage(durationValues[CUSTOM_DATE_TIME]),
+ },
+ };
+
+ useEffect(() => {
+ const menuItemArray = Object.keys(expiryMenuItems).map((item, index) => (
+ ) => {
+ event.preventDefault();
+ handleDurationChange(item as CustomStatusDuration);
+ }}
+ ariaLabel={expiryMenuItems[item as CustomStatusDuration]?.text.toLowerCase()}
+ text={expiryMenuItems[item as CustomStatusDuration]?.text}
+ id={`expiry_menu_item_${index}`}
+ />
+ ));
+
+ setMenuItems(menuItemArray);
+ }, []);
+
+ return (
+
+
+
+
+ {': '}
+ {expiryTime && duration !== DONT_CLEAR ? (
+
+ ) : (
+
+ {expiryMenuItems[duration === DATE_AND_TIME ? CUSTOM_DATE_TIME : duration]?.value}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ExpiryMenu;
diff --git a/components/custom_status/expiry_time.tsx b/components/custom_status/expiry_time.tsx
new file mode 100644
index 000000000000..016733af7187
--- /dev/null
+++ b/components/custom_status/expiry_time.tsx
@@ -0,0 +1,81 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+import React from 'react';
+
+import moment from 'moment-timezone';
+
+import {FormattedMessage} from 'react-intl';
+
+import Timestamp, {RelativeRanges} from 'components/timestamp';
+import {Props as TimestampProps} from 'components/timestamp/timestamp';
+
+import {getCurrentMomentForTimezone} from 'utils/timezone';
+
+const CUSTOM_STATUS_EXPIRY_RANGES = [
+ RelativeRanges.TODAY_TITLE_CASE,
+ RelativeRanges.TOMORROW_TITLE_CASE,
+];
+
+interface Props {
+ time: string;
+ timezone?: string;
+ className?: string;
+ showPrefix?: boolean;
+ withinBrackets?: boolean;
+}
+
+const ExpiryTime = ({time, timezone, className, showPrefix, withinBrackets}: Props) => {
+ const currentMomentTime = getCurrentMomentForTimezone(timezone);
+ const timestampProps: Partial = {
+ value: time,
+ ranges: CUSTOM_STATUS_EXPIRY_RANGES,
+ };
+
+ if (moment(time).isSame(currentMomentTime.clone().endOf('day')) || moment(time).isAfter(currentMomentTime.clone().add(1, 'day').endOf('day'))) {
+ timestampProps.useTime = false;
+ }
+
+ if (moment(time).isBefore(currentMomentTime.clone().endOf('day'))) {
+ timestampProps.useDate = false;
+ delete timestampProps.ranges;
+ }
+
+ if (moment(time).isAfter(currentMomentTime.clone().add(1, 'day').endOf('day')) && moment(time).isBefore(currentMomentTime.clone().add(6, 'days'))) {
+ timestampProps.useDate = {weekday: 'long'};
+ }
+
+ if (moment(time).isAfter(currentMomentTime.clone().add(6, 'days'))) {
+ timestampProps.month = 'short';
+ }
+
+ if (moment(time).isAfter(currentMomentTime.clone().endOf('year'))) {
+ timestampProps.year = 'numeric';
+ }
+
+ const prefix = showPrefix && (
+ <>
+ {' '}
+ >
+ );
+
+ return (
+
+ {withinBrackets && '('}
+ {prefix}
+
+ {withinBrackets && ')'}
+
+ );
+};
+
+ExpiryTime.defaultProps = {
+ showPrefix: true,
+ withinBrackets: false,
+};
+
+export default React.memo(ExpiryTime);
diff --git a/components/post_view/post_header/post_header_custom_status.tsx b/components/post_view/post_header/post_header_custom_status.tsx
index ae7b7af6f1a2..41b448fb278a 100644
--- a/components/post_view/post_header/post_header_custom_status.tsx
+++ b/components/post_view/post_header/post_header_custom_status.tsx
@@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React from 'react';
+import React, {useMemo} from 'react';
import {useDispatch, useSelector} from 'react-redux';
@@ -19,8 +19,8 @@ interface ComponentProps {
}
const PostHeaderCustomStatus = (props: ComponentProps) => {
+ const getCustomStatus = useMemo(makeGetCustomStatus, []);
const {userId, isSystemMessage, isBot} = props;
- const getCustomStatus = makeGetCustomStatus();
const dispatch = useDispatch();
const userCustomStatus = useSelector((state: GlobalState) => getCustomStatus(state, userId));
const showUpdateStatusButton = useSelector(showPostHeaderUpdateStatusButton);
diff --git a/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap b/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
index b75f28a6392a..bac3d3ad73f3 100644
--- a/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
+++ b/components/profile_popover/__snapshots__/profile_popover.test.tsx.snap
@@ -5,6 +5,7 @@ exports[`components/ProfilePopover should hide add-to-channel option if not on t
customStatus={null}
id="user-profile-popover"
isCustomStatusEnabled={true}
+ isCustomStatusExpired={false}
isInCurrentTeam={false}
placement="right"
popoverSize="sm"
@@ -93,6 +94,7 @@ exports[`components/ProfilePopover should match snapshot 1`] = `
customStatus={null}
id="user-profile-popover"
isCustomStatusEnabled={true}
+ isCustomStatusExpired={false}
isInCurrentTeam={true}
placement="right"
popoverSize="sm"
@@ -227,6 +229,7 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
customStatus={null}
id="user-profile-popover"
isCustomStatusEnabled={true}
+ isCustomStatusExpired={false}
isInCurrentTeam={true}
placement="right"
popoverSize="sm"
@@ -401,3 +404,447 @@ exports[`components/ProfilePopover should match snapshot for shared user 1`] = `
/>
`;
+
+exports[`components/ProfilePopover should match snapshot with custom status 1`] = `
+
+
+ @some_username
+
+
+ }
+>
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`components/ProfilePopover should match snapshot with custom status expired 1`] = `
+
+
+ @some_username
+
+
+ }
+>
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`components/ProfilePopover should match snapshot with custom status not set but can set 1`] = `
+
+
+ @some_username
+
+
+ }
+>
+
+
+
+
+
+
+
+
+
+`;
diff --git a/components/profile_popover/index.ts b/components/profile_popover/index.ts
index 229a7ae24dea..49b437a34137 100644
--- a/components/profile_popover/index.ts
+++ b/components/profile_popover/index.ts
@@ -20,10 +20,10 @@ import {openDirectChannelToUserId} from 'actions/channel_actions.jsx';
import {getMembershipForEntities} from 'actions/views/profile_popover';
import {closeModal, openModal} from 'actions/views/modals';
-import {areTimezonesEnabledAndSupported} from 'selectors/general';
+import {areTimezonesEnabledAndSupported, getCurrentUserTimezone} from 'selectors/general';
import {getRhsState, getSelectedPost} from 'selectors/rhs';
-import {makeGetCustomStatus, isCustomStatusEnabled} from 'selectors/views/custom_status';
+import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired} from 'selectors/views/custom_status';
import {ActionFunc, GenericAction} from 'mattermost-redux/types/actions';
import {GlobalState} from '../../types/store';
@@ -41,40 +41,43 @@ function getDefaultChannelId(state: GlobalState) {
return selectedPost.exists ? selectedPost.channel_id : getCurrentChannelId(state);
}
-function mapStateToProps(state: GlobalState, {userId, channelId = getDefaultChannelId(state)}: OwnProps) {
- const team = getCurrentTeam(state);
- const teamMember = getTeamMember(state, team.id, userId);
+function makeMapStateToProps() {
const getCustomStatus = makeGetCustomStatus();
- let isTeamAdmin = false;
- if (teamMember && teamMember.scheme_admin) {
- isTeamAdmin = true;
- }
-
- const channelMember = getChannelMembersInChannels(state)?.[channelId]?.[userId];
-
- let isChannelAdmin = false;
- if (getRhsState(state) !== 'search' && channelMember != null && channelMember.scheme_admin) {
- isChannelAdmin = true;
- }
-
- return {
- currentTeamId: team.id,
- currentUserId: getCurrentUserId(state),
- enableTimezone: areTimezonesEnabledAndSupported(state),
- isTeamAdmin,
- isChannelAdmin,
- isInCurrentTeam: Boolean(teamMember) && teamMember?.delete_at === 0,
- canManageAnyChannelMembersInCurrentTeam: canManageAnyChannelMembersInCurrentTeam(state),
- status: getStatusForUserId(state, userId),
- teamUrl: getCurrentRelativeTeamUrl(state),
- user: getUser(state, userId),
- modals: state.views.modals,
- customStatus: getCustomStatus(state, userId),
- isCustomStatusEnabled: isCustomStatusEnabled(state),
- channelId,
+ return (state: GlobalState, {userId, channelId = getDefaultChannelId(state)}: OwnProps) => {
+ const team = getCurrentTeam(state);
+ const teamMember = getTeamMember(state, team.id, userId);
+
+ const isTeamAdmin = Boolean(teamMember && teamMember.scheme_admin);
+ const channelMember = getChannelMembersInChannels(state)?.[channelId]?.[userId];
+
+ let isChannelAdmin = false;
+ if (getRhsState(state) !== 'search' && channelMember != null && channelMember.scheme_admin) {
+ isChannelAdmin = true;
+ }
+
+ const customStatus = getCustomStatus(state, userId);
+ return {
+ currentTeamId: team.id,
+ currentUserId: getCurrentUserId(state),
+ enableTimezone: areTimezonesEnabledAndSupported(state),
+ isTeamAdmin,
+ isChannelAdmin,
+ isInCurrentTeam: Boolean(teamMember) && teamMember?.delete_at === 0,
+ canManageAnyChannelMembersInCurrentTeam: canManageAnyChannelMembersInCurrentTeam(state),
+ status: getStatusForUserId(state, userId),
+ teamUrl: getCurrentRelativeTeamUrl(state),
+ user: getUser(state, userId),
+ modals: state.views.modals,
+ customStatus,
+ isCustomStatusEnabled: isCustomStatusEnabled(state),
+ isCustomStatusExpired: isCustomStatusExpired(state, customStatus),
+ channelId,
+ currentUserTimezone: getCurrentUserTimezone(state),
+ };
};
}
+
type Actions = {
openModal: (modalData: {modalId: string; dialogType: any; dialogProps?: any}) => Promise<{
data: boolean;
@@ -97,4 +100,4 @@ function mapDispatchToProps(dispatch: Dispatch) {
};
}
-export default connect(mapStateToProps, mapDispatchToProps)(ProfilePopover);
+export default connect(makeMapStateToProps, mapDispatchToProps)(ProfilePopover);
diff --git a/components/profile_popover/profile_popover.test.tsx b/components/profile_popover/profile_popover.test.tsx
index b1c0b42cd8ab..1d6b7d21b34e 100644
--- a/components/profile_popover/profile_popover.test.tsx
+++ b/components/profile_popover/profile_popover.test.tsx
@@ -5,6 +5,7 @@ import React from 'react';
import {shallowWithIntl} from 'tests/helpers/intl-test-helper';
import ProfilePopover from 'components/profile_popover/profile_popover';
+import {CustomStatusDuration} from 'mattermost-redux/types/users';
import Pluggable from '../../plugins/pluggable';
describe('components/ProfilePopover', () => {
@@ -25,6 +26,7 @@ describe('components/ProfilePopover', () => {
teamUrl: '',
canManageAnyChannelMembersInCurrentTeam: true,
isCustomStatusEnabled: true,
+ isCustomStatusExpired: false,
actions: {
getMembershipForEntities: jest.fn(),
openDirectChannelToUserId: jest.fn(),
@@ -108,4 +110,56 @@ describe('components/ProfilePopover', () => {
expect(wrapper.find(Pluggable).first().props()).toEqual({...pluggableProps, pluggableName: 'PopoverUserAttributes'});
expect(wrapper.find(Pluggable).last().props()).toEqual({...pluggableProps, pluggableName: 'PopoverUserActions'});
});
+
+ test('should match snapshot with custom status', () => {
+ const customStatus = {
+ emoji: 'calendar',
+ text: 'In a meeting',
+ duration: CustomStatusDuration.TODAY,
+ expires_at: '2021-05-03T23:59:59.000Z',
+ };
+ const props = {
+ ...baseProps,
+ customStatus,
+ };
+
+ const wrapper = shallowWithIntl(
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
+
+ test('should match snapshot with custom status not set but can set', () => {
+ const props = {
+ ...baseProps,
+ user: {
+ ...baseProps.user,
+ id: '',
+ },
+ };
+
+ const wrapper = shallowWithIntl(
+ ,
+ );
+ 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,
+ isCustomStatusExpired: true,
+ customStatus,
+ };
+
+ const wrapper = shallowWithIntl(
+ ,
+ );
+ expect(wrapper).toMatchSnapshot();
+ });
});
diff --git a/components/profile_popover/profile_popover.tsx b/components/profile_popover/profile_popover.tsx
index 5bea5e764868..ad079d73f2ef 100644
--- a/components/profile_popover/profile_popover.tsx
+++ b/components/profile_popover/profile_popover.tsx
@@ -24,7 +24,8 @@ import SharedUserIndicator from 'components/shared_user_indicator';
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 {UserCustomStatus, UserProfile, UserTimezone} from 'mattermost-redux/types/users';
+import ExpiryTime from 'components/custom_status/expiry_time';
+import {UserCustomStatus, UserProfile, UserTimezone, CustomStatusDuration} from 'mattermost-redux/types/users';
import {Dictionary} from 'mattermost-redux/types/utilities';
import {ServerError} from 'mattermost-redux/types/errors';
@@ -85,6 +86,8 @@ interface ProfilePopoverProps extends Omit,
currentUserId: string;
customStatus?: UserCustomStatus | null;
isCustomStatusEnabled: boolean;
+ isCustomStatusExpired: boolean;
+ currentUserTimezone?: string;
/**
* @internal
@@ -261,18 +264,19 @@ ProfilePopoverState
user,
currentUserId,
hideStatus,
+ isCustomStatusExpired,
} = this.props;
- const customStatusSet = customStatus?.text || customStatus?.emoji;
+ const customStatusSet = (customStatus?.text || customStatus?.emoji) && !isCustomStatusExpired;
const canSetCustomStatus = user?.id === currentUserId;
const shouldShowCustomStatus =
isCustomStatusEnabled &&
!hideStatus &&
- customStatus &&
(customStatusSet || canSetCustomStatus);
if (!shouldShowCustomStatus) {
return null;
}
let customStatusContent;
+ let expiryContent;
if (customStatusSet) {
const customStatusEmoji = (
@@ -296,6 +300,15 @@ ProfilePopoverState
/>
);
+
+ expiryContent = customStatusSet && customStatus?.expires_at && customStatus.duration !== CustomStatusDuration.DONT_CLEAR && (
+