Skip to content

Commit

Permalink
Merge pull request #30664 from Expensify/marcaaron-setLastOfferedFocu…
Browse files Browse the repository at this point in the history
…sMode

Update users to focus mode automatically if they have the minimum report number
  • Loading branch information
bondydaa authored Dec 5, 2023
2 parents 573c5e8 + eb99338 commit 395dea4
Show file tree
Hide file tree
Showing 13 changed files with 305 additions and 55 deletions.
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ const CONST = {
MAX_REPORT_PREVIEW_RECEIPTS: 3,
},
REPORT: {
MAX_COUNT_BEFORE_FOCUS_UPDATE: 30,
MAXIMUM_PARTICIPANTS: 8,
SPLIT_REPORTID: '-2',
ACTIONS: {
Expand Down
10 changes: 10 additions & 0 deletions src/Expensify.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import _ from 'underscore';
import ConfirmModal from './components/ConfirmModal';
import DeeplinkWrapper from './components/DeeplinkWrapper';
import EmojiPicker from './components/EmojiPicker/EmojiPicker';
import FocusModeNotification from './components/FocusModeNotification';
import GrowlNotification from './components/GrowlNotification';
import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import SplashScreenHider from './components/SplashScreenHider';
Expand Down Expand Up @@ -76,6 +77,9 @@ const propTypes = {
/** Whether the app is waiting for the server's response to determine if a room is public */
isCheckingPublicRoom: PropTypes.bool,

/** Whether we should display the notification alerting the user that focus mode has been auto-enabled */
focusModeNotification: PropTypes.bool,

...withLocalizePropTypes,
};

Expand All @@ -88,6 +92,7 @@ const defaultProps = {
isSidebarLoaded: false,
screenShareRequest: null,
isCheckingPublicRoom: true,
focusModeNotification: false,
};

const SplashScreenHiddenContext = React.createContext({});
Expand Down Expand Up @@ -221,6 +226,7 @@ function Expensify(props) {
isVisible
/>
) : null}
{props.focusModeNotification ? <FocusModeNotification /> : null}
</>
)}

Expand Down Expand Up @@ -261,6 +267,10 @@ export default compose(
screenShareRequest: {
key: ONYXKEYS.SCREEN_SHARE_REQUEST,
},
focusModeNotification: {
key: ONYXKEYS.FOCUS_MODE_NOTIFICATION,
initWithStoredValues: false,
},
}),
)(Expensify);

