Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wildan/fix/21518/client pusher method #24407

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
97807c2
Create is leaving client pusher event
wildan-m Aug 10, 2023
b769dc8
implement subscribeToReportLeavingEvents
wildan-m Aug 10, 2023
c87ceb9
Remove unnecessary code, run prettier
wildan-m Aug 10, 2023
b57b046
refine comment, remove leavingWatchTimers variable, refine checkAndSu…
wildan-m Aug 10, 2023
3ed2c75
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 11, 2023
8d92b2b
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 15, 2023
9bc367e
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 17, 2023
488e320
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 21, 2023
09aa78e
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 22, 2023
5376bcf
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 22, 2023
16106f8
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 23, 2023
ea6c6fe
Move REPORT_USER_IS_LEAVING_ROOM to ONYXKEYS.ts
wildan-m Aug 23, 2023
fee3986
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 25, 2023
e4d1363
Remove unnecessary code
wildan-m Aug 25, 2023
d7ac19a
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 28, 2023
edb9634
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 29, 2023
ab51aea
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Aug 29, 2023
96d1c45
Add prevReport.statusNum to dependency
wildan-m Aug 29, 2023
4ddbcc9
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 4, 2023
7ab5f2e
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 6, 2023
1952cfd
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 11, 2023
20328da
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 13, 2023
a785a00
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 13, 2023
9f47e6a
Update src/libs/actions/Report.js
wildan-m Sep 13, 2023
6a1f080
Update src/libs/actions/Report.js
wildan-m Sep 13, 2023
39c079b
Update src/pages/home/ReportScreen.js
wildan-m Sep 13, 2023
70c6b30
Update src/pages/home/ReportScreen.js
wildan-m Sep 13, 2023
f91b792
make getNormalizedTypingStatus become generic getNormalizedStatus
wildan-m Sep 13, 2023
1ba065a
Merge branch 'wildan/fix/21518/client-pusher-method' of https://githu…
wildan-m Sep 13, 2023
a658b18
fix lint
wildan-m Sep 13, 2023
e0f8cbb
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 14, 2023
30c7499
Ensure pusher's leavingStatus be sent earlier
wildan-m Sep 14, 2023
b9a001e
Add prevUserLeavingStatus to the check
wildan-m Sep 14, 2023
09ebf90
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Sep 20, 2023
b3d2a0c
resolve leftover conflict
wildan-m Sep 20, 2023
91c533e
resolve leftover conflict
wildan-m Sep 20, 2023
8926377
remove unnecessary code
wildan-m Sep 20, 2023
b2303b2
change didSubscribeToReportLeavingEvents check to early return
wildan-m Sep 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ const ONYXKEYS = {
REPORT_DRAFT_COMMENT_NUMBER_OF_LINES: 'reportDraftCommentNumberOfLines_',
REPORT_IS_COMPOSER_FULL_SIZE: 'reportIsComposerFullSize_',
REPORT_USER_IS_TYPING: 'reportUserIsTyping_',
REPORT_USER_IS_LEAVING_ROOM: 'reportUserIsLeavingRoom_',
SECURITY_GROUP: 'securityGroup_',
TRANSACTION: 'transactions_',

Expand Down Expand Up @@ -384,6 +385,7 @@ type OnyxValues = {
[ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES]: number;
[ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE]: boolean;
[ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING]: boolean;
[ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM]: boolean;
[ONYXKEYS.COLLECTION.SECURITY_GROUP]: OnyxTypes.SecurityGroup;
[ONYXKEYS.COLLECTION.TRANSACTION]: OnyxTypes.Transaction;
[ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS]: OnyxTypes.RecentlyUsedTags;
Expand Down
1 change: 1 addition & 0 deletions src/libs/Pusher/EventType.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
export default {
REPORT_COMMENT: 'reportComment',
ONYX_API_UPDATE: 'onyxApiUpdate',
USER_IS_LEAVING_ROOM: 'client-userIsLeavingRoom',
USER_IS_TYPING: 'client-userIsTyping',
MULTIPLE_EVENTS: 'multipleEvents',
MULTIPLE_EVENT_TYPE: {
Expand Down
88 changes: 79 additions & 9 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,24 +103,24 @@ function getReportChannelName(reportID) {
}

/**
* There are 2 possibilities that we can receive via pusher for a user's typing status:
* There are 2 possibilities that we can receive via pusher for a user's typing/leaving status:
* 1. The "new" way from New Expensify is passed as {[login]: Boolean} (e.g. {yuwen@expensify.com: true}), where the value
* is whether the user with that login is typing on the report or not.
* is whether the user with that login is typing/leaving on the report or not.
* 2. The "old" way from e.com which is passed as {userLogin: login} (e.g. {userLogin: bstites@expensify.com})
*
* This method makes sure that no matter which we get, we return the "new" format
*
* @param {Object} typingStatus
* @param {Object} status
* @returns {Object}
*/
function getNormalizedTypingStatus(typingStatus) {
let normalizedTypingStatus = typingStatus;
function getNormalizedStatus(status) {
let normalizedStatus = status;

if (_.first(_.keys(typingStatus)) === 'userLogin') {
normalizedTypingStatus = {[typingStatus.userLogin]: true};
if (_.first(_.keys(status)) === 'userLogin') {
normalizedStatus = {[status.userLogin]: true};
}

return normalizedTypingStatus;
return normalizedStatus;
}

/**
Expand All @@ -141,7 +141,7 @@ function subscribeToReportTypingEvents(reportID) {
// If the pusher message comes from OldDot, we expect the typing status to be keyed by user
// login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID
// since personal details are keyed by accountID.
const normalizedTypingStatus = getNormalizedTypingStatus(typingStatus);
const normalizedTypingStatus = getNormalizedStatus(typingStatus);
const accountIDOrLogin = _.first(_.keys(normalizedTypingStatus));

if (!accountIDOrLogin) {
Expand Down Expand Up @@ -170,6 +170,41 @@ function subscribeToReportTypingEvents(reportID) {
});
}

/**
* Initialize our pusher subscriptions to listen for someone leaving a room.
*
* @param {String} reportID
*/
function subscribeToReportLeavingEvents(reportID) {
if (!reportID) {
return;
}

// Make sure we have a clean Leaving indicator before subscribing to leaving events
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, false);

const pusherChannelName = getReportChannelName(reportID);
Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus) => {
// If the pusher message comes from OldDot, we expect the leaving status to be keyed by user
// login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID
// since personal details are keyed by accountID.
const normalizedLeavingStatus = getNormalizedStatus(leavingStatus);
const accountIDOrLogin = _.first(_.keys(normalizedLeavingStatus));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if this is not sorted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cloned it from typing, alternatively, we can the below code if required, but a little bit longer

const normalizedLeavingStatus = getNormalizedLeavingStatus(leavingStatus);
const accountIDOrLogin = normalizedLeavingStatus ? Object.keys(normalizedLeavingStatus)[0] : null;


if (!accountIDOrLogin) {
return;
}

if (Number(accountIDOrLogin) !== currentUserAccountID) {
return;
}

Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, true);
}).catch((error) => {
Log.hmmm('[Report] Failed to initially subscribe to Pusher channel', false, {errorType: error.type, pusherChannelName});
});
}

/**
* Remove our pusher subscriptions to listen for someone typing in a report.
*
Expand All @@ -185,6 +220,21 @@ function unsubscribeFromReportChannel(reportID) {
Pusher.unsubscribe(pusherChannelName, Pusher.TYPE.USER_IS_TYPING);
}

/**
* Remove our pusher subscriptions to listen for someone leaving a report.
*
* @param {String} reportID
*/
function unsubscribeFromLeavingRoomReportChannel(reportID) {
if (!reportID) {
return;
}

const pusherChannelName = getReportChannelName(reportID);
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, false);
Pusher.unsubscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM);
}

