forked from Ivasoft/mattermost-mobile
MM-36917 Custom status expiry (#5434)
* Started with the custom status feature Added custom status actions and API integration Added custom status selectors and types Added custom status emoji component Added EnableCustomStatuses flag in config Added Set a status option in the settings sidebar * Fixed emoji and added clear button Fixed the default emoji in the set a status in the settings sidebar Made a custom status label component to match the styling of rest of the drawer items Fixed the bug in the selectors Added localization id in en.json * Added icon for clear button Fixed the custom status label issue Added styling and icon for the clear button Fixed order of the localization ids * Made custom status modal with suggestions Fixed the long custom status overflowing in the sidebar with ellipses Added the functionality to open custom status modal from sidebar Made the constants for custom status Made the custom status suggestion functional component Made the custom status modal functional component Made the selector for the recent custom statuses Added some localization ids * Changed the custom status modal to class component Added recent custom statuses in the modal Fixed the styling of titles Fixed lint errors except one * Fix types and use i18n translations * Update types * Refactored some code and made clear button component Refactored the code of the custom status label to a different file and used it everywhere Made a separate component for clear button and added it in recents and sidebar Fixed the styling of the custom status text according to the new figma designs * Added emojipicker in custom status modal Fixed the Done button in custom status modal Added the functionality to clear status from inside the modal * Added custom status in user profile and post header and review fixes Added custom status in user profile page Added custom status emoji in post header Added functionality to preventDoubleTap everywhere Added localization ids Review fixes * Fixed several issues Increased touch area of clear button everywhere Fixed the event propagation issue in clearing suggestions Fixed the center alignment issue of title in custom status modal Added clear button in user profile page Review fixes * Fix types for custom status modal * Fix tsc * Fixed types and separated functions for Recent and suggestions in custom status modal * Update app/components/sidebars/settings/settings_sidebar_base.js * Updated localization id in settings sidebar * Changed the method in removeRecentCustomStatus API call Changed names of some variables * Fixed the input height issue in ios and modalContext issue in ios * Fixed various issues Fixed clear button border radius issue in ios Changed the method to post in removeRecentCustomStatus API call Made the custom status modal input single line and fixed the height Changed the height of iconContainer in custom status suggestion * Fixed the event propagation issue without use of PanResponder * Unit tests Added unit tests for custom status emoji component Added unit tests for custom status text component Added unit tests for clear button component Added unit tests for custom status suggestion component Added unit tests for custom status modal component Added tests and updated snapshtots in user profile screen * Added custom status in several new locations Added custom status emoji in LHS channel list Added custom status emoji in channel header Added custom status in channel info header Modified the styles of some components * Fixed width issue in LHS channel list and channel title (#8) Added one check in channel info header * E2E tests for custom status (#6) * Completed MM-T3890, MM-T3891, MM-T3892, MM-T3893 e2e tests Added EnableCustomUserStatuses in default config of detox Added several testIDs in custom status modal and clear button components Modified settings_sidebar ui component in detox support Made custom status screen ui screen in detox support Added testIDs in custom status suggestion and clear buttons Modified custom status ui screen component in detox support Added appropriate comments in the tests Modified custom status emoji component to accept styling and added testID Modified the custom status emoji styling in post header, user profile, LHS channel list, channel info header, channel title Made user profile screen in detox support Added testID prop in custom status emoji component Added testIDs in channel info header and user profile and user profile row components * Added unit tests for new components and updated tests for custom status emoji Updated snapshots for existing components Added new tests for channel title and LHS channel item Added new tests for channel info and channel info header Updated snapshots * Fixed styles and emoji opacity in custom status input in modal * Fixed the color of text and emoji in custom status suggestions * Fixed the color of emoji and font size in the custom status input in modal * Updated snapshots * Fix the MM-T3893 E2E test case * Update detox/e2e/test/smoke_test/custom_status.e2e.js * Apply suggestions from code review Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com> * Updated snapshots and moved the custom status e2e test to different location * Fix styles * Fix ios styles * Update snapshots * Updated styles and localization ids * Changed the API call for the remove recent custom status action (#12) * Fix styles for ios * Fixed styles in Android Fixed the styling of clear button everywhere Fixed the separator not showing issue in custom status suggestion Fixed the emoji positioning issue in custom status suggestion * Fixed tests and updated snapshots * Fix stylessss for ios * Updated snapshots * Added close button and title in emojipicker screen opened from custom status modal * Fixed the delay issue in clearing and setting the custom status in settings sidebar (#13) * Fixed the delay issue in clearing the custom status in settings sidebar Modified the unsetCustomStatus action to use request statuses Modified the settings sidebar to use the request status to show or hide custom status Made proper types and reducers for the unset custom status request * Added the Retry message for setting and clearing the custom status Made set custom status request action types and reducers Modified the setCustomStatus action to dispatch request status actions Made the retryMessage component Added the logic for when to show the retry message * Modified set custom status action to dispatch Update me action with the new custom status * Modified the settings sidebar to use componentDidUpdate for the delay issue in custom status clearing and setting * Modified componentDidUpdate and made a new function for handling request status change * Modified handleRequestStatusChange function to two separate functions * Code refactoring * Updated tests * Fixed the issue for custom status not updating in settings sidebar when changed from another client * Fixed the issue for custom status not updating in settings sidebar when changed from another client * Fixed the IOS UI issues - Fixed the font-weight of the channel header custom status - Fixed the extra space added on typing in input issue * Fix custom status e2e tests * Fixed the custom status input click area issue * Fix custom status input touchable area * added expiry time to custom status * removed spaces from en.json * Updated snapshots * Updated snapshots * added some ui changes to expiry stuff in custom status * Apply suggestions from code review Co-authored-by: Michel Engelen <32863416+michelengelen@users.noreply.github.com> * Review fixes (#15) * Review fixes Sorted the import order in all files Modified removeRecentCustomStatus action not to use bindClientFunction Added types for custom status client functions Added containerSize prop in clear button component Removed the check for custom status enabled from custom status emoji and added it in post_header, channel_title, channel_item Modified getCustomStatusSelector to return undefined if custom status does not exist Modified settings_sidebar, channel_info, channel_info_header, custom_status_modal, user_profile to handle undefined custom status Removed the text wrapper from the custom status emoji in channel_info_header Added check for identifying the navigation button pressed in custom status modal * Added server version check in isCustomStatusEnabled selector * Refactored some code and added types in style in custom status modal Added type in custom status suggestion functions in custom status modal Refactored some code * Fixed failing tests and updated snapshots * Expiry Time functionality added * Updated snapshot for clear button component * ran npm run fix * Added timezone functionality to custom status expiry * Merged custom status into test-1230 * Review fixes Removed logic for tracking requests for setting and unsetting custom status Added logic for clearing status without delay in settings sidebar Added event for set custom status failure and attached listener in settings sidebar Added feature in custom status text component to accept string or FormattedText as prop and ellipsizeMode and numberOfLines as well Removed labelSibling and failureText props from drawer item Passed clearButton and retryMessage in labelComponent from settings sidebar to drawer item Replaced Text with CustomStatusText in channel info header Added localization in the custom status suggestions Added exception handling in the getRecentCustomStatuses selector * Fixed drawer item unit tests and updated snapshots * Fixed UI issues for IOS in settings sidebar and main sidebar * Updated snapshots * Refactored duration code in custom status suggestion * Review fixes Removed check for user null or undefined from setCustomStatus action Refactored some code in custom status emoji and post header Memoized handleClear in custom status suggestion Made stylesheet in settings_sidebar and added all styles there Consolidated openCustomStatusModal function inside goToCustomStatusScreen * Refactored some code Refactored some code in set custom status action Updated name of a unit test in drawer item and updated snapshot * added ui fixes to date time picker component and displayExpiryTime component * Removed server version check for testing * bug fix with timezone and statusSame * Review UI fixes (#18) Changed the padding for custom status both in settings sidebar and custom status suggestions Changed the position of clear button in the settings sidebar * UI fix in user profile screen Fixed the call to apiGetChannelByName in custom status e2e test * Added server version check for custom status feature * Updated snapshots * modified testIDs, output rendering logic * Review fixes Converted mapStateToProps to makeMapStateToProps and moved makeGetCustomStatus call inside them in several components * Removed the server version check and fixed showing custom status in user profile bug * Updated datetime picker version and fixed timezone issue Fixed the memoization issue in getCurrentUserTimezone selector Refactored the getCurrentDateTimeForTimezone to getCurrentMomentForTimezone Added the timezoneOffsetInMinutes prop to the datetime picker * Fixed ios UI issues Fixed the positioning of Clear after in custom status modal in ios Fixed the positioning of spinner date time picker in ios * expiry time now showing in custom status,clear after modals * Fixed the custom status not showing in user profile of other users if own status is not set * Changed the emoji size in the channel title * Updated snapshots * Added custom status emoji in post header Fixed the lint errors and type check errors * Updated snapshots Fixed the prop type isMilitaryTime for user_profile screen * UI fixes and code refactoring (#19) * added date time picker, fixed spacing in some components * UI fixes, added isCustomStatusExpired selector * removed extra spaces from package-lock.json * Review Fixes, upgraded datetimepicker version * Refactored some code * Refactored some code * Added server version check for the custom status feature * Merge master into custom-status-expiry * Review fixes and corrected import order * Resolved tsc errors, corrected intl usage * Code refactoring and minor bug fixes Fixed the default value of isCustomStatusExpired in mapStateToProps from false to true in all components Changed the ids of various expiry dropdown texts Fixed the order of ids in en.json * Changed date time format in front of Custom option in Clear after modal (#20) * Corrected 'check' usage in front of 'Custom' in clear-after-modal * Renamed clear_after_suggestions to clear_after_menu_item. removed unused code * Added unit tests and e2e tests * Review fixes Removed custom status clear after modal as a connected component * Updated failing snapshot * Review UI fixes Changed the size of expiry time in channel header and fixed the width of custom status text along with the position of the emoji Fixed the divider between custom status input and Clear after in the custom status modal Changed the size of expiry time in custom status suggestions Fixed the position of the tick in the clear after modal * Updated snapshots * Review fixes: Code refactoring * Updated snapshots * Code refactoring and fixed several issues Updated and added unit tests for custom_status_expiry component Refactored some code in custom_status_modal and clear_after_modal Increased the size of the back icon in the Clear after modal Added the logic for showing Clear after option only when the status is set Added 30 minute intervals in the timepicker * Custom status expiry review fixes (#24) * Added server version check for custom status expiry and review fixes Made an isCustomStatusExpirySupported selector for checking server version Modified custom status modal not to show clear after row if expiry not supported Fixed the bug of status not setting if expiry not supported Added the logic for hiding the duratiion from suggestions if expiry not supported Changed the transitioning of opening Clear After modal to open in the same stack level Added the expiry supported check in settings sidebar and channel info screen and fixed the UI issues Code refactoring * Fixed the Back icon in the Clear after modal * Fixed the Clear after flow and e2e tests Fixed the Set status modal closing issue on closing Clear after modal Added the feature to show Today in the custom status expiry component Refactored and fixed the e2e tests for custom status expiry Refactored some code in the Clear After modal * Fixed unit tests and updated snapshots * Fixed the bug showing invalid date in the Clear after field in the Set status modal * Updated package-lock.json * Code refactoring * UI fixes in the user profile page Added isExpirySupported check in the user profile page * Fixed unit test and updated snapshot * Updated snapshot for user_profile * Review fixes: Code refactoring * Fixed failing snapshots * Fixed the MM-T4092 e2e test for ios (#25) * Review fixes: Code refactoring Deleted app/screens/custom_status_clear_after/index.ts file and changed import paths * Code refactoring in e2e tests and updated eslint no shadow rule * Disallowed the selection of past time in custom status date time picker Co-authored-by: Chetanya Kandhari <chetanya.kandhari@brightscout.com> Co-authored-by: Manoj <77336594+manojosh@users.noreply.github.com> Co-authored-by: Joseph Baylon <joseph.baylon@mattermost.com> Co-authored-by: Chetanya Kandhari <availchet@gmail.com> Co-authored-by: Michel Engelen <32863416+michelengelen@users.noreply.github.com>
This commit is contained in:
@@ -47,7 +47,9 @@
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
"@typescript-eslint/no-var-requires": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`components/custom_status/custom_status_expiry should match snapshot 1`] = `
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
testID=""
|
||||
>
|
||||
<Text>
|
||||
Today
|
||||
</Text>
|
||||
</Text>
|
||||
`;
|
||||
|
||||
exports[`components/custom_status/custom_status_expiry should match snapshot with prefix and brackets 1`] = `
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
},
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
testID=""
|
||||
>
|
||||
(
|
||||
<Text>
|
||||
Until
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
Today
|
||||
</Text>
|
||||
)
|
||||
</Text>
|
||||
`;
|
||||
@@ -6,6 +6,7 @@ import React from 'react';
|
||||
import CustomStatusEmoji from '@components/custom_status/custom_status_emoji';
|
||||
import * as CustomStatusSelectors from '@selectors/custom_status';
|
||||
import {renderWithRedux} from 'test/testing_library';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
|
||||
jest.mock('@selectors/custom_status');
|
||||
|
||||
@@ -14,6 +15,7 @@ describe('components/custom_status/custom_status_emoji', () => {
|
||||
return {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
};
|
||||
(CustomStatusSelectors.makeGetCustomStatus as jest.Mock).mockReturnValue(getCustomStatus);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {useSelector} from 'react-redux';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {makeGetCustomStatus} from '@selectors/custom_status';
|
||||
import {makeGetCustomStatus, isCustomStatusExpired} from '@selectors/custom_status';
|
||||
|
||||
interface ComponentProps {
|
||||
emojiSize?: number;
|
||||
@@ -21,7 +21,9 @@ const CustomStatusEmoji = ({emojiSize, userID, style, testID}: ComponentProps) =
|
||||
const customStatus = useSelector((state: GlobalState) => {
|
||||
return getCustomStatus(state, userID);
|
||||
});
|
||||
if (!customStatus?.emoji) {
|
||||
const customStatusExpired = useSelector((state: GlobalState) => isCustomStatusExpired(state, customStatus));
|
||||
|
||||
if (!customStatus?.emoji || customStatusExpired) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
61
app/components/custom_status/custom_status_expiry.test.tsx
Normal file
61
app/components/custom_status/custom_status_expiry.test.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// 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 Preferences from '@mm-redux/constants/preferences';
|
||||
import * as PreferenceSelectors from '@mm-redux/selectors/entities/preferences';
|
||||
import {renderWithReduxIntl} from 'test/testing_library';
|
||||
import CustomStatusExpiry from './custom_status_expiry';
|
||||
|
||||
jest.mock('@mm-redux/selectors/entities/preferences');
|
||||
|
||||
describe('components/custom_status/custom_status_expiry', () => {
|
||||
const date = moment().endOf('day');
|
||||
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
time: date.toDate(),
|
||||
};
|
||||
|
||||
(PreferenceSelectors.getBool as jest.Mock).mockReturnValue(false);
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithReduxIntl(
|
||||
<CustomStatusExpiry
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
expect(wrapper.getByText('Today')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should match snapshot with prefix and brackets', () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
showPrefix: true,
|
||||
withinBrackets: true,
|
||||
};
|
||||
const wrapper = renderWithReduxIntl(
|
||||
<CustomStatusExpiry
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
expect(wrapper.getByText('Until')).toBeDefined();
|
||||
expect(wrapper.getByText('Today')).toBeDefined();
|
||||
});
|
||||
|
||||
it("should render Tomorrow if given tomorrow's date", () => {
|
||||
const props = {
|
||||
...baseProps,
|
||||
time: moment().add(1, 'day').endOf('day').toDate(),
|
||||
};
|
||||
const wrapper = renderWithReduxIntl(
|
||||
<CustomStatusExpiry
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
expect(wrapper.getByText('Tomorrow')).toBeDefined();
|
||||
});
|
||||
});
|
||||
124
app/components/custom_status/custom_status_expiry.tsx
Normal file
124
app/components/custom_status/custom_status_expiry.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {Text, TextStyle} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import FormattedDate from '@components/formatted_date';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import FormattedTime from '@components/formatted_time';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {getBool} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserTimezone} from '@mm-redux/selectors/entities/timezone';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getCurrentMomentForTimezone} from '@utils/timezone';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
time: Date;
|
||||
textStyles?: TextStyle;
|
||||
testID?: string;
|
||||
showPrefix?: boolean;
|
||||
withinBrackets?: boolean;
|
||||
showToday?: boolean;
|
||||
showTimeCompulsory?: boolean;
|
||||
}
|
||||
|
||||
const CustomStatusExpiry = ({time, theme, textStyles = {}, showPrefix, withinBrackets, testID = '', showTimeCompulsory, showToday}: Props) => {
|
||||
const timezone = useSelector(getCurrentUserTimezone);
|
||||
const styles = createStyleSheet(theme);
|
||||
const militaryTime = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time'));
|
||||
const currentMomentTime = getCurrentMomentForTimezone(timezone);
|
||||
|
||||
const expiryMomentTime = timezone ? moment(time).tz(timezone) : moment(time);
|
||||
|
||||
const plusSixDaysEndTime = currentMomentTime.clone().add(6, 'days').endOf('day');
|
||||
const tomorrowEndTime = currentMomentTime.clone().add(1, 'day').endOf('day');
|
||||
const todayEndTime = currentMomentTime.clone().endOf('day');
|
||||
const isCurrentYear = currentMomentTime.get('y') === expiryMomentTime.get('y');
|
||||
|
||||
let dateComponent;
|
||||
if ((showToday && expiryMomentTime.isBefore(todayEndTime)) || expiryMomentTime.isSame(todayEndTime)) {
|
||||
dateComponent = (
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.today'
|
||||
defaultMessage='Today'
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(todayEndTime) && expiryMomentTime.isSameOrBefore(tomorrowEndTime)) {
|
||||
dateComponent = (
|
||||
<FormattedText
|
||||
id='custom_status.expiry_time.tomorrow'
|
||||
defaultMessage='Tomorrow'
|
||||
/>
|
||||
);
|
||||
} else if (expiryMomentTime.isAfter(tomorrowEndTime)) {
|
||||
let format = 'dddd';
|
||||
if (expiryMomentTime.isAfter(plusSixDaysEndTime) && isCurrentYear) {
|
||||
format = 'MMM DD';
|
||||
} else if (!isCurrentYear) {
|
||||
format = 'MMM DD, YYYY';
|
||||
}
|
||||
|
||||
dateComponent = (
|
||||
<FormattedDate
|
||||
format={format}
|
||||
timezone={timezone}
|
||||
value={expiryMomentTime.toDate()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const useTime = showTimeCompulsory || !(expiryMomentTime.isSame(todayEndTime) || expiryMomentTime.isAfter(tomorrowEndTime));
|
||||
|
||||
return (
|
||||
<Text
|
||||
testID={testID}
|
||||
style={[styles.text, textStyles]}
|
||||
>
|
||||
{withinBrackets && '('}
|
||||
{showPrefix && (
|
||||
<FormattedText
|
||||
id='custom_status.expiry.until'
|
||||
defaultMessage='Until'
|
||||
/>
|
||||
)}
|
||||
{showPrefix && ' '}
|
||||
{dateComponent}
|
||||
{useTime && dateComponent && (
|
||||
<>
|
||||
{' '}
|
||||
<FormattedText
|
||||
id='custom_status.expiry.at'
|
||||
defaultMessage='at'
|
||||
/>
|
||||
{' '}
|
||||
</>
|
||||
)}
|
||||
{useTime && (
|
||||
<FormattedTime
|
||||
isMilitaryTime={militaryTime}
|
||||
timezone={timezone || ''}
|
||||
value={expiryMomentTime.toDate()}
|
||||
/>
|
||||
)}
|
||||
{withinBrackets && ')'}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusExpiry;
|
||||
|
||||
const createStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
text: {
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -31,3 +31,19 @@ exports[`SettingsSidebar should match snapshot with custom status enabled 1`] =
|
||||
useNativeAnimations={true}
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`SettingsSidebar should match snapshot with custom status expiry 1`] = `
|
||||
<DrawerLayoutAdapter
|
||||
drawerPosition="right"
|
||||
drawerWidth={710}
|
||||
forwardRef={
|
||||
Object {
|
||||
"current": null,
|
||||
}
|
||||
}
|
||||
onDrawerClose={[Function]}
|
||||
onDrawerOpen={[Function]}
|
||||
renderNavigationView={[Function]}
|
||||
useNativeAnimations={true}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {unsetCustomStatus} from '@actions/views/custom_status';
|
||||
import {setStatus} from '@mm-redux/actions/users';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUser, getStatusForUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {isCustomStatusEnabled, makeGetCustomStatus} from '@selectors/custom_status';
|
||||
import {isCustomStatusEnabled, isCustomStatusExpired, isCustomStatusExpirySupported, makeGetCustomStatus} from '@selectors/custom_status';
|
||||
|
||||
import {logout} from 'app/actions/views/user';
|
||||
|
||||
@@ -29,6 +29,8 @@ function makeMapStateToProps() {
|
||||
theme: getTheme(state),
|
||||
isCustomStatusEnabled: customStatusEnabled,
|
||||
customStatus,
|
||||
isCustomStatusExpired: customStatusEnabled ? isCustomStatusExpired(state, customStatus) : true,
|
||||
isCustomStatusExpirySupported: customStatusEnabled ? isCustomStatusExpirySupported(state) : false,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,11 +7,13 @@ import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
|
||||
import SettingsSidebar from './settings_sidebar.ios';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
|
||||
describe('SettingsSidebar', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
@@ -26,6 +28,7 @@ describe('SettingsSidebar', () => {
|
||||
status: 'offline',
|
||||
theme: Preferences.THEMES.default,
|
||||
isCustomStatusEnabled: false,
|
||||
isCustomStatusExpired: false,
|
||||
customStatus,
|
||||
};
|
||||
|
||||
@@ -47,4 +50,20 @@ describe('SettingsSidebar', () => {
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with custom status expiry', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<SettingsSidebar
|
||||
{...baseProps}
|
||||
isCustomStatusEnabled={true}
|
||||
customStatus={{
|
||||
...customStatus,
|
||||
duration: CustomStatusDuration.DATE_AND_TIME,
|
||||
expires_at: '2200-04-13T18:09:12.451Z',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
|
||||
import {showModal, showModalOverCurrentContext, dismissModal} from '@actions/navigation';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import Emoji from '@components/emoji';
|
||||
@@ -37,6 +38,8 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
isCustomStatusEnabled: PropTypes.bool.isRequired,
|
||||
customStatus: PropTypes.object,
|
||||
isCustomStatusExpired: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpirySupported: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -77,7 +80,7 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
handleCustomStatusChange = (prevCustomStatus, customStatus) => {
|
||||
const isStatusSet = Boolean(customStatus?.emoji);
|
||||
if (isStatusSet) {
|
||||
const isStatusChanged = prevCustomStatus?.emoji !== customStatus.emoji || prevCustomStatus?.text !== customStatus.text;
|
||||
const isStatusChanged = prevCustomStatus?.emoji !== customStatus?.emoji || prevCustomStatus?.text !== customStatus?.text || prevCustomStatus?.expires_at !== customStatus?.expires_at;
|
||||
if (isStatusChanged) {
|
||||
this.setState({
|
||||
showStatus: true,
|
||||
@@ -240,7 +243,7 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
}
|
||||
|
||||
renderCustomStatus = () => {
|
||||
const {isCustomStatusEnabled, customStatus, theme} = this.props;
|
||||
const {isCustomStatusEnabled, customStatus, theme, isCustomStatusExpired, isCustomStatusExpirySupported} = this.props;
|
||||
const {showStatus, showRetryMessage} = this.state;
|
||||
|
||||
if (!isCustomStatusEnabled) {
|
||||
@@ -248,7 +251,7 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
}
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
const isStatusSet = customStatus?.emoji && showStatus;
|
||||
const isStatusSet = !isCustomStatusExpired && customStatus?.emoji && showStatus;
|
||||
|
||||
const customStatusEmoji = (
|
||||
<View
|
||||
@@ -269,24 +272,6 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
</View>
|
||||
);
|
||||
|
||||
const clearButton = isStatusSet ?
|
||||
(
|
||||
<ClearButton
|
||||
handlePress={this.clearCustomStatus}
|
||||
theme={theme}
|
||||
testID='settings.sidebar.custom_status.action.clear'
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const retryMessage = showRetryMessage ?
|
||||
(
|
||||
<FormattedText
|
||||
id='custom_status.failure_message'
|
||||
defaultMessage='Failed to update status. Try again'
|
||||
style={style.retryMessage}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const text = isStatusSet ? customStatus.text : (
|
||||
<FormattedText
|
||||
id='mobile.routes.custom_status'
|
||||
@@ -303,15 +288,35 @@ export default class SettingsSidebarBase extends PureComponent {
|
||||
text={text}
|
||||
theme={theme}
|
||||
/>
|
||||
{Boolean(isStatusSet && isCustomStatusExpirySupported && customStatus?.duration) && (
|
||||
<CustomStatusExpiry
|
||||
time={customStatus?.expires_at}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiryText}
|
||||
withinBrackets={true}
|
||||
showPrefix={true}
|
||||
testID={'custom_status.expiry'}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{retryMessage}
|
||||
{clearButton &&
|
||||
{showRetryMessage && (
|
||||
<FormattedText
|
||||
id='custom_status.failure_message'
|
||||
defaultMessage='Failed to update status. Try again'
|
||||
style={style.retryMessage}
|
||||
/>
|
||||
)}
|
||||
{isStatusSet && (
|
||||
<View
|
||||
style={style.clearButton}
|
||||
>
|
||||
{clearButton}
|
||||
<ClearButton
|
||||
handlePress={this.clearCustomStatus}
|
||||
theme={theme}
|
||||
testID='settings.sidebar.custom_status.action.clear'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -428,6 +433,11 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
customStatusIcon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
},
|
||||
customStatusExpiryText: {
|
||||
paddingTop: 3,
|
||||
fontSize: 15,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.35),
|
||||
},
|
||||
clearButton: {
|
||||
position: 'absolute',
|
||||
top: 4,
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {t} from '@utils/i18n';
|
||||
|
||||
const {
|
||||
DONT_CLEAR,
|
||||
THIRTY_MINUTES,
|
||||
ONE_HOUR,
|
||||
FOUR_HOURS,
|
||||
TODAY,
|
||||
THIS_WEEK,
|
||||
DATE_AND_TIME,
|
||||
} = CustomStatusDuration;
|
||||
|
||||
export const durationValues = {
|
||||
[DONT_CLEAR]: {
|
||||
id: t('custom_status.expiry_dropdown.dont_clear'),
|
||||
defaultMessage: "Don't clear",
|
||||
},
|
||||
[THIRTY_MINUTES]: {
|
||||
id: t('custom_status.expiry_dropdown.thirty_minutes'),
|
||||
defaultMessage: '30 minutes',
|
||||
},
|
||||
[ONE_HOUR]: {
|
||||
id: t('custom_status.expiry_dropdown.one_hour'),
|
||||
defaultMessage: '1 hour',
|
||||
},
|
||||
[FOUR_HOURS]: {
|
||||
id: t('custom_status.expiry_dropdown.four_hours'),
|
||||
defaultMessage: '4 hours',
|
||||
},
|
||||
[TODAY]: {
|
||||
id: t('custom_status.expiry_dropdown.today'),
|
||||
defaultMessage: 'Today',
|
||||
},
|
||||
[THIS_WEEK]: {
|
||||
id: t('custom_status.expiry_dropdown.this_week'),
|
||||
defaultMessage: 'This week',
|
||||
},
|
||||
[DATE_AND_TIME]: {
|
||||
id: t('custom_status.expiry_dropdown.date_and_time'),
|
||||
defaultMessage: 'Date and Time',
|
||||
},
|
||||
};
|
||||
export const CUSTOM_STATUS_TEXT_CHARACTER_LIMIT = 100;
|
||||
export const SET_CUSTOM_STATUS_FAILURE = 'set_custom_status_failure';
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {getCurrentUser} from '@mm-redux/selectors/entities/common';
|
||||
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {UserProfile} from '@mm-redux/types/users';
|
||||
|
||||
export function getUserTimezone(state: GlobalState, id: string) {
|
||||
const profile = state.entities.users.profiles[id];
|
||||
return getTimezoneForUserProfile(profile);
|
||||
}
|
||||
|
||||
function getTimezoneForUserProfile(profile: UserProfile) {
|
||||
if (profile && profile.timezone) {
|
||||
return {
|
||||
...profile.timezone,
|
||||
@@ -24,3 +32,17 @@ export function isTimezoneEnabled(state: GlobalState) {
|
||||
const {config} = state.entities.general;
|
||||
return config.ExperimentalTimezone === 'true';
|
||||
}
|
||||
|
||||
export const getCurrentUserTimezone = createSelector(
|
||||
getCurrentUser,
|
||||
isTimezoneEnabled,
|
||||
(user, enabledTimezone) => {
|
||||
let timezone;
|
||||
if (enabledTimezone) {
|
||||
const userTimezone = getTimezoneForUserProfile(user);
|
||||
timezone = userTimezone.useAutomaticTimezone ? userTimezone.automaticTimezone : userTimezone.manualTimezone;
|
||||
}
|
||||
|
||||
return timezone;
|
||||
},
|
||||
);
|
||||
|
||||
@@ -80,7 +80,19 @@ export type UserStatus = {
|
||||
active_channel?: string;
|
||||
}
|
||||
|
||||
export enum CustomStatusDuration {
|
||||
DONT_CLEAR = '',
|
||||
THIRTY_MINUTES = 'thirty_minutes',
|
||||
ONE_HOUR = 'one_hour',
|
||||
FOUR_HOURS = 'four_hours',
|
||||
TODAY = 'today',
|
||||
THIS_WEEK = 'this_week',
|
||||
DATE_AND_TIME = 'date_and_time',
|
||||
}
|
||||
|
||||
export type UserCustomStatus = {
|
||||
emoji: string;
|
||||
text: string;
|
||||
expires_at?: string;
|
||||
duration: CustomStatusDuration;
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ exports[`channelInfo should match snapshot 1`] = `
|
||||
header=""
|
||||
isArchived={false}
|
||||
isCustomStatusEnabled={false}
|
||||
isCustomStatusExpired={false}
|
||||
isGroupConstrained={false}
|
||||
isTeammateGuest={false}
|
||||
memberCount={2}
|
||||
|
||||
@@ -2034,7 +2034,6 @@ exports[`channel_info_header should match snapshot with custom status enabled 1`
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"paddingVertical": 10,
|
||||
"position": "relative",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -2045,54 +2044,494 @@ exports[`channel_info_header should match snapshot with custom status enabled 1`
|
||||
size={20}
|
||||
testID="custom_status.emoji.calendar"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginRight": 8,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginRight": 8,
|
||||
},
|
||||
Object {
|
||||
"bottom": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<CustomStatusText
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 15,
|
||||
"width": "80%",
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
>
|
||||
<CustomStatusText
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 15,
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef
|
||||
onLongPress={[Function]}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Purpose"
|
||||
id="channel_info.purpose"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
"marginBottom": 10,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
testID="channel_info.header.purpose"
|
||||
>
|
||||
Purpose string
|
||||
</Text>
|
||||
</View>
|
||||
</ForwardRef>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ForwardRef
|
||||
onLongPress={[Function]}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"paddingHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Header"
|
||||
id="channel_info.header"
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
"marginBottom": 10,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<Connect(Markdown)
|
||||
baseTextStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 13,
|
||||
"lineHeight": 20,
|
||||
}
|
||||
}
|
||||
blockStyles={
|
||||
Object {
|
||||
"adjacentParagraph": Object {
|
||||
"marginTop": 6,
|
||||
},
|
||||
"horizontalRule": Object {
|
||||
"backgroundColor": "#3d3c40",
|
||||
"height": 0.5,
|
||||
"marginVertical": 10,
|
||||
},
|
||||
"quoteBlockIcon": Object {
|
||||
"color": undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
disableAtChannelMentionHighlight={true}
|
||||
disableGallery={true}
|
||||
onChannelLinkPress={[MockFunction]}
|
||||
testID="channel_info.header.header"
|
||||
textStyles={
|
||||
Object {
|
||||
"code": Object {
|
||||
"alignSelf": "center",
|
||||
"backgroundColor": undefined,
|
||||
"fontFamily": "Menlo",
|
||||
},
|
||||
"codeBlock": Object {
|
||||
"fontFamily": "Menlo",
|
||||
},
|
||||
"del": Object {
|
||||
"textDecorationLine": "line-through",
|
||||
},
|
||||
"emph": Object {
|
||||
"fontStyle": "italic",
|
||||
},
|
||||
"error": Object {
|
||||
"color": "#fd5960",
|
||||
},
|
||||
"heading1": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading1Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"heading2": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading2Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"heading3": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading3Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"heading4": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading4Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"heading5": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading5Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"heading6": Object {
|
||||
"fontSize": 17,
|
||||
"fontWeight": "700",
|
||||
"lineHeight": 25,
|
||||
},
|
||||
"heading6Text": Object {
|
||||
"paddingBottom": 8,
|
||||
},
|
||||
"link": Object {
|
||||
"color": "#2389d7",
|
||||
},
|
||||
"mention": Object {
|
||||
"color": "#2389d7",
|
||||
},
|
||||
"mention_highlight": Object {
|
||||
"backgroundColor": "#ffe577",
|
||||
"color": "#166de0",
|
||||
},
|
||||
"strong": Object {
|
||||
"fontWeight": "bold",
|
||||
},
|
||||
"table_header_row": Object {
|
||||
"fontWeight": "700",
|
||||
},
|
||||
}
|
||||
}
|
||||
value="Header string"
|
||||
/>
|
||||
</View>
|
||||
</ForwardRef>
|
||||
</View>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"backgroundColor": "transparent",
|
||||
"color": undefined,
|
||||
"flexDirection": "row",
|
||||
"fontSize": 12,
|
||||
"marginTop": 5,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<InjectIntl(FormattedText)
|
||||
defaultMessage="Created by {creator} on "
|
||||
id="mobile.routes.channelInfo.createdBy"
|
||||
values={
|
||||
Object {
|
||||
"creator": "Creator",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<FormattedDate
|
||||
format="LL"
|
||||
value={123}
|
||||
/>
|
||||
</Text>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`channel_info_header should match snapshot with custom status expiry 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"borderBottomColor": undefined,
|
||||
"borderBottomWidth": 1,
|
||||
"marginBottom": 40,
|
||||
"paddingVertical": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"paddingVertical": 10,
|
||||
},
|
||||
Object {
|
||||
"paddingHorizontal": 15,
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
<ChannelIcon
|
||||
hasDraft={false}
|
||||
isActive={false}
|
||||
isArchived={false}
|
||||
isInfo={true}
|
||||
isUnread={false}
|
||||
membersCount={3}
|
||||
size={24}
|
||||
testID="channel_info.header.channel_icon"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
type="D"
|
||||
/>
|
||||
<Text
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 15,
|
||||
"fontWeight": "600",
|
||||
"marginLeft": 13,
|
||||
}
|
||||
}
|
||||
testID="channel_info.header.display_name"
|
||||
>
|
||||
Channel name
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"paddingHorizontal": 15,
|
||||
},
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flexDirection": "row",
|
||||
"paddingVertical": 10,
|
||||
},
|
||||
]
|
||||
}
|
||||
testID="channel_info.header.custom_status"
|
||||
>
|
||||
<Connect(Emoji)
|
||||
emojiName="calendar"
|
||||
size={20}
|
||||
testID="custom_status.emoji.calendar"
|
||||
textStyle={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginRight": 8,
|
||||
},
|
||||
Object {
|
||||
"bottom": 8,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"width": "90%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
ellipsizeMode="tail"
|
||||
numberOfLines={1}
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"flex": 1,
|
||||
"fontSize": 15,
|
||||
"height": "100%",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<CustomStatusExpiry
|
||||
showPrefix={true}
|
||||
textStyles={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"fontSize": 13,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
time="2200-04-13T18:09:12.451Z"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
|
||||
@@ -50,6 +50,8 @@ export default class ChannelInfo extends PureComponent {
|
||||
theme: PropTypes.object.isRequired,
|
||||
customStatus: PropTypes.object,
|
||||
isCustomStatusEnabled: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpired: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpirySupported: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@@ -172,6 +174,8 @@ export default class ChannelInfo extends PureComponent {
|
||||
isTeammateGuest,
|
||||
customStatus,
|
||||
isCustomStatusEnabled,
|
||||
isCustomStatusExpired,
|
||||
isCustomStatusExpirySupported,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -207,6 +211,8 @@ export default class ChannelInfo extends PureComponent {
|
||||
testID='channel_info.header'
|
||||
customStatus={customStatus}
|
||||
isCustomStatusEnabled={isCustomStatusEnabled}
|
||||
isCustomStatusExpired={isCustomStatusExpired}
|
||||
isCustomStatusExpirySupported={isCustomStatusExpirySupported}
|
||||
/>
|
||||
}
|
||||
<View style={style.rowsContainer}>
|
||||
|
||||
@@ -49,6 +49,7 @@ describe('channelInfo', () => {
|
||||
isDirectMessage: false,
|
||||
isLandscape: false,
|
||||
isCustomStatusEnabled: false,
|
||||
isCustomStatusExpired: false,
|
||||
actions: {
|
||||
getChannelStats: jest.fn(),
|
||||
getCustomEmojisInText: jest.fn(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import Clipboard from '@react-native-community/clipboard';
|
||||
|
||||
import {popToRoot} from '@actions/navigation';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedDate from '@components/formatted_date';
|
||||
@@ -47,6 +48,8 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
timezone: PropTypes.string,
|
||||
customStatus: PropTypes.object,
|
||||
isCustomStatusEnabled: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpired: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpirySupported: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -147,6 +150,8 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
timezone,
|
||||
customStatus,
|
||||
isCustomStatusEnabled,
|
||||
isCustomStatusExpired,
|
||||
isCustomStatusExpirySupported,
|
||||
} = this.props;
|
||||
|
||||
const style = getStyleSheet(theme);
|
||||
@@ -157,6 +162,9 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
android: style.detail,
|
||||
});
|
||||
|
||||
const showCustomStatus = isCustomStatusEnabled && type === General.DM_CHANNEL && customStatus?.emoji && !isCustomStatusExpired;
|
||||
const showCustomStatusExpiry = Boolean(customStatus?.duration && isCustomStatusExpirySupported);
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<View style={[style.channelNameContainer, style.row]}>
|
||||
@@ -180,7 +188,7 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
{isCustomStatusEnabled && type === General.DM_CHANNEL && customStatus?.emoji &&
|
||||
{showCustomStatus && (
|
||||
<View
|
||||
style={[style.row, style.customStatusContainer]}
|
||||
testID={`${testID}.custom_status`}
|
||||
@@ -188,18 +196,28 @@ export default class ChannelInfoHeader extends React.PureComponent {
|
||||
<Emoji
|
||||
emojiName={customStatus.emoji}
|
||||
size={20}
|
||||
textStyle={style.iconContainer}
|
||||
textStyle={[style.iconContainer, {bottom: showCustomStatusExpiry ? 8 : 0}]}
|
||||
testID={`custom_status.emoji.${customStatus.emoji}`}
|
||||
/>
|
||||
<CustomStatusText
|
||||
text={customStatus.text}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusText}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
/>
|
||||
<View style={style.customStatus}>
|
||||
<CustomStatusText
|
||||
text={customStatus.text}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusText}
|
||||
ellipsizeMode='tail'
|
||||
numberOfLines={1}
|
||||
/>
|
||||
{showCustomStatusExpiry && (
|
||||
<CustomStatusExpiry
|
||||
time={customStatus.expires_at}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiry}
|
||||
showPrefix={true}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
)}
|
||||
{this.renderHasGuestText(style)}
|
||||
{purpose.length > 0 &&
|
||||
<View style={style.section}>
|
||||
@@ -299,16 +317,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
customStatusContainer: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
},
|
||||
customStatus: {
|
||||
width: '90%',
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
fontSize: 13,
|
||||
},
|
||||
customStatusText: {
|
||||
flex: 1,
|
||||
fontSize: 15,
|
||||
color: theme.centerChannelColor,
|
||||
width: '80%',
|
||||
height: '100%',
|
||||
},
|
||||
channelNameContainer: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -5,6 +5,7 @@ import {shallow} from 'enzyme';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
|
||||
import ChannelInfoHeader from './channel_info_header.js';
|
||||
|
||||
@@ -44,6 +45,8 @@ describe('channel_info_header', () => {
|
||||
isGroupConstrained: false,
|
||||
testID: 'channel_info.header',
|
||||
isCustomStatusEnabled: false,
|
||||
isCustomStatusExpired: false,
|
||||
isCustomStatusExpirySupported: false,
|
||||
};
|
||||
|
||||
test('should match snapshot', async () => {
|
||||
@@ -120,6 +123,7 @@ describe('channel_info_header', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
@@ -133,4 +137,25 @@ describe('channel_info_header', () => {
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should match snapshot with custom status expiry', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DATE_AND_TIME,
|
||||
expires_at: '2200-04-13T18:09:12.451Z',
|
||||
};
|
||||
|
||||
const wrapper = shallow(
|
||||
<ChannelInfoHeader
|
||||
{...baseProps}
|
||||
isCustomStatusEnabled={true}
|
||||
isCustomStatusExpirySupported={true}
|
||||
type={General.DM_CHANNEL}
|
||||
customStatus={customStatus}
|
||||
/>,
|
||||
{context: {intl: intlMock}},
|
||||
);
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import {getCurrentChannel, getCurrentChannelStats} from '@mm-redux/selectors/ent
|
||||
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {getUserIdFromChannelName} from '@mm-redux/utils/channel_utils';
|
||||
import {displayUsername} from '@mm-redux/utils/user_utils';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled} from '@selectors/custom_status';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired, isCustomStatusExpirySupported} from '@selectors/custom_status';
|
||||
import {isGuest} from '@utils/users';
|
||||
|
||||
import ChannelInfo from './channel_info';
|
||||
@@ -34,6 +34,8 @@ function makeMapStateToProps() {
|
||||
let isTeammateGuest = false;
|
||||
let customStatusEnabled = false;
|
||||
let customStatus;
|
||||
let customStatusExpired = true;
|
||||
let customStatusExpirySupported = false;
|
||||
const isDirectMessage = currentChannel.type === General.DM_CHANNEL;
|
||||
|
||||
if (isDirectMessage) {
|
||||
@@ -44,7 +46,9 @@ function makeMapStateToProps() {
|
||||
currentChannelGuestCount = 1;
|
||||
}
|
||||
customStatusEnabled = isCustomStatusEnabled(state);
|
||||
customStatus = customStatusEnabled ? getCustomStatus(state, teammateId) : undefined;
|
||||
customStatus = customStatusEnabled && getCustomStatus(state, teammateId);
|
||||
customStatusExpired = customStatusEnabled ? isCustomStatusExpired(state, customStatus) : true;
|
||||
customStatusExpirySupported = customStatusEnabled ? isCustomStatusExpirySupported(state) : false;
|
||||
}
|
||||
|
||||
if (currentChannel.type === General.GM_CHANNEL) {
|
||||
@@ -63,6 +67,8 @@ function makeMapStateToProps() {
|
||||
theme: getTheme(state),
|
||||
customStatus,
|
||||
isCustomStatusEnabled: customStatusEnabled,
|
||||
isCustomStatusExpired: customStatusExpired,
|
||||
isCustomStatusExpirySupported: customStatusExpirySupported,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ exports[`screens/custom_status_modal should match snapshot 1`] = `
|
||||
}
|
||||
customStatus={
|
||||
Object {
|
||||
"duration": "",
|
||||
"emoji": "calendar",
|
||||
"text": "In a meeting",
|
||||
}
|
||||
@@ -45,6 +46,7 @@ exports[`screens/custom_status_modal should match snapshot 1`] = `
|
||||
recentCustomStatuses={
|
||||
Array [
|
||||
Object {
|
||||
"duration": "",
|
||||
"emoji": "calendar",
|
||||
"text": "In a meeting",
|
||||
},
|
||||
@@ -122,6 +124,7 @@ exports[`screens/custom_status_modal should match snapshot when user has no cust
|
||||
recentCustomStatuses={
|
||||
Array [
|
||||
Object {
|
||||
"duration": "",
|
||||
"emoji": "calendar",
|
||||
"text": "In a meeting",
|
||||
},
|
||||
@@ -170,6 +173,7 @@ exports[`screens/custom_status_modal should match snapshot when user has no rece
|
||||
}
|
||||
customStatus={
|
||||
Object {
|
||||
"duration": "",
|
||||
"emoji": "calendar",
|
||||
"text": "In a meeting",
|
||||
}
|
||||
|
||||
@@ -49,43 +49,45 @@ exports[`screens/custom_status_suggestion should match snapshot 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
<View>
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
}
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -141,95 +143,52 @@ exports[`screens/custom_status_suggestion should match snapshot with separator a
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
<View>
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
}
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": 13,
|
||||
"top": 4,
|
||||
}
|
||||
}
|
||||
>
|
||||
<ClearButton
|
||||
containerSize={40}
|
||||
handlePress={[Function]}
|
||||
iconName="close"
|
||||
size={18}
|
||||
testID="custom_status_suggestion.clear.button"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
"marginRight": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -7,11 +7,13 @@ import Preferences from '@mm-redux/constants/preferences';
|
||||
import CustomStatusModal from '@screens/custom_status/custom_status_modal';
|
||||
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
|
||||
describe('screens/custom_status_modal', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
const baseProps = {
|
||||
|
||||
@@ -1,46 +1,65 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import React from 'react';
|
||||
import {intlShape, injectIntl} from 'react-intl';
|
||||
import {View, Text, TouchableOpacity, TextInput, Keyboard, KeyboardAvoidingView, Platform, ScrollView, StyleProp, ViewStyle} from 'react-native';
|
||||
import {Navigation, NavigationComponent, NavigationComponentProps, OptionsTopBarButton, Options, NavigationButtonPressedEvent} from 'react-native-navigation';
|
||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {dismissModal, showModal, mergeNavigationOptions} from '@actions/navigation';
|
||||
import {dismissModal, showModal, mergeNavigationOptions, goToScreen} from '@actions/navigation';
|
||||
import Emoji from '@components/emoji';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
import StatusBar from '@components/status_bar';
|
||||
import {CustomStatus, DeviceTypes} from '@constants';
|
||||
import {durationValues} from '@constants/custom_status';
|
||||
import {ActionFunc, ActionResult} from '@mm-redux/types/actions';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
import {CustomStatusDuration, UserCustomStatus} from '@mm-redux/types/users';
|
||||
import EventEmitter from '@mm-redux/utils/event_emitter';
|
||||
import CustomStatusSuggestion from '@screens/custom_status/custom_status_suggestion';
|
||||
import {getRoundedTime} from '@screens/custom_status_clear_after/date_time_selector';
|
||||
import {t} from '@utils/i18n';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {getCurrentMomentForTimezone} from '@utils/timezone';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type DefaultUserCustomStatus = {
|
||||
emoji: string;
|
||||
message: string;
|
||||
messageDefault: string;
|
||||
durationDefault: CustomStatusDuration;
|
||||
};
|
||||
|
||||
const {
|
||||
DONT_CLEAR,
|
||||
THIRTY_MINUTES,
|
||||
ONE_HOUR,
|
||||
FOUR_HOURS,
|
||||
TODAY,
|
||||
THIS_WEEK,
|
||||
DATE_AND_TIME,
|
||||
} = CustomStatusDuration;
|
||||
|
||||
const defaultCustomStatusSuggestions: DefaultUserCustomStatus[] = [
|
||||
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting'},
|
||||
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch'},
|
||||
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick'},
|
||||
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home'},
|
||||
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation'},
|
||||
{emoji: 'calendar', message: t('custom_status.suggestions.in_a_meeting'), messageDefault: 'In a meeting', durationDefault: ONE_HOUR},
|
||||
{emoji: 'hamburger', message: t('custom_status.suggestions.out_for_lunch'), messageDefault: 'Out for lunch', durationDefault: THIRTY_MINUTES},
|
||||
{emoji: 'sneezing_face', message: t('custom_status.suggestions.out_sick'), messageDefault: 'Out sick', durationDefault: TODAY},
|
||||
{emoji: 'house', message: t('custom_status.suggestions.working_from_home'), messageDefault: 'Working from home', durationDefault: TODAY},
|
||||
{emoji: 'palm_tree', message: t('custom_status.suggestions.on_a_vacation'), messageDefault: 'On a vacation', durationDefault: THIS_WEEK},
|
||||
];
|
||||
|
||||
const defaultDuration: CustomStatusDuration = TODAY;
|
||||
|
||||
interface Props extends NavigationComponentProps {
|
||||
intl: typeof intlShape;
|
||||
theme: Theme;
|
||||
customStatus: UserCustomStatus;
|
||||
userTimezone: string;
|
||||
recentCustomStatuses: UserCustomStatus[];
|
||||
isLandscape: boolean;
|
||||
actions: {
|
||||
@@ -48,11 +67,15 @@ interface Props extends NavigationComponentProps {
|
||||
unsetCustomStatus: () => ActionFunc;
|
||||
removeRecentCustomStatus: (customStatus: UserCustomStatus) => ActionFunc;
|
||||
};
|
||||
isExpirySupported: boolean;
|
||||
isCustomStatusExpired: boolean;
|
||||
}
|
||||
|
||||
type State = {
|
||||
emoji: string;
|
||||
text: string;
|
||||
duration: CustomStatusDuration;
|
||||
expires_at: Moment;
|
||||
}
|
||||
|
||||
class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
@@ -75,21 +98,31 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
const {customStatus, userTimezone, isCustomStatusExpired, intl, theme, componentId} = props;
|
||||
|
||||
this.rightButton.text = props.intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'});
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
this.rightButton.text = intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'});
|
||||
this.rightButton.color = theme.sidebarHeaderTextColor;
|
||||
|
||||
const options: Options = {
|
||||
topBar: {
|
||||
rightButtons: [this.rightButton],
|
||||
},
|
||||
};
|
||||
mergeNavigationOptions(componentId, options);
|
||||
|
||||
mergeNavigationOptions(props.componentId, options);
|
||||
const currentTime = getCurrentMomentForTimezone(userTimezone);
|
||||
|
||||
let initialCustomExpiryTime: Moment = getRoundedTime(currentTime);
|
||||
const isCurrentCustomStatusSet = !isCustomStatusExpired && (customStatus?.text || customStatus?.emoji);
|
||||
if (isCurrentCustomStatusSet && customStatus?.duration === DATE_AND_TIME && customStatus?.expires_at) {
|
||||
initialCustomExpiryTime = moment(customStatus?.expires_at);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
emoji: props.customStatus?.emoji,
|
||||
text: props.customStatus?.text || '',
|
||||
emoji: isCurrentCustomStatusSet ? customStatus?.emoji : '',
|
||||
text: isCurrentCustomStatusSet ? customStatus?.text : '',
|
||||
duration: isCurrentCustomStatusSet ? (customStatus?.duration ?? DONT_CLEAR) : defaultDuration,
|
||||
expires_at: initialCustomExpiryTime,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,16 +139,25 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
}
|
||||
|
||||
handleSetStatus = async () => {
|
||||
const {emoji, text} = this.state;
|
||||
const {emoji, text, duration} = this.state;
|
||||
const isStatusSet = emoji || text;
|
||||
const {customStatus} = this.props;
|
||||
const {customStatus, isExpirySupported} = this.props;
|
||||
if (isStatusSet) {
|
||||
const isStatusSame = customStatus?.emoji === emoji && customStatus?.text === text;
|
||||
let isStatusSame = customStatus?.emoji === emoji && customStatus?.text === text && customStatus?.duration === duration;
|
||||
if (isStatusSame && duration === DATE_AND_TIME) {
|
||||
isStatusSame = customStatus?.expires_at === this.calculateExpiryTime(duration);
|
||||
}
|
||||
if (!isStatusSame) {
|
||||
const status = {
|
||||
const status: UserCustomStatus = {
|
||||
emoji: emoji || 'speech_balloon',
|
||||
text: text.trim(),
|
||||
duration: DONT_CLEAR,
|
||||
};
|
||||
|
||||
if (isExpirySupported) {
|
||||
status.duration = duration;
|
||||
status.expires_at = this.calculateExpiryTime(duration);
|
||||
}
|
||||
const {error} = await this.props.actions.setCustomStatus(status);
|
||||
if (error) {
|
||||
EventEmitter.emit(CustomStatus.SET_CUSTOM_STATUS_FAILURE);
|
||||
@@ -128,30 +170,65 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
dismissModal();
|
||||
};
|
||||
|
||||
calculateExpiryTime = (duration: CustomStatusDuration): string => {
|
||||
const {userTimezone} = this.props;
|
||||
const currentTime = getCurrentMomentForTimezone(userTimezone);
|
||||
const {expires_at} = this.state;
|
||||
switch (duration) {
|
||||
case THIRTY_MINUTES:
|
||||
return currentTime.add(30, 'minutes').seconds(0).milliseconds(0).toISOString();
|
||||
case ONE_HOUR:
|
||||
return currentTime.add(1, 'hour').seconds(0).milliseconds(0).toISOString();
|
||||
case FOUR_HOURS:
|
||||
return currentTime.add(4, 'hours').seconds(0).milliseconds(0).toISOString();
|
||||
case TODAY:
|
||||
return currentTime.endOf('day').toISOString();
|
||||
case THIS_WEEK:
|
||||
return currentTime.endOf('week').toISOString();
|
||||
case DATE_AND_TIME:
|
||||
return expires_at.toISOString();
|
||||
case DONT_CLEAR:
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
handleTextChange = (value: string) => this.setState({text: value});
|
||||
|
||||
handleRecentCustomStatusClear = (status: UserCustomStatus) => this.props.actions.removeRecentCustomStatus(status);
|
||||
|
||||
clearHandle = () => {
|
||||
this.setState({emoji: '', text: ''});
|
||||
this.setState({emoji: '', text: '', duration: defaultDuration});
|
||||
};
|
||||
|
||||
handleSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text} = status;
|
||||
this.setState({emoji, text});
|
||||
handleCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
this.setState({emoji, text, duration});
|
||||
};
|
||||
|
||||
handleRecentCustomStatusSuggestionClick = (status: UserCustomStatus) => {
|
||||
const {emoji, text, duration} = status;
|
||||
this.setState({emoji, text, duration: duration || DONT_CLEAR});
|
||||
if (duration === DATE_AND_TIME) {
|
||||
this.openClearAfterModal();
|
||||
}
|
||||
};
|
||||
|
||||
renderRecentCustomStatuses = (style: Record<string, StyleProp<ViewStyle>>) => {
|
||||
const {recentCustomStatuses, theme} = this.props;
|
||||
const {recentCustomStatuses, theme, isExpirySupported} = this.props;
|
||||
|
||||
const recentStatuses = recentCustomStatuses.map((status: UserCustomStatus, index: number) => (
|
||||
<CustomStatusSuggestion
|
||||
key={status.text}
|
||||
handleSuggestionClick={this.handleSuggestionClick}
|
||||
handleSuggestionClick={this.handleRecentCustomStatusSuggestionClick}
|
||||
handleClear={this.handleRecentCustomStatusClear}
|
||||
emoji={status.emoji}
|
||||
text={status.text}
|
||||
theme={theme}
|
||||
separator={index !== recentCustomStatuses.length - 1}
|
||||
duration={status.duration}
|
||||
expires_at={status.expires_at}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -177,22 +254,25 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
};
|
||||
|
||||
renderCustomStatusSuggestions = (style: Record<string, StyleProp<ViewStyle>>) => {
|
||||
const {recentCustomStatuses, theme, intl} = this.props;
|
||||
const {recentCustomStatuses, theme, intl, isExpirySupported} = this.props;
|
||||
const recentCustomStatusTexts = recentCustomStatuses.map((status: UserCustomStatus) => status.text);
|
||||
const customStatusSuggestions = defaultCustomStatusSuggestions.
|
||||
map((status) => ({
|
||||
emoji: status.emoji,
|
||||
text: intl.formatMessage({id: status.message, defaultMessage: status.messageDefault}),
|
||||
duration: status.durationDefault,
|
||||
})).
|
||||
filter((status: UserCustomStatus) => !recentCustomStatusTexts.includes(status.text)).
|
||||
map((status: UserCustomStatus, index: number, arr: UserCustomStatus[]) => (
|
||||
<CustomStatusSuggestion
|
||||
key={status.text}
|
||||
handleSuggestionClick={this.handleSuggestionClick}
|
||||
handleSuggestionClick={this.handleCustomStatusSuggestionClick}
|
||||
emoji={status.emoji}
|
||||
text={status.text}
|
||||
theme={theme}
|
||||
separator={index !== arr.length - 1}
|
||||
duration={status.duration}
|
||||
isExpirySupported={isExpirySupported}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -236,11 +316,31 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
this.setState({emoji});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {emoji, text} = this.state;
|
||||
const {theme, isLandscape} = this.props;
|
||||
handleClearAfterClick = (duration: CustomStatusDuration, expires_at: string) =>
|
||||
this.setState({
|
||||
duration,
|
||||
expires_at: duration === DATE_AND_TIME && expires_at ? moment(expires_at) : this.state.expires_at,
|
||||
});
|
||||
|
||||
const isStatusSet = emoji || text;
|
||||
openClearAfterModal = async () => {
|
||||
const {intl, theme} = this.props;
|
||||
const screen = 'ClearAfter';
|
||||
const title = intl.formatMessage({id: 'mobile.custom_status.clear_after', defaultMessage: 'Clear After'});
|
||||
const passProps = {
|
||||
handleClearAfterClick: this.handleClearAfterClick,
|
||||
initialDuration: this.state.duration,
|
||||
intl,
|
||||
theme,
|
||||
};
|
||||
|
||||
goToScreen(screen, title, passProps);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {emoji, text, duration, expires_at} = this.state;
|
||||
const {theme, isLandscape, intl, isExpirySupported} = this.props;
|
||||
|
||||
const isStatusSet = Boolean(emoji || text);
|
||||
const style = getStyleSheet(theme);
|
||||
const customStatusEmoji = (
|
||||
<TouchableOpacity
|
||||
@@ -265,6 +365,42 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const clearAfterTime = duration && duration === DATE_AND_TIME ? (
|
||||
<View style={style.expiryTime}>
|
||||
<CustomStatusExpiry
|
||||
time={expires_at.toDate()}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiry}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<FormattedText
|
||||
id={durationValues[duration].id}
|
||||
defaultMessage={durationValues[duration].defaultMessage}
|
||||
style={style.expiryTime}
|
||||
/>
|
||||
);
|
||||
|
||||
const clearAfter = isExpirySupported && (
|
||||
<TouchableOpacity
|
||||
testID={'custom_status.clear_after.action'}
|
||||
onPress={this.openClearAfterModal}
|
||||
>
|
||||
<View
|
||||
testID={`custom_status.duration.${duration}`}
|
||||
style={style.inputContainer}
|
||||
>
|
||||
<Text style={style.expiryTimeLabel}>{intl.formatMessage({id: 'mobile.custom_status.clear_after', defaultMessage: 'Clear After'})}</Text>
|
||||
{clearAfterTime}
|
||||
<CompassIcon
|
||||
name='chevron-right'
|
||||
size={24}
|
||||
style={style.rightIcon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const customStatusInput = (
|
||||
<View style={style.inputContainer}>
|
||||
<TextInput
|
||||
@@ -285,6 +421,9 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
underlineColorAndroid='transparent'
|
||||
value={text}
|
||||
/>
|
||||
{isStatusSet && (
|
||||
<View style={style.divider}/>
|
||||
)}
|
||||
{customStatusEmoji}
|
||||
{isStatusSet ? (
|
||||
<View
|
||||
@@ -321,10 +460,14 @@ class CustomStatusModal extends NavigationComponent<Props, State> {
|
||||
>
|
||||
<StatusBar/>
|
||||
<View style={style.scrollView}>
|
||||
{customStatusInput}
|
||||
<View style={style.block}>
|
||||
{customStatusInput}
|
||||
{isStatusSet && clearAfter}
|
||||
</View>
|
||||
{this.renderRecentCustomStatuses(style)}
|
||||
{this.renderCustomStatusSuggestions(style)}
|
||||
</View>
|
||||
<View style={style.separator}/>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
@@ -338,20 +481,15 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
paddingTop: 32,
|
||||
},
|
||||
inputContainer: {
|
||||
position: 'relative',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderTopWidth: 1,
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
justifyContent: 'center',
|
||||
height: 48,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
input: {
|
||||
@@ -361,7 +499,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
fontSize: 17,
|
||||
paddingHorizontal: 52,
|
||||
textAlignVertical: 'center',
|
||||
height: 48,
|
||||
height: '100%',
|
||||
},
|
||||
icon: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.64),
|
||||
@@ -372,7 +510,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
iconContainer: {
|
||||
position: 'absolute',
|
||||
left: 14,
|
||||
top: 12,
|
||||
top: 10,
|
||||
},
|
||||
separator: {
|
||||
marginTop: 32,
|
||||
@@ -394,5 +532,30 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
top: 3,
|
||||
right: 14,
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginRight: 16,
|
||||
marginLeft: 52,
|
||||
},
|
||||
expiryTimeLabel: {
|
||||
fontSize: 17,
|
||||
paddingLeft: 16,
|
||||
textAlignVertical: 'center',
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
expiryTime: {
|
||||
position: 'absolute',
|
||||
right: 42,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
rightIcon: {
|
||||
position: 'absolute',
|
||||
right: 18,
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import CustomStatusSuggestion from '@screens/custom_status/custom_status_suggestion';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
describe('screens/custom_status_suggestion', () => {
|
||||
const baseProps = {
|
||||
@@ -15,20 +16,21 @@ describe('screens/custom_status_suggestion', () => {
|
||||
text: 'In a meeting',
|
||||
theme: Preferences.THEMES.default,
|
||||
separator: false,
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<CustomStatusSuggestion
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.dive().getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with separator and clear button', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<CustomStatusSuggestion
|
||||
{...baseProps}
|
||||
separator={true}
|
||||
@@ -36,17 +38,17 @@ describe('screens/custom_status_suggestion', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
expect(wrapper.dive().getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call handleSuggestionClick on clicking the suggestion', () => {
|
||||
const wrapper = shallow(
|
||||
const wrapper = shallowWithIntl(
|
||||
<CustomStatusSuggestion
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.find(TouchableOpacity).simulate('press');
|
||||
wrapper.dive().find(TouchableOpacity).simulate('press');
|
||||
expect(baseProps.handleSuggestionClick).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,41 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import React, {useCallback} from 'react';
|
||||
import {View, TouchableOpacity, Text} from 'react-native';
|
||||
|
||||
import Emoji from '@components/emoji';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import {durationValues} from '@constants/custom_status';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
import {CustomStatusDuration, UserCustomStatus} from '@mm-redux/types/users';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
type Props = {
|
||||
intl: typeof intlShape;
|
||||
handleSuggestionClick: (status: UserCustomStatus) => void;
|
||||
emoji: string;
|
||||
text: string;
|
||||
handleClear?: (status: UserCustomStatus) => void;
|
||||
theme: Theme;
|
||||
separator: boolean;
|
||||
duration: CustomStatusDuration;
|
||||
isExpirySupported: boolean;
|
||||
expires_at?: string;
|
||||
};
|
||||
|
||||
const CustomStatusSuggestion = (props: Props) => {
|
||||
const {handleSuggestionClick, emoji, text, theme, separator, handleClear} = props;
|
||||
const CustomStatusSuggestion = ({handleSuggestionClick, emoji, text, theme, separator, handleClear, duration, expires_at, intl, isExpirySupported}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const handleClick = useCallback(preventDoubleTap(() => {
|
||||
handleSuggestionClick({emoji, text});
|
||||
handleSuggestionClick({emoji, text, duration});
|
||||
}), []);
|
||||
|
||||
const handleSuggestionClear = useCallback(() => {
|
||||
if (handleClear) {
|
||||
handleClear({emoji, text});
|
||||
handleClear({emoji, text, duration, expires_at});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearButton = handleClear ?
|
||||
const clearButton = handleClear && expires_at ?
|
||||
(
|
||||
<View style={style.clearButtonContainer}>
|
||||
<ClearButton
|
||||
@@ -62,11 +67,22 @@ const CustomStatusSuggestion = (props: Props) => {
|
||||
</Text>
|
||||
<View style={style.wrapper}>
|
||||
<View style={style.textContainer}>
|
||||
<CustomStatusText
|
||||
text={text}
|
||||
theme={theme}
|
||||
textStyle={{color: theme.centerChannelColor}}
|
||||
/>
|
||||
<View>
|
||||
<CustomStatusText
|
||||
text={text}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusText}
|
||||
/>
|
||||
</View>
|
||||
{Boolean(duration && isExpirySupported) && (
|
||||
<View style={{paddingTop: 5}}>
|
||||
<CustomStatusText
|
||||
text={intl.formatMessage(durationValues[duration])}
|
||||
theme={theme}
|
||||
textStyle={style.customStatusDuration}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{clearButton}
|
||||
{separator && <View style={style.divider}/>}
|
||||
@@ -76,7 +92,7 @@ const CustomStatusSuggestion = (props: Props) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomStatusSuggestion;
|
||||
export default injectIntl(CustomStatusSuggestion);
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
@@ -111,6 +127,14 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginRight: 16,
|
||||
},
|
||||
customStatusDuration: {
|
||||
color: changeOpacity(theme.centerChannelColor, 0.6),
|
||||
fontSize: 15,
|
||||
},
|
||||
customStatusText: {
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,24 +6,32 @@ import {bindActionCreators, Dispatch} from 'redux';
|
||||
|
||||
import {setCustomStatus, unsetCustomStatus, removeRecentCustomStatus} from '@actions/views/custom_status';
|
||||
import {getTheme} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserTimezone} from '@mm-redux/selectors/entities/timezone';
|
||||
import {GenericAction} from '@mm-redux/types/actions';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
import CustomStatusModal from '@screens/custom_status/custom_status_modal';
|
||||
import {getRecentCustomStatuses, makeGetCustomStatus} from '@selectors/custom_status';
|
||||
import {getRecentCustomStatuses, isCustomStatusExpired, isCustomStatusExpirySupported, makeGetCustomStatus} from '@selectors/custom_status';
|
||||
import {isLandscape} from '@selectors/device';
|
||||
|
||||
function makeMapStateToProps() {
|
||||
const getCustomStatus = makeGetCustomStatus();
|
||||
return (state: GlobalState) => {
|
||||
const customStatus = getCustomStatus(state);
|
||||
const customStatus: UserCustomStatus | undefined = getCustomStatus(state);
|
||||
const recentCustomStatuses = getRecentCustomStatuses(state);
|
||||
const theme = getTheme(state);
|
||||
const userTimezone = getCurrentUserTimezone(state);
|
||||
const isExpirySupported = isCustomStatusExpirySupported(state);
|
||||
const customStatusExpired = isCustomStatusExpired(state, customStatus);
|
||||
|
||||
return {
|
||||
userTimezone,
|
||||
customStatus,
|
||||
recentCustomStatuses,
|
||||
theme,
|
||||
isLandscape: isLandscape(state),
|
||||
isExpirySupported,
|
||||
isCustomStatusExpired: customStatusExpired,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`screens/clear_after_menu_item should match snapshot 1`] = `
|
||||
<View>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
testID="clear_after.menu_item."
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginBottom": 2,
|
||||
"marginLeft": 5,
|
||||
"position": "relative",
|
||||
"width": "70%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
text="Don't clear"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ForwardRef>
|
||||
</View>
|
||||
`;
|
||||
|
||||
exports[`screens/clear_after_menu_item should match snapshot with separator and selected check 1`] = `
|
||||
<View>
|
||||
<ForwardRef
|
||||
onPress={[Function]}
|
||||
testID="clear_after.menu_item."
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"display": "flex",
|
||||
"flexDirection": "row",
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"marginBottom": 2,
|
||||
"marginLeft": 5,
|
||||
"position": "relative",
|
||||
"width": "70%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
text="Don't clear"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"position": "absolute",
|
||||
"right": 14,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CompassIcon
|
||||
name="check"
|
||||
size={24}
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": 1000,
|
||||
"color": "#166de0",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "rgba(61,60,64,0.2)",
|
||||
"height": 1,
|
||||
"marginHorizontal": 16,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</ForwardRef>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,64 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`screens/clear_after_modal should match snapshot 1`] = `
|
||||
<ClearAfterModal
|
||||
handleClearAfterClick={[MockFunction]}
|
||||
initialDuration=""
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": "span",
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,116 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`screens/date_time_selector should match snapshot 1`] = `
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
"paddingTop": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"flex": 1,
|
||||
"flexDirection": "row",
|
||||
"justifyContent": "space-evenly",
|
||||
"marginBottom": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={Object {}}
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="clear_after.menu_item.date_and_time.button.date"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#007AFF",
|
||||
"fontSize": 18,
|
||||
"margin": 8,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#166de0",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Select Date
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
accessibilityRole="button"
|
||||
accessibilityState={Object {}}
|
||||
accessible={true}
|
||||
focusable={true}
|
||||
onClick={[Function]}
|
||||
onResponderGrant={[Function]}
|
||||
onResponderMove={[Function]}
|
||||
onResponderRelease={[Function]}
|
||||
onResponderTerminate={[Function]}
|
||||
onResponderTerminationRequest={[Function]}
|
||||
onStartShouldSetResponder={[Function]}
|
||||
style={
|
||||
Object {
|
||||
"opacity": 1,
|
||||
}
|
||||
}
|
||||
testID="clear_after.menu_item.date_and_time.button.time"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Array [
|
||||
Object {},
|
||||
]
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Array [
|
||||
Object {
|
||||
"color": "#007AFF",
|
||||
"fontSize": 18,
|
||||
"margin": 8,
|
||||
"textAlign": "center",
|
||||
},
|
||||
Object {
|
||||
"color": "#166de0",
|
||||
},
|
||||
]
|
||||
}
|
||||
>
|
||||
Select Time
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
`;
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {TouchableOpacity} from 'react-native';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
import ClearAfterMenuItem from './clear_after_menu_item';
|
||||
|
||||
describe('screens/clear_after_menu_item', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
separator: false,
|
||||
isSelected: false,
|
||||
handleItemClick: jest.fn(),
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ClearAfterMenuItem {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.dive().getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with separator and selected check', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ClearAfterMenuItem
|
||||
{...baseProps}
|
||||
separator={true}
|
||||
isSelected={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(wrapper.dive().getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should call handleItemClick on clicking the suggestion', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ClearAfterMenuItem
|
||||
{...baseProps}
|
||||
/>,
|
||||
);
|
||||
|
||||
wrapper.dive().find(TouchableOpacity).simulate('press');
|
||||
expect(baseProps.handleItemClick).toBeCalled();
|
||||
});
|
||||
});
|
||||
149
app/screens/custom_status_clear_after/clear_after_menu_item.tsx
Normal file
149
app/screens/custom_status_clear_after/clear_after_menu_item.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment';
|
||||
import React, {useCallback, useState} from 'react';
|
||||
import {injectIntl, intlShape} from 'react-intl';
|
||||
import {View, TouchableOpacity} from 'react-native';
|
||||
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import {durationValues} from '@constants/custom_status';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import DateTimePicker from './date_time_selector';
|
||||
|
||||
type Props = {
|
||||
handleItemClick: (duration: CustomStatusDuration, expiresAt: string) => void;
|
||||
duration: CustomStatusDuration;
|
||||
theme: Theme;
|
||||
separator: boolean;
|
||||
isSelected: boolean;
|
||||
intl: typeof intlShape;
|
||||
showExpiryTime?: boolean;
|
||||
showDateTimePicker?: boolean;
|
||||
};
|
||||
|
||||
const {
|
||||
DONT_CLEAR,
|
||||
THIRTY_MINUTES,
|
||||
ONE_HOUR,
|
||||
FOUR_HOURS,
|
||||
TODAY,
|
||||
THIS_WEEK,
|
||||
DATE_AND_TIME,
|
||||
} = CustomStatusDuration;
|
||||
|
||||
const ClearAfterMenuItem = ({handleItemClick, duration, theme, separator, isSelected, intl, showExpiryTime = false, showDateTimePicker = false}: Props) => {
|
||||
const style = getStyleSheet(theme);
|
||||
|
||||
const [expiry, setExpiry] = useState<string>('');
|
||||
|
||||
const expiryMenuItems: { [key in CustomStatusDuration]: string } = {
|
||||
[DONT_CLEAR]: intl.formatMessage(durationValues[DONT_CLEAR]),
|
||||
[THIRTY_MINUTES]: intl.formatMessage(durationValues[THIRTY_MINUTES]),
|
||||
[ONE_HOUR]: intl.formatMessage(durationValues[ONE_HOUR]),
|
||||
[FOUR_HOURS]: intl.formatMessage(durationValues[FOUR_HOURS]),
|
||||
[TODAY]: intl.formatMessage(durationValues[TODAY]),
|
||||
[THIS_WEEK]: intl.formatMessage(durationValues[THIS_WEEK]),
|
||||
[DATE_AND_TIME]: intl.formatMessage({id: 'custom_status.expiry_dropdown.custom', defaultMessage: 'Custom'}),
|
||||
};
|
||||
|
||||
const handleClick = useCallback(
|
||||
preventDoubleTap(() => {
|
||||
handleItemClick(duration, '');
|
||||
}), [handleItemClick, duration]);
|
||||
|
||||
const handleCustomExpiresAtChange = useCallback((expiresAt: Moment) => {
|
||||
setExpiry(expiresAt.toISOString());
|
||||
handleItemClick(duration, expiresAt.toISOString());
|
||||
}, [handleItemClick, duration]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TouchableOpacity
|
||||
testID={`clear_after.menu_item.${duration}`}
|
||||
onPress={handleClick}
|
||||
>
|
||||
<View style={style.container}>
|
||||
<View style={style.textContainer}>
|
||||
<CustomStatusText
|
||||
text={expiryMenuItems[duration]}
|
||||
theme={theme}
|
||||
textStyle={{color: theme.centerChannelColor}}
|
||||
/>
|
||||
{isSelected && (
|
||||
<View style={style.rightPosition}>
|
||||
<CompassIcon
|
||||
name={'check'}
|
||||
size={24}
|
||||
style={style.button}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{showExpiryTime && expiry !== '' && (
|
||||
<View style={style.rightPosition}>
|
||||
<CustomStatusExpiry
|
||||
theme={theme}
|
||||
time={moment(expiry).toDate()}
|
||||
textStyles={style.customStatusExpiry}
|
||||
showTimeCompulsory={true}
|
||||
showToday={true}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{separator && <View style={style.divider}/>}
|
||||
</TouchableOpacity>
|
||||
{showDateTimePicker && (
|
||||
<DateTimePicker
|
||||
theme={theme}
|
||||
handleChange={handleCustomExpiresAtChange}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(ClearAfterMenuItem);
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: 10,
|
||||
},
|
||||
textContainer: {
|
||||
marginLeft: 5,
|
||||
marginBottom: 2,
|
||||
alignItems: 'center',
|
||||
width: '70%',
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
position: 'relative',
|
||||
},
|
||||
rightPosition: {
|
||||
position: 'absolute',
|
||||
right: 14,
|
||||
},
|
||||
divider: {
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
|
||||
height: 1,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
button: {
|
||||
borderRadius: 1000,
|
||||
color: theme.buttonBg,
|
||||
},
|
||||
customStatusExpiry: {
|
||||
color: theme.linkColor,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import ClearAfterModal from '@screens/custom_status_clear_after/clear_after_modal';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
describe('screens/clear_after_modal', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
initialDuration: CustomStatusDuration.DONT_CLEAR,
|
||||
handleClearAfterClick: jest.fn(),
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<ClearAfterModal {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
188
app/screens/custom_status_clear_after/clear_after_modal.tsx
Normal file
188
app/screens/custom_status_clear_after/clear_after_modal.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {SafeAreaView, View, StatusBar} from 'react-native';
|
||||
import React from 'react';
|
||||
import {intlShape, injectIntl} from 'react-intl';
|
||||
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scrollview';
|
||||
import {
|
||||
Navigation,
|
||||
NavigationButtonPressedEvent,
|
||||
NavigationComponent,
|
||||
NavigationComponentProps,
|
||||
Options,
|
||||
OptionsTopBarButton,
|
||||
} from 'react-native-navigation';
|
||||
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme';
|
||||
|
||||
import {mergeNavigationOptions, popTopScreen} from 'app/actions/navigation';
|
||||
import ClearAfterMenuItem from './clear_after_menu_item';
|
||||
interface Props extends NavigationComponentProps {
|
||||
intl: typeof intlShape;
|
||||
theme: Theme;
|
||||
handleClearAfterClick: (duration: CustomStatusDuration, expiresAt: string) => void;
|
||||
initialDuration: CustomStatusDuration;
|
||||
}
|
||||
|
||||
type State = {
|
||||
duration: CustomStatusDuration;
|
||||
expiresAt: string;
|
||||
showExpiryTime: boolean;
|
||||
}
|
||||
|
||||
const {DATE_AND_TIME} = CustomStatusDuration;
|
||||
class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||
rightButton: OptionsTopBarButton = {
|
||||
id: 'update-custom-status-clear-after',
|
||||
testID: 'clear_after.done.button',
|
||||
enabled: true,
|
||||
showAsAction: 'always',
|
||||
};
|
||||
|
||||
static options(): Options {
|
||||
return {
|
||||
topBar: {
|
||||
title: {
|
||||
alignment: 'center',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.rightButton.text = props.intl.formatMessage({
|
||||
id: 'mobile.custom_status.modal_confirm',
|
||||
defaultMessage: 'Done',
|
||||
});
|
||||
|
||||
this.rightButton.color = props.theme.sidebarHeaderTextColor;
|
||||
const options: Options = {
|
||||
topBar: {
|
||||
rightButtons: [this.rightButton],
|
||||
},
|
||||
};
|
||||
|
||||
mergeNavigationOptions(props.componentId, options);
|
||||
|
||||
this.state = {
|
||||
duration: props.initialDuration,
|
||||
expiresAt: '',
|
||||
showExpiryTime: false,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
Navigation.events().bindComponent(this);
|
||||
}
|
||||
|
||||
navigationButtonPressed({buttonId}: NavigationButtonPressedEvent) {
|
||||
switch (buttonId) {
|
||||
case 'update-custom-status-clear-after':
|
||||
this.onDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
onDone = () => {
|
||||
this.props.handleClearAfterClick(this.state.duration, this.state.expiresAt);
|
||||
popTopScreen();
|
||||
};
|
||||
|
||||
handleItemClick = (duration: CustomStatusDuration, expiresAt: string) =>
|
||||
this.setState({
|
||||
duration,
|
||||
expiresAt,
|
||||
showExpiryTime: duration === DATE_AND_TIME && expiresAt !== '',
|
||||
});
|
||||
|
||||
renderClearAfterMenu = () => {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const {duration} = this.state;
|
||||
|
||||
const clearAfterMenu = Object.values(CustomStatusDuration).map(
|
||||
(item, index, arr) => {
|
||||
if (index === arr.length - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ClearAfterMenuItem
|
||||
key={item}
|
||||
handleItemClick={this.handleItemClick}
|
||||
duration={item}
|
||||
theme={theme}
|
||||
separator={index !== arr.length - 2}
|
||||
isSelected={duration === item}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (clearAfterMenu.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View testID='clear_after.menu'>
|
||||
<View style={style.block}>{clearAfterMenu}</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const {duration, expiresAt, showExpiryTime} = this.state;
|
||||
return (
|
||||
<SafeAreaView
|
||||
testID='clear_after.screen'
|
||||
style={style.container}
|
||||
>
|
||||
<StatusBar/>
|
||||
<KeyboardAwareScrollView bounces={false}>
|
||||
<View style={style.scrollView}>
|
||||
{this.renderClearAfterMenu()}
|
||||
</View>
|
||||
<View style={style.block}>
|
||||
<ClearAfterMenuItem
|
||||
handleItemClick={this.handleItemClick}
|
||||
duration={DATE_AND_TIME}
|
||||
theme={theme}
|
||||
separator={false}
|
||||
isSelected={duration === DATE_AND_TIME && expiresAt === ''}
|
||||
showExpiryTime={showExpiryTime}
|
||||
showDateTimePicker={duration === DATE_AND_TIME}
|
||||
/>
|
||||
</View>
|
||||
</KeyboardAwareScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default injectIntl(ClearAfterModal);
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
paddingTop: 32,
|
||||
paddingBottom: 32,
|
||||
},
|
||||
block: {
|
||||
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderBottomWidth: 1,
|
||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {renderWithRedux} from 'test/testing_library';
|
||||
import DateTimeSelector from './date_time_selector';
|
||||
|
||||
describe('screens/date_time_selector', () => {
|
||||
const baseProps = {
|
||||
theme: Preferences.THEMES.default,
|
||||
handleChange: jest.fn(),
|
||||
};
|
||||
|
||||
it('should match snapshot', () => {
|
||||
const wrapper = renderWithRedux(
|
||||
<DateTimeSelector {...baseProps}/>,
|
||||
);
|
||||
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
125
app/screens/custom_status_clear_after/date_time_selector.tsx
Normal file
125
app/screens/custom_status_clear_after/date_time_selector.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment, {Moment} from 'moment-timezone';
|
||||
import React, {useState} from 'react';
|
||||
import {View, Button, Platform} from 'react-native';
|
||||
import {useSelector} from 'react-redux';
|
||||
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {getBool} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserTimezone} from '@mm-redux/selectors/entities/timezone';
|
||||
import {Theme} from '@mm-redux/types/preferences';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {getCurrentMomentForTimezone, getUtcOffsetForTimeZone} from '@utils/timezone';
|
||||
|
||||
type Props = {
|
||||
theme: Theme;
|
||||
handleChange: (currentDate: Moment) => void;
|
||||
}
|
||||
|
||||
type AndroidMode = 'date' | 'time';
|
||||
|
||||
const CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES = 30;
|
||||
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 DateTimeSelector = (props: Props) => {
|
||||
const {theme} = props;
|
||||
const styles = getStyleSheet(theme);
|
||||
const militaryTime = useSelector((state: GlobalState) => getBool(state, Preferences.CATEGORY_DISPLAY_SETTINGS, 'use_military_time'));
|
||||
const timezone = useSelector(getCurrentUserTimezone);
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
const timezoneOffSetInMinutes = timezone ? getUtcOffsetForTimeZone(timezone) : undefined;
|
||||
|
||||
const minimumDate = getRoundedTime(currentTime);
|
||||
|
||||
const [date, setDate] = useState<Moment>(minimumDate);
|
||||
const [mode, setMode] = useState<AndroidMode>('date');
|
||||
const [show, setShow] = useState<boolean>(false);
|
||||
|
||||
const onChange = (_: React.ChangeEvent<HTMLInputElement>, selectedDate: Date) => {
|
||||
const currentDate = selectedDate || date;
|
||||
setShow(Platform.OS === 'ios');
|
||||
if (moment(currentDate).isAfter(minimumDate)) {
|
||||
setDate(moment(currentDate));
|
||||
props.handleChange(moment(currentDate));
|
||||
}
|
||||
};
|
||||
|
||||
const showMode = (currentMode: AndroidMode) => {
|
||||
setShow(true);
|
||||
setMode(currentMode);
|
||||
};
|
||||
|
||||
const showDatepicker = () => {
|
||||
showMode('date');
|
||||
props.handleChange(moment(date));
|
||||
};
|
||||
|
||||
const showTimepicker = () => {
|
||||
showMode('time');
|
||||
props.handleChange(moment(date));
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button
|
||||
testID={'clear_after.menu_item.date_and_time.button.date'}
|
||||
onPress={showDatepicker}
|
||||
title='Select Date'
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
<Button
|
||||
testID={'clear_after.menu_item.date_and_time.button.time'}
|
||||
onPress={showTimepicker}
|
||||
title='Select Time'
|
||||
color={theme.buttonBg}
|
||||
/>
|
||||
</View>
|
||||
{show && (
|
||||
<DateTimePicker
|
||||
testID='clear_after.date_time_picker'
|
||||
value={date.toDate()}
|
||||
mode={mode}
|
||||
is24Hour={militaryTime}
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
onChange={onChange}
|
||||
textColor={theme.centerChannelColor}
|
||||
minimumDate={minimumDate.toDate()}
|
||||
minuteInterval={CUSTOM_STATUS_TIME_PICKER_INTERVALS_IN_MINUTES}
|
||||
timeZoneOffsetInMinutes={timezoneOffSetInMinutes}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||
return {
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: 10,
|
||||
backgroundColor: theme.centerChannelBg,
|
||||
},
|
||||
buttonContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-evenly',
|
||||
marginBottom: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
export default DateTimeSelector;
|
||||
@@ -63,6 +63,9 @@ Navigation.setLazyComponentRegistrator((screenName) => {
|
||||
case 'ChannelNotificationPreference':
|
||||
screen = require('@screens/channel_notification_preference').default;
|
||||
break;
|
||||
case 'ClearAfter':
|
||||
screen = require('@screens/custom_status_clear_after/clear_after_modal').default;
|
||||
break;
|
||||
case 'ClockDisplaySettings':
|
||||
screen = require('@screens/settings/clock_display').default;
|
||||
break;
|
||||
|
||||
@@ -478,6 +478,7 @@ exports[`user_profile should match snapshot with custom status 1`] = `
|
||||
}
|
||||
>
|
||||
Status
|
||||
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
@@ -490,6 +491,7 @@ exports[`user_profile should match snapshot with custom status 1`] = `
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
@@ -508,16 +510,44 @@ exports[`user_profile should match snapshot with custom status 1`] = `
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
In a meeting
|
||||
</Text>
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -694,6 +724,7 @@ exports[`user_profile should match snapshot with custom status and isMyUser true
|
||||
}
|
||||
>
|
||||
Status
|
||||
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
@@ -706,6 +737,7 @@ exports[`user_profile should match snapshot with custom status and isMyUser true
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
@@ -724,16 +756,44 @@ exports[`user_profile should match snapshot with custom status and isMyUser true
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
In a meeting
|
||||
</Text>
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
@@ -866,3 +926,291 @@ exports[`user_profile should match snapshot with custom status and isMyUser true
|
||||
</ScrollView>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
exports[`user_profile should match snapshot with custom status expiry 1`] = `
|
||||
<RNCSafeAreaView
|
||||
style={
|
||||
Object {
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="user_profile.screen"
|
||||
>
|
||||
<Connect(StatusBar) />
|
||||
<ScrollView
|
||||
contentContainerStyle={
|
||||
Object {
|
||||
"paddingBottom": 48,
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#ffffff",
|
||||
"flex": 1,
|
||||
}
|
||||
}
|
||||
testID="user_profile.scroll_view"
|
||||
>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"alignItems": "center",
|
||||
"justifyContent": "center",
|
||||
"padding": 25,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Connect(ProfilePicture)
|
||||
iconSize={104}
|
||||
size={153}
|
||||
statusBorderWidth={6}
|
||||
statusSize={36}
|
||||
testID="user_profile.profile_picture"
|
||||
userId="4hzdnk6mg7gepe7yze6m3domnc"
|
||||
/>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
"marginTop": 15,
|
||||
}
|
||||
}
|
||||
testID="user_profile.username"
|
||||
>
|
||||
@fred
|
||||
</Text>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#EBEBEC",
|
||||
"height": 1,
|
||||
"marginLeft": 16,
|
||||
"marginRight": 22,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"marginBottom": 25,
|
||||
"marginHorizontal": 15,
|
||||
}
|
||||
}
|
||||
>
|
||||
<View
|
||||
testID="user_profile.custom_status"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600",
|
||||
"marginBottom": 10,
|
||||
"marginTop": 25,
|
||||
"textTransform": "uppercase",
|
||||
}
|
||||
}
|
||||
>
|
||||
Status
|
||||
|
||||
<CustomStatusExpiry
|
||||
showPrefix={true}
|
||||
textStyles={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600",
|
||||
"textTransform": "uppercase",
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
time="2200-04-13T18:09:12.451Z"
|
||||
withinBrackets={true}
|
||||
/>
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"flexDirection": "row",
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"marginBottom": 3,
|
||||
"marginRight": 5,
|
||||
}
|
||||
}
|
||||
testID="custom_status.emoji.calendar"
|
||||
>
|
||||
<Connect(Emoji)
|
||||
emojiName="calendar"
|
||||
size={20}
|
||||
/>
|
||||
</Text>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"justifyContent": "center",
|
||||
"width": "80%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomStatusText
|
||||
text="In a meeting"
|
||||
textStyle={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
testID="user_profile.display_block"
|
||||
>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": undefined,
|
||||
"fontSize": 13,
|
||||
"fontWeight": "600",
|
||||
"marginBottom": 10,
|
||||
"marginTop": 25,
|
||||
"textTransform": "uppercase",
|
||||
}
|
||||
}
|
||||
testID="user_profile.display_block.nickname.label"
|
||||
>
|
||||
Nickname
|
||||
</Text>
|
||||
<Text
|
||||
style={
|
||||
Object {
|
||||
"color": "#3d3c40",
|
||||
"fontSize": 15,
|
||||
}
|
||||
}
|
||||
testID="user_profile.display_block.nickname.value"
|
||||
>
|
||||
nick
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={
|
||||
Object {
|
||||
"backgroundColor": "#EBEBEC",
|
||||
"height": 1,
|
||||
"marginLeft": 16,
|
||||
"marginRight": 22,
|
||||
}
|
||||
}
|
||||
/>
|
||||
<userProfileRow
|
||||
action={[Function]}
|
||||
defaultMessage="Send Message"
|
||||
icon="send"
|
||||
iconColor="rgba(0, 0, 0, 0.7)"
|
||||
iconSize={24}
|
||||
testID="user_profile.send_message.action"
|
||||
textColor="#000"
|
||||
textId="mobile.routes.user_profile.send_message"
|
||||
theme={
|
||||
Object {
|
||||
"awayIndicator": "#ffbc42",
|
||||
"buttonBg": "#166de0",
|
||||
"buttonColor": "#ffffff",
|
||||
"centerChannelBg": "#ffffff",
|
||||
"centerChannelColor": "#3d3c40",
|
||||
"codeTheme": "github",
|
||||
"dndIndicator": "#f74343",
|
||||
"errorTextColor": "#fd5960",
|
||||
"linkColor": "#2389d7",
|
||||
"mentionBg": "#ffffff",
|
||||
"mentionBj": "#ffffff",
|
||||
"mentionColor": "#145dbf",
|
||||
"mentionHighlightBg": "#ffe577",
|
||||
"mentionHighlightLink": "#166de0",
|
||||
"newMessageSeparator": "#ff8800",
|
||||
"onlineIndicator": "#06d6a0",
|
||||
"sidebarBg": "#145dbf",
|
||||
"sidebarHeaderBg": "#1153ab",
|
||||
"sidebarHeaderTextColor": "#ffffff",
|
||||
"sidebarText": "#ffffff",
|
||||
"sidebarTextActiveBorder": "#579eff",
|
||||
"sidebarTextActiveColor": "#ffffff",
|
||||
"sidebarTextHoverBg": "#4578bf",
|
||||
"sidebarUnreadText": "#ffffff",
|
||||
"type": "Mattermost",
|
||||
}
|
||||
}
|
||||
togglable={false}
|
||||
/>
|
||||
</ScrollView>
|
||||
</RNCSafeAreaView>
|
||||
`;
|
||||
|
||||
@@ -7,15 +7,15 @@ import {connect} from 'react-redux';
|
||||
import {setChannelDisplayName} from '@actions/views/channel';
|
||||
import {unsetCustomStatus} from '@actions/views/custom_status';
|
||||
import {makeDirectChannel} from '@actions/views/more_dms';
|
||||
import {loadBot} from '@mm-redux/actions/bots';
|
||||
import {getRemoteClusterInfo} from '@mm-redux/actions/remote_cluster';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {getBotAccounts} from '@mm-redux/selectors/entities/bots';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {getTeammateNameDisplaySetting, getTheme, getBool} from '@mm-redux/selectors/entities/preferences';
|
||||
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {loadBot} from '@mm-redux/actions/bots';
|
||||
import {getRemoteClusterInfo} from '@mm-redux/actions/remote_cluster';
|
||||
import {getBotAccounts} from '@mm-redux/selectors/entities/bots';
|
||||
import {getCurrentUserId} from '@mm-redux/selectors/entities/users';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled} from '@selectors/custom_status';
|
||||
import {makeGetCustomStatus, isCustomStatusEnabled, isCustomStatusExpired, isCustomStatusExpirySupported} from '@selectors/custom_status';
|
||||
|
||||
import UserProfile from './user_profile';
|
||||
|
||||
@@ -28,7 +28,8 @@ function makeMapStateToProps() {
|
||||
const enableTimezone = isTimezoneEnabled(state);
|
||||
const user = state.entities.users.profiles[ownProps.userId];
|
||||
|
||||
const customStatus = isCustomStatusEnabled(state) ? getCustomStatus(state, user?.id) : undefined;
|
||||
const customStatusEnabled = isCustomStatusEnabled(state);
|
||||
const customStatus = customStatusEnabled ? getCustomStatus(state, user?.id) : undefined;
|
||||
return {
|
||||
config,
|
||||
createChannelRequest,
|
||||
@@ -42,6 +43,8 @@ function makeMapStateToProps() {
|
||||
isMyUser: getCurrentUserId(state) === ownProps.userId,
|
||||
remoteClusterInfo: state.entities.remoteCluster.info[user?.remote_id],
|
||||
customStatus,
|
||||
isCustomStatusExpired: customStatusEnabled ? isCustomStatusExpired(state, customStatus) : true,
|
||||
isCustomStatusExpirySupported: customStatusEnabled ? isCustomStatusExpirySupported(state) : false,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
dismissAllModalsAndPopToRoot,
|
||||
} from '@actions/navigation';
|
||||
import Config from '@assets/config';
|
||||
import Emoji from '@components/emoji';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import ChannelIcon from '@components/channel_icon';
|
||||
import ClearButton from '@components/custom_status/clear_button';
|
||||
import CustomStatusExpiry from '@components/custom_status/custom_status_expiry';
|
||||
import CustomStatusText from '@components/custom_status/custom_status_text';
|
||||
import Emoji from '@components/emoji';
|
||||
import FormattedTime from '@components/formatted_time';
|
||||
import ProfilePicture from '@components/profile_picture';
|
||||
import FormattedText from '@components/formatted_text';
|
||||
@@ -60,6 +62,8 @@ export default class UserProfile extends PureComponent {
|
||||
isMyUser: PropTypes.bool.isRequired,
|
||||
remoteClusterInfo: PropTypes.object,
|
||||
customStatus: PropTypes.object,
|
||||
isCustomStatusExpired: PropTypes.bool.isRequired,
|
||||
isCustomStatusExpirySupported: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
@@ -232,20 +236,33 @@ export default class UserProfile extends PureComponent {
|
||||
|
||||
buildCustomStatusBlock = () => {
|
||||
const {formatMessage} = this.context.intl;
|
||||
const {customStatus, theme, isMyUser} = this.props;
|
||||
const {customStatus, theme, isMyUser, isCustomStatusExpired, isCustomStatusExpirySupported} = this.props;
|
||||
const style = createStyleSheet(theme);
|
||||
const isStatusSet = customStatus?.emoji;
|
||||
const isStatusSet = !isCustomStatusExpired && customStatus?.emoji;
|
||||
|
||||
if (!isStatusSet) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const label = formatMessage({id: 'user.settings.general.status', defaultMessage: 'Status'});
|
||||
|
||||
return (
|
||||
<View
|
||||
testID='user_profile.custom_status'
|
||||
>
|
||||
<Text style={style.header}>{label}</Text>
|
||||
<Text style={style.header}>
|
||||
{label}
|
||||
{' '}
|
||||
{Boolean(customStatus?.duration && isCustomStatusExpirySupported) && (
|
||||
<CustomStatusExpiry
|
||||
time={customStatus?.expires_at}
|
||||
theme={theme}
|
||||
textStyles={style.customStatusExpiry}
|
||||
showPrefix={true}
|
||||
withinBrackets={true}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
<View style={style.customStatus}>
|
||||
<Text
|
||||
style={style.iconContainer}
|
||||
@@ -257,9 +274,11 @@ export default class UserProfile extends PureComponent {
|
||||
/>
|
||||
</Text>
|
||||
<View style={style.customStatusTextContainer}>
|
||||
<Text style={style.text}>
|
||||
{customStatus.text}
|
||||
</Text>
|
||||
<CustomStatusText
|
||||
text={customStatus?.text}
|
||||
theme={theme}
|
||||
textStyle={style.text}
|
||||
/>
|
||||
</View>
|
||||
{isMyUser && (
|
||||
<View style={style.clearButton}>
|
||||
@@ -494,6 +513,7 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
marginBottom: 3,
|
||||
marginRight: 5,
|
||||
color: theme.centerChannelColor,
|
||||
},
|
||||
@@ -501,8 +521,14 @@ const createStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
customStatusTextContainer: {
|
||||
justifyContent: 'center',
|
||||
width: '80%',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
customStatusExpiry: {
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
textTransform: 'uppercase',
|
||||
color: changeOpacity(theme.centerChannelColor, 0.5),
|
||||
},
|
||||
clearButton: {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -5,6 +5,7 @@ import React from 'react';
|
||||
import * as NavigationActions from '@actions/navigation';
|
||||
import {BotTag, GuestTag} from '@components/tag';
|
||||
import Preferences from '@mm-redux/constants/preferences';
|
||||
import {CustomStatusDuration} from '@mm-redux/types/users';
|
||||
import {shallowWithIntl} from 'test/intl-test-helper';
|
||||
|
||||
import UserProfile from './user_profile.js';
|
||||
@@ -37,6 +38,8 @@ describe('user_profile', () => {
|
||||
isMilitaryTime: false,
|
||||
isMyUser: false,
|
||||
componentId: 'component-id',
|
||||
isCustomStatusExpired: false,
|
||||
isCustomStatusExpirySupported: false,
|
||||
};
|
||||
|
||||
const user = {
|
||||
@@ -52,6 +55,7 @@ describe('user_profile', () => {
|
||||
const customStatus = {
|
||||
emoji: 'calendar',
|
||||
text: 'In a meeting',
|
||||
duration: CustomStatusDuration.DONT_CLEAR,
|
||||
};
|
||||
|
||||
const customStatusProps = {
|
||||
@@ -94,6 +98,23 @@ describe('user_profile', () => {
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with custom status expiry', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<UserProfile
|
||||
{...customStatusProps}
|
||||
customStatus={{
|
||||
...customStatus,
|
||||
duration: CustomStatusDuration.DATE_AND_TIME,
|
||||
expires_at: '2200-04-13T18:09:12.451Z',
|
||||
}}
|
||||
isCustomStatusExpirySupported={true}
|
||||
/>,
|
||||
{context: {intl: {formatMessage: jest.fn()}}},
|
||||
);
|
||||
|
||||
expect(wrapper.getElement()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('should contain bot tag', () => {
|
||||
const botUser = {
|
||||
email: 'test@test.com',
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
import {createSelector} from 'reselect';
|
||||
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {CustomStatusDuration, UserCustomStatus} from '@mm-redux/types/users';
|
||||
|
||||
import {Preferences} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import {get} from '@mm-redux/selectors/entities/preferences';
|
||||
import {getCurrentUserTimezone} from '@mm-redux/selectors/entities/timezone';
|
||||
import {getCurrentUser, getUser} from '@mm-redux/selectors/entities/users';
|
||||
import {UserCustomStatus} from '@mm-redux/types/users';
|
||||
import {GlobalState} from '@mm-redux/types/store';
|
||||
import {isMinimumServerVersion} from '@mm-redux/utils/helpers';
|
||||
import {getCurrentMomentForTimezone} from '@utils/timezone';
|
||||
|
||||
export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => UserCustomStatus {
|
||||
return createSelector(
|
||||
@@ -25,6 +29,21 @@ export function makeGetCustomStatus(): (state: GlobalState, userID?: string) =>
|
||||
);
|
||||
}
|
||||
|
||||
export function isCustomStatusExpired(state: GlobalState, customStatus?: UserCustomStatus) {
|
||||
if (!customStatus) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (customStatus.duration === CustomStatusDuration.DONT_CLEAR || !customStatus.hasOwnProperty('duration')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expiryTime = moment(customStatus.expires_at);
|
||||
const timezone = getCurrentUserTimezone(state);
|
||||
const currentTime = getCurrentMomentForTimezone(timezone);
|
||||
return currentTime.isSameOrAfter(expiryTime);
|
||||
}
|
||||
|
||||
export const getRecentCustomStatuses = createSelector(
|
||||
(state: GlobalState) => get(state, Preferences.CATEGORY_CUSTOM_STATUS, Preferences.NAME_RECENT_CUSTOM_STATUSES),
|
||||
(value) => {
|
||||
@@ -41,3 +60,8 @@ export function isCustomStatusEnabled(state: GlobalState) {
|
||||
const serverVersion = state.entities.general.serverVersion;
|
||||
return config && config.EnableCustomUserStatuses === 'true' && isMinimumServerVersion(serverVersion, 5, 36);
|
||||
}
|
||||
|
||||
export function isCustomStatusExpirySupported(state: GlobalState) {
|
||||
const serverVersion = state.entities.general.serverVersion;
|
||||
return isMinimumServerVersion(serverVersion, 5, 37);
|
||||
}
|
||||
|
||||
@@ -16,3 +16,6 @@ export function getUtcOffsetForTimeZone(timezone) {
|
||||
return moment.tz(timezone).utcOffset();
|
||||
}
|
||||
|
||||
export function getCurrentMomentForTimezone(timezone) {
|
||||
return timezone ? moment.tz(timezone) : moment();
|
||||
}
|
||||
|
||||
@@ -129,6 +129,18 @@
|
||||
"create_comment.addComment": "Add a comment...",
|
||||
"create_post.deactivated": "You are viewing an archived channel with a deactivated user.",
|
||||
"create_post.write": "Write to {channelDisplayName}",
|
||||
"custom_status.expiry_dropdown.custom": "Custom",
|
||||
"custom_status.expiry_dropdown.date_and_time": "Date and Time",
|
||||
"custom_status.expiry_dropdown.dont_clear": "Don't clear",
|
||||
"custom_status.expiry_dropdown.four_hours": "4 hours",
|
||||
"custom_status.expiry_dropdown.one_hour": "1 hour",
|
||||
"custom_status.expiry_dropdown.thirty_minutes": "30 minutes",
|
||||
"custom_status.expiry_dropdown.this_week": "This week",
|
||||
"custom_status.expiry_dropdown.today": "Today",
|
||||
"custom_status.expiry_time.today": "Today",
|
||||
"custom_status.expiry_time.tomorrow": "Tomorrow",
|
||||
"custom_status.expiry.at": "at",
|
||||
"custom_status.expiry.until": "Until",
|
||||
"custom_status.failure_message": "Failed to update status. Try again",
|
||||
"custom_status.set_status": "Set a status",
|
||||
"custom_status.suggestions.in_a_meeting": "In a meeting",
|
||||
@@ -301,6 +313,7 @@
|
||||
"mobile.create_post.read_only": "This channel is read-only",
|
||||
"mobile.custom_list.no_results": "No Results",
|
||||
"mobile.custom_status.choose_emoji": "Choose an emoji",
|
||||
"mobile.custom_status.clear_after": "Clear After",
|
||||
"mobile.custom_status.modal_confirm": "Done",
|
||||
"mobile.display_settings.sidebar": "Sidebar",
|
||||
"mobile.display_settings.theme": "Theme",
|
||||
|
||||
40
detox/e2e/support/ui/component/date_time_picker.js
Normal file
40
detox/e2e/support/ui/component/date_time_picker.js
Normal file
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
class DateTimePicker {
|
||||
testID = {
|
||||
dateTimePicker: 'clear_after.date_time_picker',
|
||||
}
|
||||
|
||||
changeTimeAndroid = async (hour, minute) => {
|
||||
const keyboardIconButton = element(
|
||||
by.type('androidx.appcompat.widget.AppCompatImageButton'),
|
||||
);
|
||||
|
||||
await keyboardIconButton.tap();
|
||||
|
||||
const hourTextinput = element(
|
||||
by.type('androidx.appcompat.widget.AppCompatEditText'),
|
||||
).atIndex(0);
|
||||
|
||||
const minuteTextinput = element(
|
||||
by.type('androidx.appcompat.widget.AppCompatEditText'),
|
||||
).atIndex(1);
|
||||
|
||||
await hourTextinput.replaceText(hour);
|
||||
await minuteTextinput.replaceText(minute);
|
||||
}
|
||||
|
||||
tapCancelButtonAndroid = async () => {
|
||||
await element(by.text('Cancel')).tap();
|
||||
}
|
||||
|
||||
tapOkButtonAndroid = async () => {
|
||||
await element(by.text('OK')).tap();
|
||||
}
|
||||
|
||||
getDateTimePickerIOS = () => element(by.type('UIPickerView').withAncestor(by.id(this.testID.dateTimePicker)))
|
||||
}
|
||||
|
||||
const dateTimePicker = new DateTimePicker();
|
||||
export default dateTimePicker;
|
||||
@@ -5,6 +5,7 @@ import Alert from './alert';
|
||||
import Autocomplete from './autocomplete';
|
||||
import BottomSheet from './bottom_sheet';
|
||||
import CameraQuickAction from './camera_quick_action';
|
||||
import DateTimePicker from './date_time_picker';
|
||||
import EditChannelInfo from './edit_channel_info';
|
||||
import FileQuickAction from './file_quick_action';
|
||||
import ImageQuickAction from './image_quick_action';
|
||||
@@ -27,6 +28,7 @@ export {
|
||||
Autocomplete,
|
||||
BottomSheet,
|
||||
CameraQuickAction,
|
||||
DateTimePicker,
|
||||
EditChannelInfo,
|
||||
FileQuickAction,
|
||||
ImageQuickAction,
|
||||
|
||||
@@ -8,10 +8,12 @@ class CustomStatusScreen {
|
||||
customStatusScreen: 'custom_status.screen',
|
||||
input: 'custom_status.input',
|
||||
selectedEmojiPrefix: 'custom_status.emoji.',
|
||||
selectedDurationPrefix: 'custom_status.duration.',
|
||||
inputClearButton: 'custom_status.input.clear.button',
|
||||
doneButton: 'custom_status.done.button',
|
||||
suggestionPrefix: 'custom_status_suggestion.',
|
||||
suggestionClearButton: 'custom_status_suggestion.clear.button',
|
||||
clearAfterAction: 'custom_status.clear_after.action',
|
||||
}
|
||||
|
||||
customStatusScreen = element(by.id(this.testID.customStatusScreen));
|
||||
@@ -24,6 +26,11 @@ class CustomStatusScreen {
|
||||
return element(by.id(emojiTestID));
|
||||
}
|
||||
|
||||
getCustomStatusSelectedDuration = (duration) => {
|
||||
const durationTestID = `${this.testID.selectedDurationPrefix}${duration}`;
|
||||
return element(by.id(durationTestID));
|
||||
}
|
||||
|
||||
getCustomStatusSuggestion = (text) => {
|
||||
const suggestionID = `${this.testID.suggestionPrefix}${text}`;
|
||||
return element(by.id(suggestionID));
|
||||
@@ -54,6 +61,10 @@ class CustomStatusScreen {
|
||||
await expect(this.getCustomStatusSelectedEmoji(emoji)).toBeVisible();
|
||||
}
|
||||
|
||||
openClearAfterModal = async () => {
|
||||
await element(by.id(this.testID.clearAfterAction)).tap();
|
||||
}
|
||||
|
||||
close = async () => {
|
||||
await this.doneButton.tap();
|
||||
return expect(this.customStatusScreen).not.toBeVisible();
|
||||
|
||||
86
detox/e2e/support/ui/screen/custom_status_clear_after.js
Normal file
86
detox/e2e/support/ui/screen/custom_status_clear_after.js
Normal file
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {CustomStatusScreen} from '@support/ui/screen';
|
||||
|
||||
class ClearAfterScreen {
|
||||
testID = {
|
||||
clearAfterScreen: 'clear_after.screen',
|
||||
doneButton: 'clear_after.done.button',
|
||||
menuItemPrefix: 'clear_after.menu_item.',
|
||||
selectDateButton: 'clear_after.menu_item.date_and_time.button.date',
|
||||
selectTimeButton: 'clear_after.menu_item.date_and_time.button.time',
|
||||
}
|
||||
|
||||
clearAfterScreen = element(by.id(this.testID.clearAfterScreen));
|
||||
doneButton = element(by.id(this.testID.doneButton));
|
||||
selectDateButton = element(by.id(this.testID.selectDateButton));
|
||||
selectTimeButton = element(by.id(this.testID.selectTimeButton));
|
||||
|
||||
getClearAfterMenuItem = (duration) => {
|
||||
const menuItemID = `${this.testID.menuItemPrefix}${duration}`;
|
||||
return element(by.id(menuItemID));
|
||||
}
|
||||
|
||||
toBeVisible = async () => {
|
||||
await expect(this.clearAfterScreen).toBeVisible();
|
||||
|
||||
return this.clearAfterScreen;
|
||||
}
|
||||
|
||||
open = async () => {
|
||||
// # Open clear after screen
|
||||
await CustomStatusScreen.openClearAfterModal();
|
||||
|
||||
return this.toBeVisible();
|
||||
}
|
||||
|
||||
tapSuggestion = async (duration) => {
|
||||
await this.getClearAfterMenuItem(duration).tap();
|
||||
}
|
||||
|
||||
openDatePicker = async () => {
|
||||
await this.selectDateButton.tap();
|
||||
}
|
||||
|
||||
openTimePicker = async () => {
|
||||
await this.selectTimeButton.tap();
|
||||
}
|
||||
|
||||
close = async () => {
|
||||
await this.doneButton.tap();
|
||||
return expect(this.clearAfterScreen).not.toBeVisible();
|
||||
}
|
||||
|
||||
getExpiryText = (minutes) => {
|
||||
const currentMomentTime = moment();
|
||||
const expiryMomentTime = currentMomentTime.clone().add(minutes, 'm');
|
||||
const tomorrowEndTime = currentMomentTime.clone().add(1, 'day').endOf('day');
|
||||
const todayEndTime = currentMomentTime.clone().endOf('day');
|
||||
|
||||
let isTomorrow = false;
|
||||
let isToday = false;
|
||||
|
||||
if (expiryMomentTime.isSame(todayEndTime)) {
|
||||
isToday = true;
|
||||
}
|
||||
if (expiryMomentTime.isAfter(todayEndTime) && expiryMomentTime.isSameOrBefore(tomorrowEndTime)) {
|
||||
isTomorrow = true;
|
||||
}
|
||||
|
||||
const showTime = expiryMomentTime.format('h:mm A');
|
||||
const showTomorrow = isTomorrow ? 'Tomorrow at ' : '';
|
||||
const showToday = isToday ? 'Today' : '';
|
||||
|
||||
let expiryText = '';
|
||||
expiryText += showToday;
|
||||
expiryText += showTomorrow;
|
||||
expiryText += showTime;
|
||||
return expiryText;
|
||||
}
|
||||
}
|
||||
|
||||
const clearAfterScreen = new ClearAfterScreen();
|
||||
export default clearAfterScreen;
|
||||
@@ -9,6 +9,7 @@ import ChannelAddMembersScreen from './channel_add_members';
|
||||
import ChannelMembersScreen from './channel_members';
|
||||
import ChannelNotificationPreferenceScreen from './channel_notification_preference';
|
||||
import ChannelScreen from './channel';
|
||||
import ClearAfterScreen from './custom_status_clear_after';
|
||||
import ClockDisplaySettingsScreen from './clock_display_settings';
|
||||
import CreateChannelScreen from './create_channel';
|
||||
import CustomStatusScreen from './custom_status';
|
||||
@@ -46,6 +47,7 @@ export {
|
||||
ChannelMembersScreen,
|
||||
ChannelNotificationPreferenceScreen,
|
||||
ChannelScreen,
|
||||
ClearAfterScreen,
|
||||
ClockDisplaySettingsScreen,
|
||||
CreateChannelScreen,
|
||||
CustomStatusScreen,
|
||||
|
||||
215
detox/e2e/test/custom_statuses/custom_status_expiry.e2e.js
Normal file
215
detox/e2e/test/custom_statuses/custom_status_expiry.e2e.js
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import moment from 'moment-timezone';
|
||||
|
||||
import {
|
||||
Setup,
|
||||
System,
|
||||
} from '@support/server_api';
|
||||
import {DateTimePicker} from '@support/ui/component';
|
||||
import {
|
||||
ChannelInfoScreen,
|
||||
ChannelScreen,
|
||||
CustomStatusScreen,
|
||||
MoreDirectMessagesScreen,
|
||||
UserProfileScreen,
|
||||
ClearAfterScreen,
|
||||
} from '@support/ui/screen';
|
||||
import {wait, timeouts, isAndroid} from '@support/utils';
|
||||
|
||||
describe('Custom status expiry', () => {
|
||||
const {
|
||||
closeSettingsSidebar,
|
||||
openSettingsSidebar,
|
||||
} = ChannelScreen;
|
||||
const {
|
||||
getCustomStatusSuggestion,
|
||||
tapSuggestion,
|
||||
getCustomStatusSelectedDuration,
|
||||
} = CustomStatusScreen;
|
||||
const defaultCustomStatuses = ['In a meeting', 'Out for lunch', 'Out sick', 'Working from home', 'On a vacation'];
|
||||
const defaultStatus = {
|
||||
emoji: 'hamburger',
|
||||
text: 'Out for lunch',
|
||||
duration: 'thirty_minutes',
|
||||
};
|
||||
let testUser;
|
||||
|
||||
beforeAll(async () => {
|
||||
await System.apiUpdateConfig({TeamSettings: {EnableCustomUserStatuses: true}});
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
const {user} = await Setup.apiInit();
|
||||
testUser = user;
|
||||
|
||||
// # Open channel screen
|
||||
await ChannelScreen.open(user);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await ChannelScreen.logout();
|
||||
});
|
||||
|
||||
xit('MM-T4090 RN apps: Custom Status Expiry (mobile)', async () => {// enable only when running locally
|
||||
const expiryText = '(Until ' + ClearAfterScreen.getExpiryText(30) + ')';
|
||||
|
||||
// # Open custom status screen
|
||||
await openSettingsSidebar();
|
||||
await CustomStatusScreen.open();
|
||||
|
||||
// * Check if all the default suggestions are visible
|
||||
const isSuggestionPresentPromiseArray = [];
|
||||
defaultCustomStatuses.map(async (text) => {
|
||||
isSuggestionPresentPromiseArray.push(expect(getCustomStatusSuggestion(text)).toBeVisible());
|
||||
});
|
||||
await Promise.all(isSuggestionPresentPromiseArray);
|
||||
|
||||
// * Tap a suggestion and check if it is selected
|
||||
await tapSuggestion(defaultStatus);
|
||||
await expect(getCustomStatusSelectedDuration(defaultStatus.duration)).toBeVisible();
|
||||
|
||||
// # Tap on Done button and check if the modal closes
|
||||
await CustomStatusScreen.close();
|
||||
|
||||
// * Check if the selected emoji and text are visible in the sidebar
|
||||
await openSettingsSidebar();
|
||||
await expect(element(by.text(defaultStatus.text))).toBeVisible();
|
||||
await expect(element(by.id(`custom_status.emoji.${defaultStatus.emoji}`))).toBeVisible();
|
||||
await expect(element(by.text(expiryText))).toBeVisible();
|
||||
|
||||
// # Wait for status to get cleared
|
||||
await wait(timeouts.ONE_MIN * 31);
|
||||
await expect(element(by.text(expiryText))).toBeNotVisible();
|
||||
await closeSettingsSidebar();
|
||||
}, timeouts.ONE_MIN * 35);
|
||||
|
||||
it('MM-T4091 RN apps: Custom Expiry Visibility (mobile)', async () => {
|
||||
const message = 'Hello';
|
||||
const expiryText = ClearAfterScreen.getExpiryText(30);
|
||||
|
||||
// # Open custom status screen
|
||||
await openSettingsSidebar();
|
||||
await CustomStatusScreen.open();
|
||||
|
||||
// * Check if all the default suggestions are visible
|
||||
const isSuggestionPresentPromiseArray = [];
|
||||
defaultCustomStatuses.map(async (text) => {
|
||||
isSuggestionPresentPromiseArray.push(expect(getCustomStatusSuggestion(text)).toBeVisible());
|
||||
});
|
||||
await Promise.all(isSuggestionPresentPromiseArray);
|
||||
|
||||
// * Tap a suggestion and check if it is selected
|
||||
await tapSuggestion(defaultStatus);
|
||||
await expect(getCustomStatusSelectedDuration(defaultStatus.duration)).toBeVisible();
|
||||
|
||||
// # Tap on Done button and check if the modal closes
|
||||
await CustomStatusScreen.close();
|
||||
|
||||
// * Check if the selected emoji, text and expiry time are visible in the sidebar
|
||||
await openSettingsSidebar();
|
||||
await expect(element(by.text(defaultStatus.text))).toBeVisible();
|
||||
await expect(element(by.id(`custom_status.emoji.${defaultStatus.emoji}`))).toBeVisible();
|
||||
await expect(element(by.text('(Until ' + expiryText + ')'))).toBeVisible();
|
||||
|
||||
// # Close settings sidebar
|
||||
await closeSettingsSidebar();
|
||||
|
||||
// # Post a message and check if custom status and expiry time is present in the user popover
|
||||
await ChannelScreen.postMessage(message);
|
||||
|
||||
// # Open user profile screen
|
||||
await openSettingsSidebar();
|
||||
await UserProfileScreen.open();
|
||||
await UserProfileScreen.toBeVisible();
|
||||
|
||||
// * Check if custom status is present in the user profile screen and close it
|
||||
await expect(element(by.id(`custom_status.emoji.${defaultStatus.emoji}`)).atIndex(0)).toExist();
|
||||
await expect(element(by.text(defaultStatus.text).withAncestor(by.id(UserProfileScreen.testID.customStatus)))).toBeVisible();
|
||||
await expect(element(by.text('STATUS (UNTIL ' + expiryText + ')'))).toBeVisible();
|
||||
|
||||
await UserProfileScreen.close();
|
||||
|
||||
// # Open the main sidebar and click on more direct messages button
|
||||
await ChannelScreen.openMainSidebar();
|
||||
await MoreDirectMessagesScreen.open();
|
||||
|
||||
// # Type the logged in user's username and tap it to open the DM
|
||||
await MoreDirectMessagesScreen.searchInput.typeText(testUser.username);
|
||||
await MoreDirectMessagesScreen.getUserAtIndex(0).tap();
|
||||
|
||||
// # Open the channel info screen
|
||||
await ChannelInfoScreen.open();
|
||||
|
||||
// * Check if the custom status is present in the channel info screen and then close the screen
|
||||
await expect(element(by.id(`custom_status.emoji.${defaultStatus.emoji}`)).atIndex(0)).toExist();
|
||||
await expect(element(by.text(defaultStatus.text).withAncestor(by.id(ChannelInfoScreen.testID.headerCustomStatus)))).toBeVisible();
|
||||
await expect(element(by.text('Until ' + expiryText))).toBeVisible();
|
||||
|
||||
await ChannelInfoScreen.close();
|
||||
});
|
||||
|
||||
it("MM-T4092 RN apps: Custom Status Expiry - Editing 'Clear After' Time (mobile)", async () => {
|
||||
// # Open custom status screen
|
||||
await openSettingsSidebar();
|
||||
await CustomStatusScreen.open();
|
||||
|
||||
// * Check if all the default suggestions are visible
|
||||
const isSuggestionPresentPromiseArray = [];
|
||||
defaultCustomStatuses.map(async (text) => {
|
||||
isSuggestionPresentPromiseArray.push(expect(getCustomStatusSuggestion(text)).toBeVisible());
|
||||
});
|
||||
await Promise.all(isSuggestionPresentPromiseArray);
|
||||
|
||||
// * Tap a suggestion and check if it is selected
|
||||
await tapSuggestion(defaultStatus);
|
||||
await expect(getCustomStatusSelectedDuration(defaultStatus.duration)).toBeVisible();
|
||||
|
||||
// # Click on the Clear After option and check if the modal opens
|
||||
await ClearAfterScreen.open();
|
||||
|
||||
// # Select a different expiry time and check if it is shown in Clear After
|
||||
await ClearAfterScreen.tapSuggestion('four_hours');
|
||||
await ClearAfterScreen.close();
|
||||
|
||||
// Check if selected duration is shown in clear after
|
||||
await expect(getCustomStatusSelectedDuration('four_hours')).toBeVisible();
|
||||
|
||||
// * Open Clear After modal
|
||||
await ClearAfterScreen.open();
|
||||
|
||||
// * Tap 'Custom' and check if it is selected
|
||||
await element(by.text('Custom')).tap();
|
||||
|
||||
// * Check if select date and select time buttons are visible
|
||||
expect(element(by.id(ClearAfterScreen.testID.selectDateButton))).toBeVisible();
|
||||
expect(element(by.id(ClearAfterScreen.testID.selectTimeButton))).toBeVisible();
|
||||
|
||||
const am_pm = moment().format('A').toString();
|
||||
|
||||
// * Select some time in future in date time picker
|
||||
await ClearAfterScreen.openTimePicker();
|
||||
|
||||
if (isAndroid()) {
|
||||
await DateTimePicker.changeTimeAndroid('11', '30');
|
||||
await DateTimePicker.tapOkButtonAndroid();
|
||||
} else {
|
||||
const timePicker = DateTimePicker.getDateTimePickerIOS();
|
||||
await timePicker.setColumnToValue(0, '11');
|
||||
await timePicker.setColumnToValue(1, '30');
|
||||
}
|
||||
|
||||
await ClearAfterScreen.close();
|
||||
await CustomStatusScreen.close();
|
||||
|
||||
// * Check if the selected emoji and text are visible in the sidebar
|
||||
await openSettingsSidebar();
|
||||
await expect(element(by.text(defaultStatus.text))).toBeVisible();
|
||||
await expect(element(by.id(`custom_status.emoji.${defaultStatus.emoji}`))).toBeVisible();
|
||||
await expect(element(by.text('(Until 11:30 ' + am_pm + ')'))).toBeVisible();
|
||||
|
||||
// # Close settings sidebar
|
||||
await closeSettingsSidebar();
|
||||
});
|
||||
});
|
||||
@@ -34,4 +34,4 @@ target 'Mattermost' do
|
||||
puts 'Patching XCDYouTube so it can playback videos'
|
||||
%x(patch Pods/XCDYouTubeKit/XCDYouTubeKit/XCDYouTubeVideoOperation.m < patches/XCDYouTubeVideoOperation.patch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
17
package-lock.json
generated
17
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@react-native-community/async-storage": "1.12.1",
|
||||
"@react-native-community/cameraroll": "4.0.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "3.5.2",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-cookies/cookies": "6.0.8",
|
||||
@@ -4545,6 +4546,14 @@
|
||||
"react-native": ">=0.57.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/datetimepicker": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-3.5.2.tgz",
|
||||
"integrity": "sha512-TWRuAtr/DnrEcRewqvXMLea2oB+YF+SbtuYLHguALLxNJQLl/RFB7aTNZeF+OoH75zKFqtXECXV1/uxQUpA+sg==",
|
||||
"dependencies": {
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-community/eslint-config": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-config/-/eslint-config-3.0.0.tgz",
|
||||
@@ -42172,6 +42181,14 @@
|
||||
"integrity": "sha512-AHAmrkLEH5UtPaDiRqoULERHh3oNv7Dgs0bTC0hO5Z2GdNokAMPT5w8ci8aMcRemcwbtdHjxChgtjbeA38GBdA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-community/datetimepicker": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/datetimepicker/-/datetimepicker-3.5.2.tgz",
|
||||
"integrity": "sha512-TWRuAtr/DnrEcRewqvXMLea2oB+YF+SbtuYLHguALLxNJQLl/RFB7aTNZeF+OoH75zKFqtXECXV1/uxQUpA+sg==",
|
||||
"requires": {
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"@react-native-community/eslint-config": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-community/eslint-config/-/eslint-config-3.0.0.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"@react-native-community/async-storage": "1.12.1",
|
||||
"@react-native-community/cameraroll": "4.0.4",
|
||||
"@react-native-community/clipboard": "1.5.1",
|
||||
"@react-native-community/datetimepicker": "3.5.2",
|
||||
"@react-native-community/masked-view": "0.1.11",
|
||||
"@react-native-community/netinfo": "6.0.0",
|
||||
"@react-native-cookies/cookies": "6.0.8",
|
||||
|
||||
Reference in New Issue
Block a user