Skip to content

Commit

Permalink
Merge pull request #40010 from rezkiy37/feature/38773-support-read-on…
Browse files Browse the repository at this point in the history
…ly-messages

Support read only messages
  • Loading branch information
mountiny authored Apr 26, 2024
2 parents d0c2551 + 38cbb1b commit 3f379ec
Show file tree
Hide file tree
Showing 16 changed files with 197 additions and 9 deletions.
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,12 @@ const CONST = {
OWNER_EMAIL_FAKE: '__FAKE__',
OWNER_ACCOUNT_ID_FAKE: 0,
DEFAULT_REPORT_NAME: 'Chat Report',
PERMISSIONS: {
READ: 'read',
WRITE: 'write',
SHARE: 'share',
OWN: 'own',
},
},
NEXT_STEP: {
FINISHED: 'Finished!',
Expand Down
30 changes: 25 additions & 5 deletions src/components/Banner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import getButtonState from '@libs/getButtonState';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import Hoverable from './Hoverable';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
Expand All @@ -17,7 +18,13 @@ import Tooltip from './Tooltip';

type BannerProps = {
/** Text to display in the banner. */
text: string;
text?: string;

/** Content to display in the banner. */
content?: React.ReactNode;

/** The icon asset to display to the left of the text */
icon?: IconAsset | null;

/** Should this component render the left-aligned exclamation icon? */
shouldShowIcon?: boolean;
Expand All @@ -41,7 +48,18 @@ type BannerProps = {
textStyles?: StyleProp<TextStyle>;
};

function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRenderHTML = false, shouldShowIcon = false, shouldShowCloseButton = false}: BannerProps) {
function Banner({
text,
content,
icon = Expensicons.Exclamation,
onClose,
onPress,
containerStyles,
textStyles,
shouldRenderHTML = false,
shouldShowIcon = false,
shouldShowCloseButton = false,
}: BannerProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand All @@ -65,15 +83,17 @@ function Banner({text, onClose, onPress, containerStyles, textStyles, shouldRend
]}
>
<View style={[styles.flexRow, styles.flexGrow1, styles.mw100, styles.alignItemsCenter]}>
{shouldShowIcon && (
{shouldShowIcon && icon && (
<View style={[styles.mr3]}>
<Icon
src={Expensicons.Exclamation}
src={icon}
fill={StyleUtils.getIconFillColor(getButtonState(shouldHighlight))}
/>
</View>
)}
{shouldRenderHTML ? (
{content && content}

{shouldRenderHTML && text ? (
<RenderHTML html={text} />
) : (
<Text
Expand Down
4 changes: 4 additions & 0 deletions src/components/TaskHeaderActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ function TaskHeaderActionButton({report, session}: TaskHeaderActionButtonProps)
const {translate} = useLocalize();
const styles = useThemeStyles();

if (!ReportUtils.canWriteInReport(report)) {
return null;
}

return (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.justifyContentEnd]}>
<Button
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2770,6 +2770,16 @@ export default {
},
copyReferralLink: 'Copy invite link',
},
systemChatFooterMessage: {
[CONST.INTRO_CHOICES.MANAGE_TEAM]: {
phrase1: 'Chat with your setup specialist in ',
phrase2: ' for help',
},
default: {
phrase1: 'Message ',
phrase2: ' for help with setup',
},
},
violations: {
allTagLevelsRequired: 'All tags required',
autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`,
Expand Down
10 changes: 10 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3263,6 +3263,16 @@ export default {
},
copyReferralLink: 'Copiar enlace de invitación',
},
systemChatFooterMessage: {
[CONST.INTRO_CHOICES.MANAGE_TEAM]: {
phrase1: 'Chatea con tu especialista asignado en ',
phrase2: ' para obtener ayuda',
},
default: {
phrase1: 'Envía un email a ',
phrase2: ' para obtener ayuda con la configuración',
},
},
violations: {
allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
Expand Down
20 changes: 18 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1172,10 +1172,25 @@ function isJoinRequestInAdminRoom(report: OnyxEntry<Report>): boolean {
return ReportActionsUtils.isActionableJoinRequestPending(report.reportID);
}

/**
* Checks if the user can write in the provided report
*/
function canWriteInReport(report: OnyxEntry<Report>): boolean {
if (Array.isArray(report?.permissions) && report?.permissions.length > 0) {
return report?.permissions?.includes(CONST.REPORT.PERMISSIONS.WRITE);
}

return true;
}

/**
* Checks if the current user is allowed to comment on the given report.
*/
function isAllowedToComment(report: OnyxEntry<Report>): boolean {
if (!canWriteInReport(report)) {
return false;
}

// Default to allowing all users to post
const capability = report?.writeCapability ?? CONST.REPORT.WRITE_CAPABILITIES.ALL;

Expand Down Expand Up @@ -5374,7 +5389,7 @@ function canUserPerformWriteAction(report: OnyxEntry<Report>) {
return false;
}

return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser;
return !isArchivedRoom(report) && isEmptyObject(reportErrors) && report && isAllowedToComment(report) && !isAnonymousUser && canWriteInReport(report);
}

/**
Expand Down Expand Up @@ -6281,7 +6296,6 @@ export {
getParsedComment,
getParticipantAccountIDs,
getParticipants,
getPayeeName,
getPendingChatMembers,
getPersonalDetailsForAccountID,
getPolicyDescriptionText,
Expand Down Expand Up @@ -6316,6 +6330,7 @@ export {
getWorkspaceChats,
getWorkspaceIcon,
goBackToDetailsPage,
getPayeeName,
hasActionsWithErrors,
hasAutomatedExpensifyAccountIDs,
hasExpensifyGuidesEmails,
Expand Down Expand Up @@ -6403,6 +6418,7 @@ export {
isValidReport,
isValidReportIDFromPath,
isWaitingForAssigneeToCompleteTask,
canWriteInReport,
navigateToDetailsPage,
navigateToPrivateNotes,
parseReportRouteParams,
Expand Down
1 change: 1 addition & 0 deletions src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2199,6 +2199,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
errors: {},
},
},
chatReportIDAdmins: makeMeAdmin ? Number(adminsChatReportID) : undefined,
},
},
{
Expand Down
4 changes: 4 additions & 0 deletions src/libs/actions/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,10 @@ function canModifyTask(taskReport: OnyxEntry<OnyxTypes.Report>, sessionAccountID
return true;
}

if (!ReportUtils.canWriteInReport(ReportUtils.getReport(taskReport?.reportID))) {
return false;
}

return !isEmptyObject(taskReport) && ReportUtils.isAllowedToComment(taskReport);
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/HeaderView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ function HeaderView({
}

// Task is not closed
if (report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) {
if (ReportUtils.canWriteInReport(report) && report.stateNum !== CONST.REPORT.STATE_NUM.APPROVED && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED && canModifyTask) {
threeDotMenuItems.push({
icon: Expensicons.Trashcan,
text: translate('common.delete'),
Expand Down
2 changes: 2 additions & 0 deletions src/pages/home/ReportScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ function ReportScreen({
isOptimisticReport: reportProp?.isOptimisticReport,
lastMentionedTime: reportProp?.lastMentionedTime,
avatarUrl: reportProp?.avatarUrl,
permissions: reportProp?.permissions,
}),
[
reportProp?.lastReadTime,
Expand Down Expand Up @@ -249,6 +250,7 @@ function ReportScreen({
reportProp?.isOptimisticReport,
reportProp?.lastMentionedTime,
reportProp?.avatarUrl,
reportProp?.permissions,
],
);

Expand Down
13 changes: 13 additions & 0 deletions src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -513,5 +513,18 @@ const ContextMenuActions: ContextMenuAction[] = [
},
];

const restrictedReadOnlyActions: TranslationPaths[] = [
'common.download',
'reportActionContextMenu.replyInThread',
'reportActionContextMenu.editAction',
'reportActionContextMenu.joinThread',
'reportActionContextMenu.deleteAction',
];

const RestrictedReadOnlyContextMenuActions: ContextMenuAction[] = ContextMenuActions.filter(
(action) => 'textTranslateKey' in action && restrictedReadOnlyActions.includes(action.textTranslateKey),
);

export {RestrictedReadOnlyContextMenuActions};
export default ContextMenuActions;
export type {ContextMenuActionPayload, ContextMenuAction};
2 changes: 2 additions & 0 deletions src/pages/home/report/ReportActionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {OriginalMessageActionableMentionWhisper, OriginalMessageActionableTrackedExpenseWhisper, OriginalMessageJoinPolicyChangeLog} from '@src/types/onyx/OriginalMessage';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
import {RestrictedReadOnlyContextMenuActions} from './ContextMenu/ContextMenuActions';
import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu';
import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu';
import {hideContextMenu} from './ContextMenu/ReportActionContextMenu';
Expand Down Expand Up @@ -914,6 +915,7 @@ function ReportActionItem({
originalReportID={originalReportID ?? ''}
isArchivedRoom={ReportUtils.isArchivedRoom(report)}
displayAsGroup={displayAsGroup}
disabledActions={!ReportUtils.canWriteInReport(report) ? RestrictedReadOnlyContextMenuActions : []}
isVisible={hovered && draftMessage === undefined && !hasErrors}
draftMessage={draftMessage}
isChronosReport={ReportUtils.chatIncludesChronos(originalReport)}
Expand Down
5 changes: 4 additions & 1 deletion src/pages/home/report/ReportFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type * as OnyxTypes from '@src/types/onyx';
import type {PendingAction} from '@src/types/onyx/OnyxCommon';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import ReportActionCompose from './ReportActionCompose/ReportActionCompose';
import SystemChatReportFooterMessage from './SystemChatReportFooterMessage';

type ReportFooterOnyxProps = {
/** Whether to show the compose input */
Expand Down Expand Up @@ -81,6 +82,7 @@ function ReportFooter({

const isSmallSizeLayout = windowWidth - (isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint;
const hideComposer = !ReportUtils.canUserPerformWriteAction(report);
const canWriteInReport = ReportUtils.canWriteInReport(report);

const allPersonalDetails = usePersonalDetails();

Expand Down Expand Up @@ -131,14 +133,15 @@ function ReportFooter({
return (
<>
{hideComposer && (
<View style={[styles.chatFooter, isArchivedRoom || isAnonymousUser ? styles.mt4 : {}, isSmallScreenWidth ? styles.mb5 : null]}>
<View style={[styles.chatFooter, isArchivedRoom || isAnonymousUser || !canWriteInReport ? styles.mt4 : {}, isSmallScreenWidth ? styles.mb5 : null]}>
{isAnonymousUser && !isArchivedRoom && (
<AnonymousReportFooter
report={report}
isSmallSizeLayout={isSmallSizeLayout}
/>
)}
{isArchivedRoom && <ArchivedReportFooter report={report} />}
{!canWriteInReport && <SystemChatReportFooterMessage />}
{!isSmallScreenWidth && <View style={styles.offlineIndicatorRow}>{hideComposer && <OfflineIndicator containerStyles={[styles.chatItemComposeSecondaryRow]} />}</View>}
</View>
)}
Expand Down
91 changes: 91 additions & 0 deletions src/pages/home/report/SystemChatReportFooterMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React, {useMemo} from 'react';
import {withOnyx} from 'react-native-onyx';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import Banner from '@components/Banner';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as ReportUtils from '@libs/ReportUtils';
import Navigation from '@navigation/Navigation';
import * as ReportInstance from '@userActions/Report';
import type {OnboardingPurposeType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy as PolicyType} from '@src/types/onyx';

type SystemChatReportFooterMessageOnyxProps = {
/** Saved onboarding purpose selected by the user */
choice: OnyxEntry<OnboardingPurposeType>;

/** The list of this user's policies */
policies: OnyxCollection<PolicyType>;

/** policyID for main workspace */
activePolicyID: OnyxEntry<Required<string>>;
};

type SystemChatReportFooterMessageProps = SystemChatReportFooterMessageOnyxProps;

function SystemChatReportFooterMessage({choice, policies, activePolicyID}: SystemChatReportFooterMessageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();

const adminChatReport = useMemo(() => {
const adminPolicy = activePolicyID
? PolicyUtils.getPolicy(activePolicyID ?? '')
: Object.values(policies ?? {}).find((policy) => PolicyUtils.shouldShowPolicy(policy, false) && policy?.role === CONST.POLICY.ROLE.ADMIN && policy?.chatReportIDAdmins);

return ReportUtils.getReport(String(adminPolicy?.chatReportIDAdmins));
}, [activePolicyID, policies]);

const content = useMemo(() => {
switch (choice) {
case CONST.ONBOARDING_CHOICES.MANAGE_TEAM:
return (
<>
{translate('systemChatFooterMessage.newDotManageTeam.phrase1')}
<TextLink onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(adminChatReport?.reportID ?? ''))}>
{adminChatReport?.reportName ?? CONST.REPORT.WORKSPACE_CHAT_ROOMS.ADMINS}
</TextLink>
{translate('systemChatFooterMessage.newDotManageTeam.phrase2')}
</>
);
default:
return (
<>
{translate('systemChatFooterMessage.default.phrase1')}
<TextLink onPress={() => ReportInstance.navigateToConciergeChat()}>{CONST?.CONCIERGE_CHAT_NAME}</TextLink>
{translate('systemChatFooterMessage.default.phrase2')}
</>
);
}
}, [adminChatReport?.reportName, adminChatReport?.reportID, choice, translate]);

return (
<Banner
containerStyles={[styles.archivedReportFooter]}
shouldShowIcon
icon={Expensicons.Lightbulb}
content={<Text suppressHighlighting>{content}</Text>}
/>
);
}

SystemChatReportFooterMessage.displayName = 'SystemChatReportFooterMessage';

export default withOnyx<SystemChatReportFooterMessageProps, SystemChatReportFooterMessageOnyxProps>({
choice: {
key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
},
activePolicyID: {
key: ONYXKEYS.NVP_ACTIVE_POLICY_ID,
initialValue: null,
},
})(SystemChatReportFooterMessage);
4 changes: 4 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,10 @@ const styles = (theme: ThemeColors) =>
borderRadius: variables.buttonBorderRadius,
},

borderRadiusComponentLarge: {
borderRadius: variables.componentBorderRadiusLarge,
},

bottomTabBarContainer: {
flexDirection: 'row',
height: variables.bottomTabHeight,
Expand Down
Loading

0 comments on commit 3f379ec

Please sign in to comment.