// New action subscriber array for report pages
let newActionSubscribers = [];

Expand Down Expand Up @@ -865,6 +915,17 @@ function broadcastUserIsTyping(reportID) {
typingStatus[currentUserAccountID] = true;
Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus);
}
/**
* Broadcasts to the report's private pusher channel whether a user is leaving a report
*
* @param {String} reportID
*/
function broadcastUserIsLeavingRoom(reportID) {
const privateReportChannelName = getReportChannelName(reportID);
const leavingStatus = {};
leavingStatus[currentUserAccountID] = true;
Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus);
}

/**
* When a report changes in Onyx, this fetches the report from the API if the report doesn't have a name
Expand Down Expand Up @@ -1781,6 +1842,12 @@ function getCurrentUserAccountID() {
function leaveRoom(reportID) {
const report = lodashGet(allReports, [reportID], {});
const reportKeys = _.keys(report);

// Pusher's leavingStatus should be sent earlier.
// Place the broadcast before calling the LeaveRoom API to prevent a race condition
// between Onyx report being null and Pusher's leavingStatus becoming true.
broadcastUserIsLeavingRoom(reportID);

API.write(
'LeaveRoom',
{
Expand Down Expand Up @@ -2067,10 +2134,13 @@ export {
updateWriteCapabilityAndNavigate,
updateNotificationPreferenceAndNavigate,
subscribeToReportTypingEvents,
subscribeToReportLeavingEvents,
unsubscribeFromReportChannel,
unsubscribeFromLeavingRoomReportChannel,
saveReportComment,
saveReportCommentNumberOfLines,
broadcastUserIsTyping,
broadcastUserIsLeavingRoom,
togglePinnedState,
editReportComment,
handleUserDeletedLinksInHtml,
Expand Down
57 changes: 51 additions & 6 deletions src/pages/home/ReportScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ const propTypes = {
/** All of the personal details for everyone */
personalDetails: PropTypes.objectOf(personalDetailsPropType),

