Skip to content

Commit

Permalink
Merge pull request #23267 from margelo/feat/##23130-status-page-settings
Browse files Browse the repository at this point in the history
[Feat #23130] Add custom status settings to profile
  • Loading branch information
stitesExpensify authored Aug 9, 2023
2 parents b2d147b + b36faec commit 457c6df
Show file tree
Hide file tree
Showing 19 changed files with 718 additions and 8 deletions.
125 changes: 125 additions & 0 deletions assets/images/money-stack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ const CONST = {
TASKS: 'tasks',
THREADS: 'threads',
SCAN_RECEIPTS: 'scanReceipts',
CUSTOM_STATUS: 'customStatus',
DISTANCE_REQUESTS: 'distanceRequests',
},
BUTTON_STATES: {
Expand Down Expand Up @@ -2517,6 +2518,14 @@ const CONST = {
TRANSLATION_KEYS: {
ATTACHMENT: 'common.attachment',
},
CUSTOM_STATUS_TYPES: {
NEVER: 'never',
THIRTY_MINUTES: 'thirtyMinutes',
ONE_HOUR: 'oneHour',
AFTER_TODAY: 'afterToday',
AFTER_WEEK: 'afterWeek',
CUSTOM: 'custom',
},
TAB: {
RECEIPT_TAB_ID: 'ReceiptTab',
MANUAL: 'manual',
Expand Down
6 changes: 6 additions & 0 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ export default {
// Has information about the network status (offline/online)
NETWORK: 'network',

// draft status
CUSTOM_STATUS_DRAFT: 'customStatusDraft',

// Contains all the personalDetails the user has access to, keyed by accountID
PERSONAL_DETAILS_LIST: 'personalDetailsList',

Expand Down Expand Up @@ -219,6 +222,9 @@ export default {
MONEY_REQUEST_DESCRIPTION_FORM: 'moneyRequestDescriptionForm',
NEW_CONTACT_METHOD_FORM: 'newContactMethodForm',
PAYPAL_FORM: 'payPalForm',
SETTINGS_STATUS_SET_FORM: 'settingsStatusSetForm',
SETTINGS_STATUS_CLEAR_AFTER_FORM: 'settingsStatusClearAfterForm',
SETTINGS_STATUS_SET_CLEAR_AFTER_FORM: 'settingsStatusSetClearAfterForm',
},

// Whether we should show the compose input or not
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const IOU_SEND = 'send/new';
const NEW_TASK = 'new/task';
const SETTINGS_PERSONAL_DETAILS = 'settings/profile/personal-details';
const SETTINGS_CONTACT_METHODS = 'settings/profile/contact-methods';
const SETTINGS_STATUS = 'settings/profile/status';
const SETTINGS_STATUS_SET = 'settings/profile/status/set';

export default {
BANK_ACCOUNT: 'bank-account',
Expand Down Expand Up @@ -58,6 +60,8 @@ export default {
SETTINGS_2FA_CODES: 'settings/security/two-factor-auth/codes',
SETTINGS_2FA_VERIFY: 'settings/security/two-factor-auth/verify',
SETTINGS_2FA_SUCCESS: 'settings/security/two-factor-auth/success',
SETTINGS_STATUS,
SETTINGS_STATUS_SET,
NEW_GROUP: 'new/group',
NEW_CHAT: 'new/chat',
NEW_TASK,
Expand Down
13 changes: 7 additions & 6 deletions src/components/EmojiPicker/EmojiPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@ const EmojiPicker = forwardRef((props, ref) => {
onModalHide.current = onModalHideValue;
onEmojiSelected.current = onEmojiSelectedValue;
emojiPopoverAnchor.current = emojiPopoverAnchorValue;

if (emojiPopoverAnchor.current) {
if (emojiPopoverAnchor.current && emojiPopoverAnchor.current.blur) {
// Drop focus to avoid blue focus ring.
emojiPopoverAnchor.current.blur();
}

calculateAnchorPosition(emojiPopoverAnchor.current).then((value) => {
const anchorOriginValue = anchorOrigin || DEFAULT_ANCHOR_ORIGIN;

calculateAnchorPosition(emojiPopoverAnchor.current, anchorOriginValue).then((value) => {
onWillShow();
setIsEmojiPickerVisible(true);
setEmojiPopoverAnchorPosition(value);
setEmojiPopoverAnchorOrigin(anchorOrigin || DEFAULT_ANCHOR_ORIGIN);
setEmojiPopoverAnchorOrigin(anchorOriginValue);
setReportAction(reportActionValue);
});
};
Expand Down Expand Up @@ -130,14 +131,14 @@ const EmojiPicker = forwardRef((props, ref) => {
}
return;
}
calculateAnchorPosition(emojiPopoverAnchor.current).then((value) => {
calculateAnchorPosition(emojiPopoverAnchor.current, emojiPopoverAnchorOrigin).then((value) => {
setEmojiPopoverAnchorPosition(value);
});
});
return () => {
emojiPopoverDimensionListener.remove();
};
}, [isEmojiPickerVisible, props.isSmallScreenWidth]);
}, [isEmojiPickerVisible, props.isSmallScreenWidth, emojiPopoverAnchorOrigin]);

// There is no way to disable animations, and they are really laggy, because there are so many
// emojis. The best alternative is to set it to 1ms so it just "pops" in and out
Expand Down
81 changes: 81 additions & 0 deletions src/components/EmojiPicker/EmojiPickerButtonDropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, {useRef, useEffect} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import styles from '../../styles/styles';
import CONST from '../../CONST';
import * as StyleUtils from '../../styles/StyleUtils';
import getButtonState from '../../libs/getButtonState';
import * as Expensicons from '../Icon/Expensicons';
import Tooltip from '../Tooltip';
import Text from '../Text';
import Icon from '../Icon';
import withLocalize, {withLocalizePropTypes} from '../withLocalize';
import * as EmojiPickerAction from '../../libs/actions/EmojiPickerAction';
import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback';

const propTypes = {
/** Flag to disable the emoji picker button */
isDisabled: PropTypes.bool,

...withLocalizePropTypes,
};

const defaultProps = {
isDisabled: false,
};

function EmojiPickerButtonDropdown(props) {
const emojiPopoverAnchor = useRef(null);
useEffect(() => EmojiPickerAction.resetEmojiPopoverAnchor, []);

const onPress = () =>
EmojiPickerAction.showEmojiPicker(props.onModalHide, (emoji) => props.onInputChange(emoji), emojiPopoverAnchor.current, {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
shiftVertical: 4,
});

return (
<Tooltip text={props.translate('reportActionCompose.emoji')}>
<PressableWithoutFeedback
ref={emojiPopoverAnchor}
style={styles.emojiPickerButtonDropdown}
disabled={props.isDisabled}
onPress={onPress}
nativeID="emojiDropdownButton"
accessibilityLabel="statusEmoji"
accessibilityRole="text"
>
{({hovered, pressed}) => (
<View style={styles.emojiPickerButtonDropdownContainer}>
<Text
style={styles.emojiPickerButtonDropdownIcon}
numberOfLines={1}
>
{props.value}
</Text>
<View style={[styles.popoverMenuIcon, styles.pointerEventsAuto, props.disabled && styles.cursorDisabled, styles.rotate90]}>
<Icon
src={Expensicons.ArrowRight}
fill={StyleUtils.getIconFillColor(getButtonState(hovered, pressed))}
/>
</View>
</View>
)}
</PressableWithoutFeedback>
</Tooltip>
);
}

EmojiPickerButtonDropdown.propTypes = propTypes;
EmojiPickerButtonDropdown.defaultProps = defaultProps;
EmojiPickerButtonDropdown.displayName = 'EmojiPickerButtonDropdown';
export default withLocalize(
React.forwardRef((props, ref) => (
<EmojiPickerButtonDropdown
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
forwardedRef={ref}
/>
)),
);
92 changes: 92 additions & 0 deletions src/components/StaticHeaderPageLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import _ from 'underscore';
import React, {useMemo} from 'react';
import PropTypes from 'prop-types';
import {ScrollView, View} from 'react-native';
import headerWithBackButtonPropTypes from './HeaderWithBackButton/headerWithBackButtonPropTypes';
import HeaderWithBackButton from './HeaderWithBackButton';
import ScreenWrapper from './ScreenWrapper';
import styles from '../styles/styles';
import themeColors from '../styles/themes/default';
import * as StyleUtils from '../styles/StyleUtils';
import useWindowDimensions from '../hooks/useWindowDimensions';
import FixedFooter from './FixedFooter';

const propTypes = {
...headerWithBackButtonPropTypes,

/** Children to display in the lower half of the page (below the header section w/ an animation) */
children: PropTypes.node.isRequired,

/** The background color to apply in the upper half of the screen. */
backgroundColor: PropTypes.string,

/** A fixed footer to display at the bottom of the page. */
footer: PropTypes.node,
};

const defaultProps = {
backgroundColor: themeColors.appBG,
footer: null,
};

function StaticHeaderPageLayout({backgroundColor, children, image: Image, footer, imageContainerStyle, style, ...propsToPassToHeader}) {
const {windowHeight} = useWindowDimensions();

const {titleColor, iconFill} = useMemo(() => {
const isColorfulBackground = backgroundColor !== themeColors.appBG;
return {
titleColor: isColorfulBackground ? themeColors.textColorfulBackground : undefined,
iconFill: isColorfulBackground ? themeColors.iconColorfulBackground : undefined,
};
}, [backgroundColor]);

return (
<ScreenWrapper
style={[StyleUtils.getBackgroundColorStyle(backgroundColor)]}
shouldEnablePickerAvoiding={false}
includeSafeAreaPaddingBottom={false}
offlineIndicatorStyle={[StyleUtils.getBackgroundColorStyle(themeColors.appBG)]}
>
{({safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton
// eslint-disable-next-line react/jsx-props-no-spreading
{...propsToPassToHeader}
titleColor={titleColor}
iconFill={iconFill}
/>
<View style={[styles.flex1, StyleUtils.getBackgroundColorStyle(themeColors.appBG)]}>
<ScrollView
contentContainerStyle={[safeAreaPaddingBottomStyle, style]}
showsVerticalScrollIndicator={false}
>
<View style={styles.overscrollSpacer(backgroundColor, windowHeight)} />
<View
style={[
styles.alignItemsCenter,
styles.justifyContentEnd,
StyleUtils.getBackgroundColorStyle(backgroundColor),
imageContainerStyle,
styles.staticHeaderImage,
]}
>
<Image
pointerEvents="none"
style={styles.staticHeaderImage}
/>
</View>
<View style={styles.pt5}>{children}</View>
</ScrollView>
{!_.isNull(footer) && <FixedFooter>{footer}</FixedFooter>}
</View>
</>
)}
</ScreenWrapper>
);
}

StaticHeaderPageLayout.propTypes = propTypes;
StaticHeaderPageLayout.defaultProps = defaultProps;
StaticHeaderPageLayout.displayName = 'StaticHeaderPageLayout';

export default StaticHeaderPageLayout;
9 changes: 9 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,15 @@ export default {
setPasswordLinkInvalid: 'This set password link is invalid or has expired. A new one is waiting for you in your email inbox!',
validateAccount: 'Verify account',
},
statusPage: {
status: 'Status',
setStatusTitle: 'Set your status',
statusExplanation: "Add an emoji to give your colleagues and friends an easy way to know what's going on. You can optionally add a message too!",
today: 'Today',
clearStatus: 'Clear status',
save: 'Save',
message: 'Message',
},
stepCounter: ({step, total, text}) => {
let result = `Step ${step}`;

Expand Down
9 changes: 9 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,15 @@ export default {
setPasswordLinkInvalid: 'El enlace para configurar tu contraseña ha expirado. Te hemos enviado un nuevo enlace a tu correo.',
validateAccount: 'Verificar cuenta',
},
statusPage: {
status: 'Estado',
setStatusTitle: 'Establece tu estado',
statusExplanation: 'Agrega un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes agregar un mensaje opcionalmente!',
today: 'Hoy',
clearStatus: 'Borrar estado',
save: 'Guardar',
message: 'Mensaje',
},
stepCounter: ({step, total, text}) => {
let result = `Paso ${step}`;

Expand Down
14 changes: 14 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,20 @@ const SettingsModalStackNavigator = createModalStackNavigator([
},
name: 'Settings_Add_Bank_Account',
},
{
getComponent: () => {
const SettingsStatus = require('../../../pages/settings/Profile/CustomStatus/StatusPage').default;
return SettingsStatus;
},
name: 'Settings_Status',
},
{
getComponent: () => {
const SettingsStatusSet = require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default;
return SettingsStatusSet;
},
name: 'Settings_Status_Set',
},
{
getComponent: () => {
const WorkspaceInitialPage = require('../../../pages/workspace/WorkspaceInitialPage').default;
Expand Down
8 changes: 8 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,14 @@ export default {
path: ROUTES.SETTINGS_SHARE_CODE,
exact: true,
},
Settings_Status: {
path: ROUTES.SETTINGS_STATUS,
exact: true,
},
Settings_Status_Set: {
path: ROUTES.SETTINGS_STATUS_SET,
exact: true,
},
Workspace_Initial: {
path: ROUTES.WORKSPACE_INITIAL,
},
Expand Down
9 changes: 9 additions & 0 deletions src/libs/Permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ function canUseScanReceipts(betas) {
return _.contains(betas, CONST.BETAS.SCAN_RECEIPTS) || canUseAllBetas(betas);
}

/**
* @param {Array<String>} betas
* @returns {Boolean}
*/
function canUseCustomStatus(betas) {
return _.contains(betas, CONST.BETAS.CUSTOM_STATUS) || canUseAllBetas(betas);
}

/**
* @param {Array<String>} betas
* @returns {Boolean}
Expand All @@ -113,5 +121,6 @@ export default {
canUsePolicyExpenseChat,
canUseTasks,
canUseScanReceipts,
canUseCustomStatus,
canUseDistanceRequests,
};
Loading

0 comments on commit 457c6df

Please sign in to comment.