Skip to content

Commit

Permalink
Merge pull request Expensify#30927 from namhihi237/fix-39987-translat…
Browse files Browse the repository at this point in the history
…e-invite-member

translate invite member to room
  • Loading branch information
mountiny authored Dec 6, 2023
2 parents 27cb3f7 + 243f932 commit 9b71f23
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 61 deletions.
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1547,6 +1547,12 @@ export default {
invitePeople: 'Invite new members',
genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.',
pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
user: 'user',
users: 'users',
invited: 'invited',
removed: 'removed',
to: 'to',
from: 'from',
},
inviteMessage: {
inviteMessageTitle: 'Add message',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,12 @@ export default {
invitePeople: 'Invitar nuevos miembros',
genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..',
pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,
user: 'usuario',
users: 'usuarios',
invited: 'invitó',
removed: 'eliminó',
to: 'a',
from: 'de',
},
inviteMessage: {
inviteMessageTitle: 'Añadir un mensaje',
Expand Down
46 changes: 40 additions & 6 deletions src/libs/Localize/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as RNLocalize from 'react-native-localize';
import Onyx from 'react-native-onyx';
import Log from '@libs/Log';
import {MessageElementBase, MessageTextElement} from '@libs/MessageElement';
import Config from '@src/CONFIG';
import CONST from '@src/CONST';
import translations from '@src/languages/translations';
Expand Down Expand Up @@ -121,15 +122,48 @@ function translateIfPhraseKey(message: MaybePhraseKey): string {
}
}

function getPreferredListFormat(): Intl.ListFormat {
if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
init();
}

return CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
}

/**
* Format an array into a string with comma and "and" ("a dog, a cat and a chicken")
*/
function arrayToString(anArray: string[]) {
if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) {
init();
function formatList(components: string[]) {
const listFormat = getPreferredListFormat();
return listFormat.format(components);
}

function formatMessageElementList<E extends MessageElementBase>(elements: readonly E[]): ReadonlyArray<E | MessageTextElement> {
const listFormat = getPreferredListFormat();
const parts = listFormat.formatToParts(elements.map((e) => e.content));
const resultElements: Array<E | MessageTextElement> = [];

let nextElementIndex = 0;
for (const part of parts) {
if (part.type === 'element') {
/**
* The standard guarantees that all input elements will be present in the constructed parts, each exactly
* once, and without any modifications: https://tc39.es/ecma402/#sec-createpartsfromlist
*/
const element = elements[nextElementIndex++];

resultElements.push(element);
} else {
const literalElement: MessageTextElement = {
kind: 'text',
content: part.value,
};

resultElements.push(literalElement);
}
}
const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()];
return listFormat.format(anArray);

return resultElements;
}

/**
Expand All @@ -139,5 +173,5 @@ function getDevicePreferredLocale(): string {
return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT;
}

export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale};
export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale};
export type {PhraseParameters, Phrase, MaybePhraseKey};
11 changes: 11 additions & 0 deletions src/libs/MessageElement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
type MessageElementBase = {
readonly kind: string;
readonly content: string;
};

type MessageTextElement = {
readonly kind: 'text';
readonly content: string;
} & MessageElementBase;

export type {MessageElementBase, MessageTextElement};
13 changes: 13 additions & 0 deletions src/libs/PersonalDetailsUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ function getFormattedAddress(privatePersonalDetails) {
return formattedAddress.trim().replace(/,$/, '');
}

/**
* @param {Object} personalDetail - details object
* @returns {String | undefined} - The effective display name
*/
function getEffectiveDisplayName(personalDetail) {
if (personalDetail) {
return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName;
}

return undefined;
}