/** Whether user is leaving the current report */
userLeavingStatus: PropTypes.bool,

...windowDimensionsPropTypes,
...viewportOffsetTopPropTypes,
...withCurrentReportIDPropTypes,
Expand All @@ -105,6 +108,7 @@ const defaultProps = {
betas: [],
policies: {},
accountManagerReportID: null,
userLeavingStatus: false,
personalDetails: {},
...withCurrentReportIDDefaultProps,
};
Expand Down Expand Up @@ -145,12 +149,14 @@ function ReportScreen({
viewportOffsetTop,
isComposerFullSize,
errors,
userLeavingStatus,
currentReportID,
}) {
const firstRenderRef = useRef(true);
const flatListRef = useRef();
const reactionListRef = useRef();
const prevReport = usePrevious(report);
const prevUserLeavingStatus = usePrevious(userLeavingStatus);

const [skeletonViewContainerHeight, setSkeletonViewContainerHeight] = useState(0);
const [isBannerVisible, setIsBannerVisible] = useState(true);
Expand All @@ -175,6 +181,7 @@ function ReportScreen({
const policy = policies[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`];

const isTopMostReportId = currentReportID === getReportID(route);
const didSubscribeToReportLeavingEvents = useRef(false);

const isDefaultReport = checkDefaultReport(report);

Expand Down Expand Up @@ -234,7 +241,7 @@ function ReportScreen({
}

// It possible that we may not have the report object yet in Onyx yet e.g. we navigated to a URL for an accessible report that
// is not stored locally yet. If props.report.reportID exists, then the report has been stored locally and nothing more needs to be done.
// is not stored locally yet. If report.reportID exists, then the report has been stored locally and nothing more needs to be done.
// If it doesn't exist, then we fetch the report from the API.
if (report.reportID && report.reportID === getReportID(route)) {
return;
Expand Down Expand Up @@ -282,6 +289,14 @@ function ReportScreen({
useEffect(() => {
fetchReportIfNeeded();
ComposerActions.setShouldShowComposeInput(true);
return () => {
if (!didSubscribeToReportLeavingEvents) {
return;
}

Report.unsubscribeFromLeavingRoomReportChannel(report.reportID);
};

// I'm disabling the warning, as it expects to use exhaustive deps, even though we want this useEffect to run only on the first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Expand All @@ -292,24 +307,51 @@ function ReportScreen({
firstRenderRef.current = false;
return;
}

const onyxReportID = report.reportID;
const prevOnyxReportID = prevReport.reportID;
const routeReportID = getReportID(route);

// Navigate to the Concierge chat if the room was removed from another device (e.g. user leaving a room)
if (
// non-optimistic case
wildan-m marked this conversation as resolved.
Show resolved Hide resolved
(!prevUserLeavingStatus && userLeavingStatus) ||
// optimistic case
wildan-m marked this conversation as resolved.
Show resolved Hide resolved
(prevOnyxReportID && prevOnyxReportID === routeReportID && !onyxReportID && prevReport.statusNum === CONST.REPORT.STATUS.OPEN && report.statusNum === CONST.REPORT.STATUS.CLOSED)
) {
Navigation.goBack();
Report.navigateToConciergeChat();
return;
}

// If you already have a report open and are deeplinking to a new report on native,
// the ReportScreen never actually unmounts and the reportID in the route also doesn't change.
// Therefore, we need to compare if the existing reportID is the same as the one in the route
// before deciding that we shouldn't call OpenReport.
const onyxReportID = report.reportID;
const routeReportID = getReportID(route);
if (onyxReportID === prevReport.reportID && (!onyxReportID || onyxReportID === routeReportID)) {
return;
}

fetchReportIfNeeded();
ComposerActions.setShouldShowComposeInput(true);
}, [route, report, errors, fetchReportIfNeeded, prevReport.reportID]);
}, [route, report, errors, fetchReportIfNeeded, prevReport.reportID, prevUserLeavingStatus, userLeavingStatus, prevReport.statusNum]);

useEffect(() => {
// Ensures subscription event succeeds when the report/workspace room is created optimistically.
Comment on lines +339 to +340
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have early returned if reportID is null to avoid the console error mentioned in #29116.

Copy link
Contributor Author

@wildan-m wildan-m Nov 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fedirjh Thanks for your feedback. We did it here and here, but we might also need to put it in useEffect

The issue also related to this function:

function getReportID(route) {
// // The reportID is used inside a collection key and should not be empty, as an empty reportID will result in the entire collection being returned.
return String(lodashGet(route, 'params.reportID', null));
}

it returned 'null' string instead of an actual null.

I'd recommend to change it to:

function getReportID(route) {
    const reportID = lodashGet(route, 'params.reportID', null);
    return reportID !== null ? String(reportID) : null;
}

@luacmartins, @allroundexperts, considering that this has passed the regression period, and any change would require re-testing. What should we do in this situation? Will there be any adjustments?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend to change it to:

We attempted this in PR #29590, but it resulted in a regression, so we reverted the changes in PR #29893. The issue arises from the fact that getReportID is utilized within the withOnyx HOC. When null is passed as its value, Onyx will return the entire collection. Therefore, it was intentional for getReportID to return a 'null' string when the report is empty.

key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getReportID(route)}`,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fedirjh seems you've resolved it here. thank you

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we also need to check onyx report reportId availability to avoid #38174

// Check if the optimistic `OpenReport` or `AddWorkspaceRoom` has succeeded by confirming
// any `pendingFields.createChat` or `pendingFields.addWorkspaceRoom` fields are set to null.
// Existing reports created will have empty fields for `pendingFields`.
const didCreateReportSuccessfully = !report.pendingFields || (!report.pendingFields.addWorkspaceRoom && !report.pendingFields.createChat);
if (!didSubscribeToReportLeavingEvents.current && didCreateReportSuccessfully) {
Report.subscribeToReportLeavingEvents(reportID);
didSubscribeToReportLeavingEvents.current = true;
}
}, [report, didSubscribeToReportLeavingEvents, reportID]);

// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = useMemo(
() => (!_.isEmpty(report) && !isDefaultReport && !report.reportID && !isOptimisticDelete && !report.isLoadingReportActions && !isLoading) || shouldHideReport,
[report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete],
() => (!_.isEmpty(report) && !isDefaultReport && !report.reportID && !isOptimisticDelete && !report.isLoadingReportActions && !isLoading && !userLeavingStatus) || shouldHideReport,
[report, isLoading, shouldHideReport, isDefaultReport, isOptimisticDelete, userLeavingStatus],
);

return (
Expand Down Expand Up @@ -452,5 +494,8 @@ export default compose(
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
userLeavingStatus: {
key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${getReportID(route)}`,
},
}),
)(ReportScreen);
Loading