Expand Down
8 changes: 8 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ const ONYXKEYS = {
/** The user's cash card and imported cards (including the Expensify Card) */
CARD_LIST: 'cardList',

/** Whether the user has tried focus mode yet */
NVP_TRY_FOCUS_MODE: 'tryFocusMode',

/** Boolean flag used to display the focus mode notification */
FOCUS_MODE_NOTIFICATION: 'focusModeNotification',

/** Stores information about the user's saved statements */
WALLET_STATEMENT: 'walletStatement',

Expand Down Expand Up @@ -383,6 +389,8 @@ type OnyxValues = {
[ONYXKEYS.NVP_PRIORITY_MODE]: ValueOf<typeof CONST.PRIORITY_MODE>;
[ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge;
[ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string;
[ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean;
[ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean;
[ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record<string, string>;
[ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[];
[ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean;
Expand Down
47 changes: 47 additions & 0 deletions src/components/FocusModeNotification.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, {useEffect} from 'react';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import styles from '@styles/styles';
import * as Link from '@userActions/Link';
import * as User from '@userActions/User';
import CONST from '@src/CONST';
import ConfirmModal from './ConfirmModal';
import Text from './Text';
import TextLinkWithRef from './TextLink';

function FocusModeNotification() {
const {environmentURL} = useEnvironment();
const {translate} = useLocalize();
useEffect(() => {
User.updateChatPriorityMode(CONST.PRIORITY_MODE.GSD, true);
}, []);
const href = `${environmentURL}/settings/preferences/priority-mode`;
return (
<ConfirmModal
title={translate('focusModeUpdateModal.title')}
confirmText={translate('common.buttonConfirm')}
onConfirm={User.clearFocusModeNotification}
shouldShowCancelButton={false}
prompt={
<Text>
{translate('focusModeUpdateModal.prompt')}
<TextLinkWithRef
href={href}
style={styles.link}
onPress={() => {
User.clearFocusModeNotification();
Link.openLink(href, environmentURL);
}}
>
{translate('common.here')}
</TextLinkWithRef>
.
</Text>
}
isVisible
/>
);
}

FocusModeNotification.displayName = 'FocusModeNotification';
export default FocusModeNotification;
55 changes: 4 additions & 51 deletions src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@ import AnchorForCommentsOnly from '@components/AnchorForCommentsOnly';
import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils';
import Text from '@components/Text';
import useEnvironment from '@hooks/useEnvironment';
import Navigation from '@libs/Navigation/Navigation';
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
import * as Url from '@libs/Url';
import useThemeStyles from '@styles/useThemeStyles';
import * as Link from '@userActions/Link';
import * as Session from '@userActions/Session';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import htmlRendererPropTypes from './htmlRendererPropTypes';

function AnchorRenderer(props) {
Expand All @@ -26,50 +21,8 @@ function AnchorRenderer(props) {
const displayName = lodashGet(props.tnode, 'domNode.children[0].data', '');
const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {});
const attrHref = htmlAttribs.href || '';
const attrPath = Url.getPathFromURL(attrHref);
const hasSameOrigin = Url.hasSameExpensifyOrigin(attrHref, environmentURL);
const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(attrHref, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(attrHref, CONFIG.EXPENSIFY.STAGING_API_ROOT);
const internalNewExpensifyPath =
(Url.hasSameExpensifyOrigin(attrHref, CONST.NEW_EXPENSIFY_URL) ||
Url.hasSameExpensifyOrigin(attrHref, CONST.STAGING_NEW_EXPENSIFY_URL) ||
attrHref.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
!CONST.PATHS_TO_TREAT_AS_EXTERNAL.includes(attrPath)
? attrPath
: '';
const internalExpensifyPath =
hasExpensifyOrigin && !attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME) && !attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME) && attrPath;
const navigateToLink = () => {
// There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
// https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
// clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
// Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
// the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
if (hasExpensifyOrigin && attrHref.indexOf('newdotreport?reportID=') > -1) {
const reportID = attrHref.split('newdotreport?reportID=').pop();
const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID);
Navigation.navigate(reportRoute);
return;
}

// If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
// instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
if (internalNewExpensifyPath && hasSameOrigin) {
if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(internalNewExpensifyPath)) {
Session.signOutAndRedirectToSignIn();
return;
}
Navigation.navigate(internalNewExpensifyPath);
return;
}

// If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in.
// As attachments also use expensify.com we don't want it working the same as links.
if (internalExpensifyPath && !isAttachment) {
Link.openOldDotLink(internalExpensifyPath);
return;
}
Link.openExternalLink(attrHref);
};
const internalNewExpensifyPath = Link.getInternalNewExpensifyPath(attrHref);
const internalExpensifyPath = Link.getInternalExpensifyPath(attrHref);

if (!HTMLEngineUtils.isChildOfComment(props.tnode)) {
// This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click.
Expand All @@ -78,7 +31,7 @@ function AnchorRenderer(props) {
return (
<Text
style={styles.link}
onPress={navigateToLink}
onPress={() => Link.openLink(attrHref, environmentURL, isAttachment)}
suppressHighlighting
>
<TNodeChildrenRenderer tnode={props.tnode} />
Expand Down Expand Up @@ -109,7 +62,7 @@ function AnchorRenderer(props) {
key={props.key}
displayName={displayName}
// Only pass the press handler for internal links. For public links or whitelisted internal links fallback to default link handling
onPress={internalNewExpensifyPath || internalExpensifyPath ? navigateToLink : undefined}
onPress={internalNewExpensifyPath || internalExpensifyPath ? Link.openLink : undefined}
>
<TNodeChildrenRenderer tnode={props.tnode} />
</AnchorForCommentsOnly>
Expand Down
4 changes: 3 additions & 1 deletion src/components/TextLink.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import _ from 'underscore';
import useEnvironment from '@hooks/useEnvironment';
import stylePropTypes from '@styles/stylePropTypes';
import useThemeStyles from '@styles/useThemeStyles';
import * as Link from '@userActions/Link';
Expand Down Expand Up @@ -37,6 +38,7 @@ const defaultProps = {
};

function TextLink(props) {
const {environmentURL} = useEnvironment();
const styles = useThemeStyles();
const rest = _.omit(props, _.keys(propTypes));
const additionalStyles = _.isArray(props.style) ? props.style : [props.style];
Expand All @@ -51,7 +53,7 @@ function TextLink(props) {
return;
}

Link.openExternalLink(props.href);
Link.openLink(props.href, environmentURL);
};

/**
Expand Down
4 changes: 4 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,10 @@ export default {
year: 'Year',
selectYear: 'Please select a year',
},
focusModeUpdateModal: {
title: 'Welcome to #focus mode!',
prompt: "Read chats will be hidden, unless they have a green dot, which means there's an action you need to take on them. You can change this in your account settings ",
},
notFound: {
chatYouLookingForCannotBeFound: 'The chat you are looking for cannot be found.',
getMeOutOfHere: 'Get me out of here',
Expand Down
4 changes: 4 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,10 @@ export default {
year: 'Año',
selectYear: 'Por favor, selecciona un año',
},
focusModeUpdateModal: {
title: '¡Bienvenido al modo #concentración!',
prompt: 'Los mensajes leídos se ocultarán, a menos que tengan un punto verde, lo que significa que tienes que tomar una acción en ellos. Puedes cambiar esto en la configuración de tu cuenta ',
},
notFound: {
chatYouLookingForCannotBeFound: 'El chat que estás buscando no se pudo encontrar.',
getMeOutOfHere: 'Sácame de aquí',
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as App from '@userActions/App';
import * as Download from '@userActions/Download';
import * as Modal from '@userActions/Modal';
import * as PersonalDetails from '@userActions/PersonalDetails';
import * as PriorityMode from '@userActions/PriorityMode';
import * as Report from '@userActions/Report';
import * as Session from '@userActions/Session';
import Timing from '@userActions/Timing';
Expand Down Expand Up @@ -194,6 +195,8 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio
App.reconnectApp(lastUpdateIDAppliedToClient);
}

PriorityMode.autoSwitchToFocusMode();

App.setUpPoliciesAndNavigate(session);

App.redirectThirdPartyDesktopSignIn();
Expand Down
60 changes: 59 additions & 1 deletion src/libs/actions/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import asyncOpenURL from '@libs/asyncOpenURL';
import * as Environment from '@libs/Environment/Environment';
import Navigation from '@libs/Navigation/Navigation';
import * as Url from '@libs/Url';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

let isNetworkOffline = false;
Onyx.connect({
Expand Down Expand Up @@ -56,4 +60,58 @@ function openOldDotLink(url: string) {
(oldDotURL) => oldDotURL,
);
}
export {buildOldDotURL, openOldDotLink, openExternalLink};

function getInternalNewExpensifyPath(href: string) {
const attrPath = Url.getPathFromURL(href);
return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) &&
!CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => path === attrPath)
? attrPath
: '';
}

function getInternalExpensifyPath(href: string) {
const attrPath = Url.getPathFromURL(href);
const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
if (!hasExpensifyOrigin || attrPath.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL_PATHNAME) || attrPath.startsWith(CONFIG.EXPENSIFY.DEVPORTAL_URL_PATHNAME)) {
return '';
}

return attrPath;
}

function openLink(href: string, environmentURL: string, isAttachment = false) {
const hasSameOrigin = Url.hasSameExpensifyOrigin(href, environmentURL);
const hasExpensifyOrigin = Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONFIG.EXPENSIFY.STAGING_API_ROOT);
const internalNewExpensifyPath = getInternalNewExpensifyPath(href);
const internalExpensifyPath = getInternalExpensifyPath(href);

// There can be messages from Concierge with links to specific NewDot reports. Those URLs look like this:
// https://www.expensify.com.dev/newdotreport?reportID=3429600449838908 and they have a target="_blank" attribute. This is so that when a user is on OldDot,
// clicking on the link will open the chat in NewDot. However, when a user is in NewDot and clicks on the concierge link, the link needs to be handled differently.
// Normally, the link would be sent to Link.openOldDotLink() and opened in a new tab, and that's jarring to the user. Since the intention is to link to a specific NewDot chat,
// the reportID is extracted from the URL and then opened as an internal link, taking the user straight to the chat in the same tab.
if (hasExpensifyOrigin && href.indexOf('newdotreport?reportID=') > -1) {
const reportID = href.split('newdotreport?reportID=').pop();
const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID ?? '');
Navigation.navigate(reportRoute);
return;
}

// If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
// instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
if (internalNewExpensifyPath && hasSameOrigin) {
Navigation.navigate(internalNewExpensifyPath);

Check failure on line 103 in src/libs/actions/Link.ts

View workflow job for this annotation

GitHub Actions / typecheck / typecheck

Argument of type 'string' is not assignable to parameter of type 'AllRoutes | undefined'.
return;
}

// If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in.
// As attachments also use expensify.com we don't want it working the same as links.
if (internalExpensifyPath && !isAttachment) {
openOldDotLink(internalExpensifyPath);
return;
}

openExternalLink(href);
}

export {buildOldDotURL, openOldDotLink, openExternalLink, openLink, getInternalNewExpensifyPath, getInternalExpensifyPath};
Loading

0 comments on commit 395dea4

Please sign in to comment.