export {
getDisplayNameOrDefault,
getPersonalDetailsByIDs,
Expand All @@ -206,4 +218,5 @@ export {
getFormattedAddress,
getFormattedStreet,
getStreetLines,
getEffectiveDisplayName,
};
113 changes: 109 additions & 4 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,37 @@ import OnyxUtils from 'react-native-onyx/lib/utils';
import {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {ActionName} from '@src/types/onyx/OriginalMessage';
import {ActionName, ChangeLog} from '@src/types/onyx/OriginalMessage';
import Report from '@src/types/onyx/Report';
import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction';
import ReportAction, {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject';
import * as CollectionUtils from './CollectionUtils';
import * as Environment from './Environment/Environment';
import isReportMessageAttachment from './isReportMessageAttachment';
import * as Localize from './Localize';
import Log from './Log';
import {MessageElementBase, MessageTextElement} from './MessageElement';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';

type LastVisibleMessage = {
lastMessageTranslationKey?: string;
lastMessageText: string;
lastMessageHtml?: string;
};

type MemberChangeMessageUserMentionElement = {
readonly kind: 'userMention';
readonly accountID: number;
} & MessageElementBase;

type MemberChangeMessageRoomReferenceElement = {
readonly kind: 'roomReference';
readonly roomName: string;
readonly roomID: number;
} & MessageElementBase;

type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;

const allReports: OnyxCollection<Report> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
Expand Down Expand Up @@ -100,7 +116,7 @@ function isReimbursementQueuedAction(reportAction: OnyxEntry<ReportAction>) {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED;
}

function isChannelLogMemberAction(reportAction: OnyxEntry<ReportAction>) {
function isMemberChangeAction(reportAction: OnyxEntry<ReportAction>) {
return (
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM ||
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM ||
Expand All @@ -109,6 +125,10 @@ function isChannelLogMemberAction(reportAction: OnyxEntry<ReportAction>) {
);
}

function isInviteMemberAction(reportAction: OnyxEntry<ReportAction>) {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM;
}

function isReimbursementDeQueuedAction(reportAction: OnyxEntry<ReportAction>): boolean {
return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED;
}
Expand Down Expand Up @@ -639,6 +659,89 @@ function isNotifiableReportAction(reportAction: OnyxEntry<ReportAction>): boolea
return actions.includes(reportAction.actionName);
}

function getMemberChangeMessageElements(reportAction: OnyxEntry<ReportAction>): readonly MemberChangeMessageElement[] {
const isInviteAction = isInviteMemberAction(reportAction);

// Currently, we only render messages when members are invited
const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed');

const originalMessage = reportAction?.originalMessage as ChangeLog;
const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? [];
const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs(targetAccountIDs, 0);

const mentionElements = targetAccountIDs.map((accountID): MemberChangeMessageUserMentionElement => {
const personalDetail = personalDetails.find((personal) => personal.accountID === accountID);
const handleText = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail) ?? Localize.translateLocal('common.hidden');

return {
kind: 'userMention',
content: `@${handleText}`,
accountID,
};
});

const buildRoomElements = (): readonly MemberChangeMessageElement[] => {
const roomName = originalMessage?.roomName;

if (roomName) {
const preposition = isInviteAction ? ` ${Localize.translateLocal('workspace.invite.to')} ` : ` ${Localize.translateLocal('workspace.invite.from')} `;

if (originalMessage.reportID) {
return [
{
kind: 'text',
content: preposition,
},
{
kind: 'roomReference',
roomName,
roomID: originalMessage.reportID,
content: roomName,
},
];
}
}

return [];
};

return [
{
kind: 'text',
content: `${verb} `,
},
...Localize.formatMessageElementList(mentionElements),
...buildRoomElements(),
];
}

function getMemberChangeMessageFragment(reportAction: OnyxEntry<ReportAction>): Message {
const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction);
const html = messageElements
.map((messageElement) => {
switch (messageElement.kind) {
case 'userMention':
return `<mention-user accountID=${messageElement.accountID}></mention-user>`;
case 'roomReference':
return `<a href="${environmentURL}/r/${messageElement.roomID}" target="_blank">${messageElement.roomName}</a>`;
default:
return messageElement.content;
}
})
.join('');

return {
html: `<muted-text>${html}</muted-text>`,
text: reportAction?.message ? reportAction?.message[0].text : '',
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
};
}

function getMemberChangeMessagePlainText(reportAction: OnyxEntry<ReportAction>): string {
const messageElements = getMemberChangeMessageElements(reportAction);
return messageElements.map((element) => element.content).join('');
}

/**
* Helper method to determine if the provided accountID has made a request on the specified report.
*
Expand Down Expand Up @@ -701,7 +804,9 @@ export {
shouldReportActionBeVisibleAsLastAction,
hasRequestFromCurrentAccount,
getFirstVisibleReportActionID,
isChannelLogMemberAction,
isMemberChangeAction,
getMemberChangeMessageFragment,
getMemberChangeMessagePlainText,
isReimbursementDeQueuedAction,
};

Expand Down
41 changes: 1 addition & 40 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx';
import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon';
import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage';
import {Message, ReportActions} from '@src/types/onyx/ReportAction';
import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction';
import DeepValueOf from '@src/types/utils/DeepValueOf';
Expand Down Expand Up @@ -4174,44 +4174,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry<ReportAction>)
});
}

/**
* Return room channel log display message
*/
function getChannelLogMemberMessage(reportAction: OnyxEntry<ReportAction>): string {
const verb =
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
? 'invited'
: 'removed';

const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => {
const personalDetail = allPersonalDetails?.accountID;
const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden');
return `@${displayNameOrLogin}`;
});

const lastMention = mentions?.pop();
let message = '';

if (mentions?.length === 0) {
message = `${verb} ${lastMention}`;
} else if (mentions?.length === 1) {
message = `${verb} ${mentions?.[0]} and ${lastMention}`;
} else {
message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`;
}

const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? '';
if (roomName) {
const preposition =
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
? ' to'
: ' from';
message += `${preposition} ${roomName}`;
}

return message;
}

/**
* Checks if a report is a group chat.
*
Expand Down Expand Up @@ -4446,7 +4408,6 @@ export {
getReimbursementQueuedActionMessage,
getReimbursementDeQueuedActionMessage,
getPersonalDetailsForAccountID,
getChannelLogMemberMessage,
getRoom,
shouldDisableWelcomeMessage,
navigateToPrivateNotes,
Expand Down
10 changes: 5 additions & 5 deletions src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,17 +375,17 @@ function getOptionData(
const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? [];
const verb =
lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
? 'invited'
: 'removed';
const users = targetAccountIDs.length > 1 ? 'users' : 'user';
? Localize.translate(preferredLocale, 'workspace.invite.invited')
: Localize.translate(preferredLocale, 'workspace.invite.removed');
const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user');
result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`;

const roomName = lastAction?.originalMessage?.roomName ?? '';
if (roomName) {
const preposition =
lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM
? ' to'
: ' from';
? ` ${Localize.translate(preferredLocale, 'workspace.invite.to')}`
: ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`;
result.alternateText += `${preposition} ${roomName}`;
}
} else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ export default [
} else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) {
const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction);
Clipboard.setString(displayMessage);
} else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) {
const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction);
} else if (ReportActionsUtils.isMemberChangeAction(reportAction)) {
const logMessage = ReportActionsUtils.getMemberChangeMessagePlainText(reportAction);
Clipboard.setString(logMessage);
} else if (content) {
const parser = new ExpensiMark();
Expand Down
Loading

0 comments on commit 9b71f23

Please sign in to comment.