Make valid-i18n-keys rule strict and fix most exceptions
This commit is contained in:
		
					parent
					
						
							
								18a6da310f
							
						
					
				
			
			
				commit
				
					
						11cfcb4e32
					
				
			
		
					 36 changed files with 796 additions and 687 deletions
				
			
		| 
						 | 
				
			
			@ -14,9 +14,11 @@ const messagesCacheKey = hashSum.digest('hex');
 | 
			
		|||
 | 
			
		||||
function isI18nCall(node) {
 | 
			
		||||
  return (
 | 
			
		||||
    node.type === 'CallExpression' &&
 | 
			
		||||
    (node.type === 'CallExpression' &&
 | 
			
		||||
      node.callee.type === 'Identifier' &&
 | 
			
		||||
    node.callee.name === 'i18n'
 | 
			
		||||
      node.callee.name === 'i18n') ||
 | 
			
		||||
    (node.callee.type === 'MemberExpression' &&
 | 
			
		||||
      node.callee.property.name === 'i18n')
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -36,22 +38,9 @@ function valueToMessageKey(node) {
 | 
			
		|||
  if (isStringLiteral(node)) {
 | 
			
		||||
    return node.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (node.type !== 'TemplateLiteral') {
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  if (node.quasis.length === 1) {
 | 
			
		||||
    return node.quasis[0].value.cooked;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const parts = node.quasis.map(element => {
 | 
			
		||||
    return element.value.cooked;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return new RegExp(`^${parts.join('(.*)')}$`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getI18nCallMessageKey(node) {
 | 
			
		||||
  if (node.arguments.length < 1) {
 | 
			
		||||
    return null;
 | 
			
		||||
| 
						 | 
				
			
			@ -80,24 +69,11 @@ function getIntlElementMessageKey(node) {
 | 
			
		|||
 | 
			
		||||
  let value = idAttribute.value;
 | 
			
		||||
 | 
			
		||||
  if (value.type === 'JSXExpressionContainer') {
 | 
			
		||||
    value = value.expression;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return valueToMessageKey(value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isValidMessageKey(key) {
 | 
			
		||||
  if (typeof key === 'string') {
 | 
			
		||||
    if (Object.hasOwn(messages, key)) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  } else if (key instanceof RegExp) {
 | 
			
		||||
    if (messageKeys.some(k => key.test(k))) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
  return Object.hasOwn(messages, key);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,27 +27,68 @@ ruleTester.run('valid-i18n-keys', rule, {
 | 
			
		|||
      options: [{ messagesCacheKey }],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: 'i18n(`AddCaptionModal__${title}`)',
 | 
			
		||||
      code: `window.i18n("AddCaptionModal__title")`,
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: `let jsx = <Intl id="AddCaptionModal__title"/>`,
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  invalid: [
 | 
			
		||||
    {
 | 
			
		||||
      code: 'i18n(`AddCaptionModal__${title}`)',
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
      errors: [
 | 
			
		||||
        {
 | 
			
		||||
          message: "i18n()'s first argument should always be a literal string",
 | 
			
		||||
          type: 'CallExpression',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: 'window.i18n(`AddCaptionModal__${title}`)',
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
      errors: [
 | 
			
		||||
        {
 | 
			
		||||
          message: "i18n()'s first argument should always be a literal string",
 | 
			
		||||
          type: 'CallExpression',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: `let jsx = <Intl id={"AddCaptionModal__title"}/>`,
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
      errors: [
 | 
			
		||||
        {
 | 
			
		||||
          message:
 | 
			
		||||
            "<Intl> must always be provided an 'id' attribute with a literal string",
 | 
			
		||||
          type: 'JSXOpeningElement',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: 'let jsx = <Intl id={`AddCaptionModal__title`}/>',
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
      errors: [
 | 
			
		||||
        {
 | 
			
		||||
          message:
 | 
			
		||||
            "<Intl> must always be provided an 'id' attribute with a literal string",
 | 
			
		||||
          type: 'JSXOpeningElement',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: 'let jsx = <Intl id={`AddCaptionModal__${title}`}/>',
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
      errors: [
 | 
			
		||||
        {
 | 
			
		||||
          message:
 | 
			
		||||
            "<Intl> must always be provided an 'id' attribute with a literal string",
 | 
			
		||||
          type: 'JSXOpeningElement',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
  invalid: [
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      code: `i18n("THIS_KEY_SHOULD_NEVER_EXIST")`,
 | 
			
		||||
      options: [{ messagesCacheKey }],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -604,19 +604,23 @@ export function Headers(): JSX.Element {
 | 
			
		|||
      rows={[
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'conversationsHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('conversationsHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'messagesHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('messagesHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
 | 
			
		||||
        },
 | 
			
		||||
      ]}
 | 
			
		||||
    />
 | 
			
		||||
| 
						 | 
				
			
			@ -629,7 +633,8 @@ export function FindByPhoneNumber(): JSX.Element {
 | 
			
		|||
      rows={[
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.StartNewConversation,
 | 
			
		||||
| 
						 | 
				
			
			@ -673,7 +678,8 @@ export function FindByUsername(): JSX.Element {
 | 
			
		|||
      rows={[
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.UsernameSearchResult,
 | 
			
		||||
| 
						 | 
				
			
			@ -745,7 +751,8 @@ export function KitchenSink(): JSX.Element {
 | 
			
		|||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'contactsHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('contactsHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Contact,
 | 
			
		||||
| 
						 | 
				
			
			@ -753,7 +760,8 @@ export function KitchenSink(): JSX.Element {
 | 
			
		|||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'messagesHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('messagesHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Conversation,
 | 
			
		||||
| 
						 | 
				
			
			@ -765,7 +773,8 @@ export function KitchenSink(): JSX.Element {
 | 
			
		|||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: RowType.UsernameSearchResult,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -103,9 +103,17 @@ type MessageRowType = {
 | 
			
		|||
 | 
			
		||||
type HeaderRowType = {
 | 
			
		||||
  type: RowType.Header;
 | 
			
		||||
  i18nKey: string;
 | 
			
		||||
  getHeaderText: (i18n: LocalizerType) => string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Exported for tests across multiple files
 | 
			
		||||
export function _testHeaderText(row: Row | void): string | null {
 | 
			
		||||
  if (row?.type === RowType.Header) {
 | 
			
		||||
    return row.getHeaderText(((key: string) => key) as LocalizerType);
 | 
			
		||||
  }
 | 
			
		||||
  return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type SearchResultsLoadingFakeHeaderType = {
 | 
			
		||||
  type: RowType.SearchResultsLoadingFakeHeader;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -375,18 +383,18 @@ export function ConversationList({
 | 
			
		|||
            />
 | 
			
		||||
          );
 | 
			
		||||
          break;
 | 
			
		||||
        case RowType.Header:
 | 
			
		||||
        case RowType.Header: {
 | 
			
		||||
          const headerText = row.getHeaderText(i18n);
 | 
			
		||||
          result = (
 | 
			
		||||
            <div
 | 
			
		||||
              className="module-conversation-list__item--header"
 | 
			
		||||
              // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
              aria-label={i18n(row.i18nKey)}
 | 
			
		||||
              aria-label={headerText}
 | 
			
		||||
            >
 | 
			
		||||
              {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
              {i18n(row.i18nKey)}
 | 
			
		||||
              {headerText}
 | 
			
		||||
            </div>
 | 
			
		||||
          );
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        case RowType.MessageSearchResult:
 | 
			
		||||
          result = <>{renderMessageSearchResult?.(row.messageId)}</>;
 | 
			
		||||
          break;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ import type { ConversationType } from '../state/ducks/conversations';
 | 
			
		|||
import type { PreferredBadgeSelectorType } from '../state/selectors/badges';
 | 
			
		||||
import { GroupDialog } from './GroupDialog';
 | 
			
		||||
import { sortByTitle } from '../util/sortByTitle';
 | 
			
		||||
import { missingCaseError } from '../util';
 | 
			
		||||
 | 
			
		||||
export type DataPropsType = {
 | 
			
		||||
  conversationId: string;
 | 
			
		||||
| 
						 | 
				
			
			@ -70,8 +71,6 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
 | 
			
		|||
    const keepHistory = hasMigrated
 | 
			
		||||
      ? i18n('GroupV1--Migration--info--keep-history')
 | 
			
		||||
      : i18n('GroupV1--Migration--migrate--keep-history');
 | 
			
		||||
    const migrationKey = hasMigrated ? 'after' : 'before';
 | 
			
		||||
    const droppedMembersKey = `GroupV1--Migration--info--removed--${migrationKey}`;
 | 
			
		||||
 | 
			
		||||
    let primaryButtonText: string;
 | 
			
		||||
    let onClickPrimaryButton: () => void;
 | 
			
		||||
| 
						 | 
				
			
			@ -116,14 +115,16 @@ export const GroupV1MigrationDialog: React.FunctionComponent<PropsType> =
 | 
			
		|||
              getPreferredBadge,
 | 
			
		||||
              i18n,
 | 
			
		||||
              members: invitedMembers,
 | 
			
		||||
              prefix: 'GroupV1--Migration--info--invited',
 | 
			
		||||
              hasMigrated,
 | 
			
		||||
              kind: 'invited',
 | 
			
		||||
              theme,
 | 
			
		||||
            })}
 | 
			
		||||
            {renderMembers({
 | 
			
		||||
              getPreferredBadge,
 | 
			
		||||
              i18n,
 | 
			
		||||
              members: droppedMembers,
 | 
			
		||||
              prefix: droppedMembersKey,
 | 
			
		||||
              hasMigrated,
 | 
			
		||||
              kind: 'dropped',
 | 
			
		||||
              theme,
 | 
			
		||||
            })}
 | 
			
		||||
          </>
 | 
			
		||||
| 
						 | 
				
			
			@ -136,26 +137,49 @@ function renderMembers({
 | 
			
		|||
  getPreferredBadge,
 | 
			
		||||
  i18n,
 | 
			
		||||
  members,
 | 
			
		||||
  prefix,
 | 
			
		||||
  hasMigrated,
 | 
			
		||||
  kind,
 | 
			
		||||
  theme,
 | 
			
		||||
}: Readonly<{
 | 
			
		||||
  getPreferredBadge: PreferredBadgeSelectorType;
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
  members: Array<ConversationType>;
 | 
			
		||||
  prefix: string;
 | 
			
		||||
  hasMigrated: boolean;
 | 
			
		||||
  kind: 'invited' | 'dropped';
 | 
			
		||||
  theme: ThemeType;
 | 
			
		||||
}>): React.ReactNode {
 | 
			
		||||
  if (!members.length) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const postfix = members.length === 1 ? '--one' : '--many';
 | 
			
		||||
  const key = `${prefix}${postfix}`;
 | 
			
		||||
  let text: string;
 | 
			
		||||
  switch (kind) {
 | 
			
		||||
    case 'invited':
 | 
			
		||||
      text =
 | 
			
		||||
        members.length === 1
 | 
			
		||||
          ? i18n('GroupV1--Migration--info--invited--one')
 | 
			
		||||
          : i18n('GroupV1--Migration--info--invited--many');
 | 
			
		||||
      break;
 | 
			
		||||
    case 'dropped':
 | 
			
		||||
      if (hasMigrated) {
 | 
			
		||||
        text =
 | 
			
		||||
          members.length === 1
 | 
			
		||||
            ? i18n('GroupV1--Migration--info--removed--before--one')
 | 
			
		||||
            : i18n('GroupV1--Migration--info--removed--before--many');
 | 
			
		||||
      } else {
 | 
			
		||||
        text =
 | 
			
		||||
          members.length === 1
 | 
			
		||||
            ? i18n('GroupV1--Migration--info--removed--after--one')
 | 
			
		||||
            : i18n('GroupV1--Migration--info--removed--after--many');
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      throw missingCaseError(kind);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
      <GroupDialog.Paragraph>{i18n(key)}</GroupDialog.Paragraph>
 | 
			
		||||
      <GroupDialog.Paragraph>{text}</GroupDialog.Paragraph>
 | 
			
		||||
      <GroupDialog.Contacts
 | 
			
		||||
        contacts={sortByTitle(members)}
 | 
			
		||||
        getPreferredBadge={getPreferredBadge}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,24 +71,29 @@ export class Intl extends React.Component<Props> {
 | 
			
		|||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | 
			
		||||
  public override render() {
 | 
			
		||||
    const { components, id, i18n, renderText = defaultRenderText } = this.props;
 | 
			
		||||
    const {
 | 
			
		||||
      components,
 | 
			
		||||
      id,
 | 
			
		||||
      // Indirection for linter/migration tooling
 | 
			
		||||
      i18n: localizer,
 | 
			
		||||
      renderText = defaultRenderText,
 | 
			
		||||
    } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (!id) {
 | 
			
		||||
      log.error('Error: Intl id prop not provided');
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!i18n.isLegacyFormat(id)) {
 | 
			
		||||
    if (!localizer.isLegacyFormat(id)) {
 | 
			
		||||
      strictAssert(
 | 
			
		||||
        !Array.isArray(components),
 | 
			
		||||
        `components cannot be an array for ICU message ${id}`
 | 
			
		||||
      );
 | 
			
		||||
      const intl = i18n.getIntl();
 | 
			
		||||
      const intl = localizer.getIntl();
 | 
			
		||||
      return intl.formatMessage({ id }, components);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
    const text = i18n(id);
 | 
			
		||||
    const text = localizer(id);
 | 
			
		||||
    const results: Array<
 | 
			
		||||
      string | JSX.Element | Array<string | JSX.Element> | null
 | 
			
		||||
    > = [];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -339,7 +339,7 @@ export function SendStoryModal({
 | 
			
		|||
        <EditMyStoryPrivacy
 | 
			
		||||
          hasDisclaimerAbove
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          learnMore="SendStoryModal__privacy-disclaimer"
 | 
			
		||||
          kind="privacy"
 | 
			
		||||
          myStories={stagedMyStories}
 | 
			
		||||
          signalConnectionsCount={signalConnections.length}
 | 
			
		||||
          onClickExclude={() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -627,7 +627,7 @@ export function DistributionListSettingsModal({
 | 
			
		|||
      {isMyStory && (
 | 
			
		||||
        <EditMyStoryPrivacy
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          learnMore="StoriesSettings__mine__disclaimer"
 | 
			
		||||
          kind="mine"
 | 
			
		||||
          myStories={listToEdit}
 | 
			
		||||
          onClickExclude={() => {
 | 
			
		||||
            setPage(Page.HideStoryFrom);
 | 
			
		||||
| 
						 | 
				
			
			@ -791,7 +791,7 @@ function CheckboxRender({
 | 
			
		|||
type EditMyStoryPrivacyPropsType = {
 | 
			
		||||
  hasDisclaimerAbove?: boolean;
 | 
			
		||||
  i18n: LocalizerType;
 | 
			
		||||
  learnMore: string;
 | 
			
		||||
  kind: 'privacy' | 'mine';
 | 
			
		||||
  myStories: StoryDistributionListWithMembersDataType;
 | 
			
		||||
  onClickExclude: () => unknown;
 | 
			
		||||
  onClickOnlyShareWith: () => unknown;
 | 
			
		||||
| 
						 | 
				
			
			@ -805,7 +805,7 @@ type EditMyStoryPrivacyPropsType = {
 | 
			
		|||
export function EditMyStoryPrivacy({
 | 
			
		||||
  hasDisclaimerAbove,
 | 
			
		||||
  i18n,
 | 
			
		||||
  learnMore,
 | 
			
		||||
  kind,
 | 
			
		||||
  myStories,
 | 
			
		||||
  onClickExclude,
 | 
			
		||||
  onClickOnlyShareWith,
 | 
			
		||||
| 
						 | 
				
			
			@ -814,12 +814,7 @@ export function EditMyStoryPrivacy({
 | 
			
		|||
  toggleSignalConnectionsModal,
 | 
			
		||||
  signalConnectionsCount,
 | 
			
		||||
}: EditMyStoryPrivacyPropsType): JSX.Element {
 | 
			
		||||
  const disclaimerElement = (
 | 
			
		||||
    <div className="StoriesSettingsModal__disclaimer">
 | 
			
		||||
      {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
      <Intl
 | 
			
		||||
        components={{
 | 
			
		||||
          learnMore: (
 | 
			
		||||
  const learnMore = (
 | 
			
		||||
    <button
 | 
			
		||||
      className="StoriesSettingsModal__disclaimer__learn-more"
 | 
			
		||||
      onClick={toggleSignalConnectionsModal}
 | 
			
		||||
| 
						 | 
				
			
			@ -827,11 +822,22 @@ export function EditMyStoryPrivacy({
 | 
			
		|||
    >
 | 
			
		||||
      {i18n('StoriesSettings__mine__disclaimer--learn-more')}
 | 
			
		||||
    </button>
 | 
			
		||||
          ),
 | 
			
		||||
        }}
 | 
			
		||||
  );
 | 
			
		||||
  const disclaimerElement = (
 | 
			
		||||
    <div className="StoriesSettingsModal__disclaimer">
 | 
			
		||||
      {kind === 'mine' ? (
 | 
			
		||||
        <Intl
 | 
			
		||||
          components={{ learnMore }}
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
        id={learnMore}
 | 
			
		||||
          id="StoriesSettings__mine__disclaimer"
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Intl
 | 
			
		||||
          components={{ learnMore }}
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          id="SendStoryModal__privacy-disclaimer"
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,10 +40,9 @@ export function ToastManager({
 | 
			
		|||
  if (toastType === ToastType.AddingUserToGroup) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Toast onClose={hideToast} timeout={SHORT_TIMEOUT}>
 | 
			
		||||
        {i18n(
 | 
			
		||||
          'AddUserToAnotherGroupModal__toast--adding-user-to-group',
 | 
			
		||||
          toast.parameters
 | 
			
		||||
        )}
 | 
			
		||||
        {i18n('AddUserToAnotherGroupModal__toast--adding-user-to-group', {
 | 
			
		||||
          ...toast.parameters,
 | 
			
		||||
        })}
 | 
			
		||||
      </Toast>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +106,9 @@ export function ToastManager({
 | 
			
		|||
  if (toastType === ToastType.CannotStartGroupCall) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Toast onClose={hideToast}>
 | 
			
		||||
        {i18n('GroupV2--cannot-start-group-call', toast.parameters)}
 | 
			
		||||
        {i18n('GroupV2--cannot-start-group-call', {
 | 
			
		||||
          ...toast.parameters,
 | 
			
		||||
        })}
 | 
			
		||||
      </Toast>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -344,10 +345,9 @@ export function ToastManager({
 | 
			
		|||
  if (toastType === ToastType.UserAddedToGroup) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Toast onClose={hideToast}>
 | 
			
		||||
        {i18n(
 | 
			
		||||
          'AddUserToAnotherGroupModal__toast--user-added-to-group',
 | 
			
		||||
          toast.parameters
 | 
			
		||||
        )}
 | 
			
		||||
        {i18n('AddUserToAnotherGroupModal__toast--user-added-to-group', {
 | 
			
		||||
          ...toast.parameters,
 | 
			
		||||
        })}
 | 
			
		||||
      </Toast>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import React from 'react';
 | 
			
		|||
import moment from 'moment';
 | 
			
		||||
 | 
			
		||||
import { Modal } from './Modal';
 | 
			
		||||
import type { IntlComponentsType } from './Intl';
 | 
			
		||||
import { Intl } from './Intl';
 | 
			
		||||
import { Emojify } from './conversation/Emojify';
 | 
			
		||||
import type { LocalizerType, RenderTextCallbackType } from '../types/Util';
 | 
			
		||||
| 
						 | 
				
			
			@ -19,61 +18,46 @@ export type PropsType = {
 | 
			
		|||
type ReleaseNotesType = {
 | 
			
		||||
  date: Date;
 | 
			
		||||
  version: string;
 | 
			
		||||
  features: Array<{ key: string; components: IntlComponentsType }>;
 | 
			
		||||
  features: Array<JSX.Element>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const renderText: RenderTextCallbackType = ({ key, text }) => (
 | 
			
		||||
  <Emojify key={key} text={text} />
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const releaseNotes: ReleaseNotesType = {
 | 
			
		||||
  date: new Date(window.getBuildCreation?.() || Date.now()),
 | 
			
		||||
  version: window.getVersion?.(),
 | 
			
		||||
  features: [
 | 
			
		||||
    {
 | 
			
		||||
      key: 'icu:WhatsNew__v6.12--0',
 | 
			
		||||
      components: {},
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      key: 'icu:WhatsNew__v6.12--1',
 | 
			
		||||
      components: {},
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function WhatsNewModal({
 | 
			
		||||
  i18n,
 | 
			
		||||
  hideWhatsNewModal,
 | 
			
		||||
}: PropsType): JSX.Element {
 | 
			
		||||
  let contentNode: ReactChild;
 | 
			
		||||
 | 
			
		||||
  if (releaseNotes.features.length === 1) {
 | 
			
		||||
    const { key, components } = releaseNotes.features[0];
 | 
			
		||||
    contentNode = (
 | 
			
		||||
      <p>
 | 
			
		||||
        {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
  const releaseNotes: ReleaseNotesType = {
 | 
			
		||||
    date: new Date(window.getBuildCreation?.() || Date.now()),
 | 
			
		||||
    version: window.getVersion?.(),
 | 
			
		||||
    features: [
 | 
			
		||||
      <Intl
 | 
			
		||||
        i18n={i18n}
 | 
			
		||||
          id={key}
 | 
			
		||||
        id="icu:WhatsNew__v6.12--0"
 | 
			
		||||
        renderText={renderText}
 | 
			
		||||
          components={components}
 | 
			
		||||
        />
 | 
			
		||||
      </p>
 | 
			
		||||
    );
 | 
			
		||||
        components={{}}
 | 
			
		||||
      />,
 | 
			
		||||
      <Intl
 | 
			
		||||
        i18n={i18n}
 | 
			
		||||
        id="icu:WhatsNew__v6.12--1"
 | 
			
		||||
        renderText={renderText}
 | 
			
		||||
        components={{}}
 | 
			
		||||
      />,
 | 
			
		||||
    ],
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (releaseNotes.features.length === 1) {
 | 
			
		||||
    contentNode = <p>{releaseNotes.features[0]}</p>;
 | 
			
		||||
  } else {
 | 
			
		||||
    contentNode = (
 | 
			
		||||
      <ul>
 | 
			
		||||
        {releaseNotes.features.map(({ key, components }) => (
 | 
			
		||||
          <li key={key}>
 | 
			
		||||
            {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id={key}
 | 
			
		||||
              renderText={renderText}
 | 
			
		||||
              components={components}
 | 
			
		||||
            />
 | 
			
		||||
          </li>
 | 
			
		||||
        ))}
 | 
			
		||||
        {releaseNotes.features.map(element => {
 | 
			
		||||
          return <li key={element.props.id}>{element}</li>;
 | 
			
		||||
        })}
 | 
			
		||||
      </ul>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,16 +60,8 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
 | 
			
		|||
                i18n('GroupV1--Migration--invited--you')
 | 
			
		||||
              ) : (
 | 
			
		||||
                <>
 | 
			
		||||
                  {renderUsers(
 | 
			
		||||
                    invitedMembers,
 | 
			
		||||
                    i18n,
 | 
			
		||||
                    'GroupV1--Migration--invited'
 | 
			
		||||
                  )}
 | 
			
		||||
                  {renderUsers(
 | 
			
		||||
                    droppedMembers,
 | 
			
		||||
                    i18n,
 | 
			
		||||
                    'GroupV1--Migration--removed'
 | 
			
		||||
                  )}
 | 
			
		||||
                  {renderUsers(invitedMembers, i18n, 'invited')}
 | 
			
		||||
                  {renderUsers(droppedMembers, i18n, 'removed')}
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
            </p>
 | 
			
		||||
| 
						 | 
				
			
			@ -106,31 +98,52 @@ export function GroupV1Migration(props: PropsType): React.ReactElement {
 | 
			
		|||
function renderUsers(
 | 
			
		||||
  members: Array<ConversationType>,
 | 
			
		||||
  i18n: LocalizerType,
 | 
			
		||||
  keyPrefix: string
 | 
			
		||||
  kind: 'invited' | 'removed'
 | 
			
		||||
): React.ReactElement | null {
 | 
			
		||||
  if (!members || members.length === 0) {
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (members.length === 1) {
 | 
			
		||||
    const contact = <ContactName title={members[0].title} />;
 | 
			
		||||
    return (
 | 
			
		||||
      <p>
 | 
			
		||||
        {kind === 'invited' && (
 | 
			
		||||
          <Intl
 | 
			
		||||
            i18n={i18n}
 | 
			
		||||
          id={`${keyPrefix}--one`}
 | 
			
		||||
          components={{
 | 
			
		||||
            contact: <ContactName title={members[0].title} />,
 | 
			
		||||
          }}
 | 
			
		||||
            id="GroupV1--Migration--invited--one"
 | 
			
		||||
            components={{ contact }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {kind === 'removed' && (
 | 
			
		||||
          <Intl
 | 
			
		||||
            i18n={i18n}
 | 
			
		||||
            id="GroupV1--Migration--removed--one"
 | 
			
		||||
            components={{ contact }}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </p>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const count = members.length.toString();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <p>
 | 
			
		||||
      {i18n(`${keyPrefix}--many`, {
 | 
			
		||||
        count: members.length.toString(),
 | 
			
		||||
      })}
 | 
			
		||||
      {kind === 'invited' && members.length > 1 && (
 | 
			
		||||
        <Intl
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          id="GroupV1--Migration--invited--many"
 | 
			
		||||
          components={{ count }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {kind === 'removed' && members.length > 1 && (
 | 
			
		||||
        <Intl
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          id="GroupV1--Migration--removed--many"
 | 
			
		||||
          components={{ count }}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    </p>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -40,6 +40,26 @@ export function MandatoryProfileSharingActions({
 | 
			
		|||
}: Props): JSX.Element {
 | 
			
		||||
  const [mrState, setMrState] = React.useState(MessageRequestState.default);
 | 
			
		||||
 | 
			
		||||
  const firstNameContact = (
 | 
			
		||||
    <strong
 | 
			
		||||
      key="name"
 | 
			
		||||
      className="module-message-request-actions__message__name"
 | 
			
		||||
    >
 | 
			
		||||
      <ContactName firstName={firstName} title={title} preferFirstName />
 | 
			
		||||
    </strong>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const learnMore = (
 | 
			
		||||
    <a
 | 
			
		||||
      href="https://support.signal.org/hc/articles/360007459591"
 | 
			
		||||
      target="_blank"
 | 
			
		||||
      rel="noreferrer"
 | 
			
		||||
      className="module-message-request-actions__message__learn-more"
 | 
			
		||||
    >
 | 
			
		||||
      {i18n('MessageRequests--learn-more')}
 | 
			
		||||
    </a>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {mrState !== MessageRequestState.default ? (
 | 
			
		||||
| 
						 | 
				
			
			@ -62,34 +82,19 @@ export function MandatoryProfileSharingActions({
 | 
			
		|||
      ) : null}
 | 
			
		||||
      <div className="module-message-request-actions">
 | 
			
		||||
        <p className="module-message-request-actions__message">
 | 
			
		||||
          {conversationType === 'direct' ? (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
            id={`MessageRequests--profile-sharing--${conversationType}`}
 | 
			
		||||
            components={{
 | 
			
		||||
              firstName: (
 | 
			
		||||
                <strong
 | 
			
		||||
                  key="name"
 | 
			
		||||
                  className="module-message-request-actions__message__name"
 | 
			
		||||
                >
 | 
			
		||||
                  <ContactName
 | 
			
		||||
                    firstName={firstName}
 | 
			
		||||
                    title={title}
 | 
			
		||||
                    preferFirstName
 | 
			
		||||
              id="MessageRequests--profile-sharing--direct"
 | 
			
		||||
              components={{ firstName: firstNameContact, learnMore }}
 | 
			
		||||
            />
 | 
			
		||||
                </strong>
 | 
			
		||||
              ),
 | 
			
		||||
              learnMore: (
 | 
			
		||||
                <a
 | 
			
		||||
                  href="https://support.signal.org/hc/articles/360007459591"
 | 
			
		||||
                  target="_blank"
 | 
			
		||||
                  rel="noreferrer"
 | 
			
		||||
                  className="module-message-request-actions__message__learn-more"
 | 
			
		||||
                >
 | 
			
		||||
                  {i18n('MessageRequests--learn-more')}
 | 
			
		||||
                </a>
 | 
			
		||||
              ),
 | 
			
		||||
            }}
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id="MessageRequests--profile-sharing--group"
 | 
			
		||||
              components={{ firstName: firstNameContact, learnMore }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </p>
 | 
			
		||||
        <div className="module-message-request-actions__buttons">
 | 
			
		||||
          <Button
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1251,7 +1251,10 @@ export class Message extends React.PureComponent<Props, State> {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    if (giftBadge.state === GiftBadgeStates.Unopened) {
 | 
			
		||||
      const description = i18n(`icu:message--donation--unopened--${direction}`);
 | 
			
		||||
      const description =
 | 
			
		||||
        direction === 'incoming'
 | 
			
		||||
          ? i18n('icu:message--donation--unopened--incoming')
 | 
			
		||||
          : i18n('icu:message--donation--unopened--outgoing');
 | 
			
		||||
      const isRTL = getDirection(description) === 'rtl';
 | 
			
		||||
      const { metadataWidth } = this.state;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1931,26 +1934,23 @@ export class Message extends React.PureComponent<Props, State> {
 | 
			
		|||
      isTapToViewError,
 | 
			
		||||
    } = this.props;
 | 
			
		||||
 | 
			
		||||
    const incomingString = isTapToViewExpired
 | 
			
		||||
      ? i18n('Message--tap-to-view-expired')
 | 
			
		||||
      : i18n(
 | 
			
		||||
          `Message--tap-to-view--incoming${
 | 
			
		||||
            isVideo(attachments) ? '-video' : ''
 | 
			
		||||
          }`
 | 
			
		||||
        );
 | 
			
		||||
    const outgoingString = i18n('Message--tap-to-view--outgoing');
 | 
			
		||||
    const isDownloadPending = this.isAttachmentPending();
 | 
			
		||||
 | 
			
		||||
    if (isDownloadPending) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // eslint-disable-next-line no-nested-ternary
 | 
			
		||||
    return isTapToViewError
 | 
			
		||||
      ? i18n('incomingError')
 | 
			
		||||
      : direction === 'outgoing'
 | 
			
		||||
      ? outgoingString
 | 
			
		||||
      : incomingString;
 | 
			
		||||
    if (isTapToViewError) {
 | 
			
		||||
      return i18n('incomingError');
 | 
			
		||||
    }
 | 
			
		||||
    if (direction === 'outgoing') {
 | 
			
		||||
      return i18n('Message--tap-to-view--outgoing');
 | 
			
		||||
    }
 | 
			
		||||
    if (isTapToViewExpired) {
 | 
			
		||||
      return i18n('Message--tap-to-view-expired');
 | 
			
		||||
    }
 | 
			
		||||
    if (isVideo(attachments)) {
 | 
			
		||||
      return i18n('Message--tap-to-view--incoming-video');
 | 
			
		||||
    }
 | 
			
		||||
    return i18n('Message--tap-to-view--incoming');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public renderTapToView(): JSX.Element {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ import * as log from '../../logging/log';
 | 
			
		|||
import { formatDateTimeLong } from '../../util/timestamp';
 | 
			
		||||
import { DurationInSeconds } from '../../util/durations';
 | 
			
		||||
import { format as formatRelativeTime } from '../../util/expirationTimer';
 | 
			
		||||
import { missingCaseError } from '../../util';
 | 
			
		||||
 | 
			
		||||
export type Contact = Pick<
 | 
			
		||||
  ConversationType,
 | 
			
		||||
| 
						 | 
				
			
			@ -200,24 +201,49 @@ export class MessageDetail extends React.Component<Props> {
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private renderContactGroupHeaderText(
 | 
			
		||||
    sendStatus: undefined | SendStatus
 | 
			
		||||
  ): string {
 | 
			
		||||
    const { i18n } = this.props;
 | 
			
		||||
 | 
			
		||||
    if (sendStatus === undefined) {
 | 
			
		||||
      return i18n('from');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    switch (sendStatus) {
 | 
			
		||||
      case SendStatus.Failed:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Failed');
 | 
			
		||||
      case SendStatus.Pending:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Pending');
 | 
			
		||||
      case SendStatus.Sent:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Sent');
 | 
			
		||||
      case SendStatus.Delivered:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Delivered');
 | 
			
		||||
      case SendStatus.Read:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Read');
 | 
			
		||||
      case SendStatus.Viewed:
 | 
			
		||||
        return i18n('MessageDetailsHeader--Viewed');
 | 
			
		||||
      default:
 | 
			
		||||
        throw missingCaseError(sendStatus);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private renderContactGroup(
 | 
			
		||||
    sendStatus: undefined | SendStatus,
 | 
			
		||||
    contacts: undefined | ReadonlyArray<Contact>
 | 
			
		||||
  ): ReactNode {
 | 
			
		||||
    const { i18n } = this.props;
 | 
			
		||||
    if (!contacts || !contacts.length) {
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const i18nKey =
 | 
			
		||||
      sendStatus === undefined ? 'from' : `MessageDetailsHeader--${sendStatus}`;
 | 
			
		||||
 | 
			
		||||
    const sortedContacts = [...contacts].sort((a, b) =>
 | 
			
		||||
      contactSortCollator.compare(a.title, b.title)
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const headerText = this.renderContactGroupHeaderText(sendStatus);
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div key={i18nKey} className="module-message-detail__contact-group">
 | 
			
		||||
      <div key={headerText} className="module-message-detail__contact-group">
 | 
			
		||||
        <div
 | 
			
		||||
          className={classNames(
 | 
			
		||||
            'module-message-detail__contact-group__header',
 | 
			
		||||
| 
						 | 
				
			
			@ -225,8 +251,7 @@ export class MessageDetail extends React.Component<Props> {
 | 
			
		|||
              `module-message-detail__contact-group__header--${sendStatus}`
 | 
			
		||||
          )}
 | 
			
		||||
        >
 | 
			
		||||
          {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
          {i18n(i18nKey)}
 | 
			
		||||
          {headerText}
 | 
			
		||||
        </div>
 | 
			
		||||
        {sortedContacts.map(contact => this.renderContact(contact))}
 | 
			
		||||
      </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,6 +35,15 @@ export function MessageRequestActions({
 | 
			
		|||
}: Props): JSX.Element {
 | 
			
		||||
  const [mrState, setMrState] = React.useState(MessageRequestState.default);
 | 
			
		||||
 | 
			
		||||
  const name = (
 | 
			
		||||
    <strong
 | 
			
		||||
      key="name"
 | 
			
		||||
      className="module-message-request-actions__message__name"
 | 
			
		||||
    >
 | 
			
		||||
      <ContactName firstName={firstName} title={title} preferFirstName />
 | 
			
		||||
    </strong>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {mrState !== MessageRequestState.default ? (
 | 
			
		||||
| 
						 | 
				
			
			@ -53,26 +62,34 @@ export function MessageRequestActions({
 | 
			
		|||
      ) : null}
 | 
			
		||||
      <div className="module-message-request-actions">
 | 
			
		||||
        <p className="module-message-request-actions__message">
 | 
			
		||||
          {conversationType === 'direct' && isBlocked && (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
            id={`MessageRequests--message-${conversationType}${
 | 
			
		||||
              isBlocked ? '-blocked' : ''
 | 
			
		||||
            }`}
 | 
			
		||||
            components={{
 | 
			
		||||
              name: (
 | 
			
		||||
                <strong
 | 
			
		||||
                  key="name"
 | 
			
		||||
                  className="module-message-request-actions__message__name"
 | 
			
		||||
                >
 | 
			
		||||
                  <ContactName
 | 
			
		||||
                    firstName={firstName}
 | 
			
		||||
                    title={title}
 | 
			
		||||
                    preferFirstName
 | 
			
		||||
              id="MessageRequests--message-direct-blocked"
 | 
			
		||||
              components={{ name }}
 | 
			
		||||
            />
 | 
			
		||||
                </strong>
 | 
			
		||||
              ),
 | 
			
		||||
            }}
 | 
			
		||||
          )}
 | 
			
		||||
          {conversationType === 'direct' && !isBlocked && (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id="MessageRequests--message-direct"
 | 
			
		||||
              components={{ name }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {conversationType === 'group' && isBlocked && (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id="MessageRequests--message-group-blocked"
 | 
			
		||||
              components={{ name }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
          {conversationType === 'group' && !isBlocked && (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id="MessageRequests--message-group"
 | 
			
		||||
              components={{ name }}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </p>
 | 
			
		||||
        <div className="module-message-request-actions__buttons">
 | 
			
		||||
          <Button
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -132,13 +132,23 @@ export function MessageRequestActionsConfirmation({
 | 
			
		|||
          onChangeState(MessageRequestState.default);
 | 
			
		||||
        }}
 | 
			
		||||
        title={
 | 
			
		||||
          conversationType === 'direct' ? (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
            id={`MessageRequests--delete-${conversationType}-confirm-title`}
 | 
			
		||||
              id="MessageRequests--delete-direct-confirm-title"
 | 
			
		||||
              components={{
 | 
			
		||||
                title: <ContactName key="name" title={title} />,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Intl
 | 
			
		||||
              i18n={i18n}
 | 
			
		||||
              id="MessageRequests--delete-group-confirm-title"
 | 
			
		||||
              components={{
 | 
			
		||||
                title: <ContactName key="name" title={title} />,
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        actions={[
 | 
			
		||||
          {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -35,19 +35,7 @@ export function SafetyNumberNotification({
 | 
			
		|||
  i18n,
 | 
			
		||||
  toggleSafetyNumberModal,
 | 
			
		||||
}: Props): JSX.Element {
 | 
			
		||||
  const changeKey = isGroup
 | 
			
		||||
    ? 'safetyNumberChangedGroup'
 | 
			
		||||
    : 'safetyNumberChanged';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SystemMessage
 | 
			
		||||
      icon="safety-number"
 | 
			
		||||
      contents={
 | 
			
		||||
        // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
        <Intl
 | 
			
		||||
          id={changeKey}
 | 
			
		||||
          components={{
 | 
			
		||||
            name: (
 | 
			
		||||
  const name = (
 | 
			
		||||
    <span
 | 
			
		||||
      key="external-1"
 | 
			
		||||
      className="module-safety-number-notification__contact"
 | 
			
		||||
| 
						 | 
				
			
			@ -57,10 +45,20 @@ export function SafetyNumberNotification({
 | 
			
		|||
        module="module-safety-number-notification__contact"
 | 
			
		||||
      />
 | 
			
		||||
    </span>
 | 
			
		||||
            ),
 | 
			
		||||
          }}
 | 
			
		||||
  );
 | 
			
		||||
  return (
 | 
			
		||||
    <SystemMessage
 | 
			
		||||
      icon="safety-number"
 | 
			
		||||
      contents={
 | 
			
		||||
        isGroup ? (
 | 
			
		||||
          <Intl
 | 
			
		||||
            id="safetyNumberChangedGroup"
 | 
			
		||||
            components={{ name }}
 | 
			
		||||
            i18n={i18n}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Intl id="safetyNumberChanged" components={{ name }} i18n={i18n} />
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      button={
 | 
			
		||||
        <Button
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -41,51 +41,46 @@ export type Props = PropsData & PropsHousekeeping;
 | 
			
		|||
export function TimerNotification(props: Props): JSX.Element {
 | 
			
		||||
  const { disabled, i18n, title, type } = props;
 | 
			
		||||
 | 
			
		||||
  let changeKey: string;
 | 
			
		||||
  let timespan: string;
 | 
			
		||||
  if (props.disabled) {
 | 
			
		||||
    changeKey = 'disabledDisappearingMessages';
 | 
			
		||||
    timespan = ''; // Set to the empty string to satisfy types
 | 
			
		||||
  } else {
 | 
			
		||||
    changeKey = 'theyChangedTheTimer';
 | 
			
		||||
    timespan = expirationTimer.format(i18n, props.expireTimer);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const name = <ContactName key="external-1" title={title} />;
 | 
			
		||||
 | 
			
		||||
  let message: ReactNode;
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'fromOther':
 | 
			
		||||
      message = (
 | 
			
		||||
        // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
      message = props.disabled ? (
 | 
			
		||||
        <Intl
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          id={changeKey}
 | 
			
		||||
          components={{
 | 
			
		||||
            name: <ContactName key="external-1" title={title} />,
 | 
			
		||||
            time: timespan,
 | 
			
		||||
          }}
 | 
			
		||||
          id="disabledDisappearingMessages"
 | 
			
		||||
          components={{ name }}
 | 
			
		||||
        />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Intl
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
          id="theyChangedTheTimer"
 | 
			
		||||
          components={{ name, time: timespan }}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
      break;
 | 
			
		||||
    case 'fromMe':
 | 
			
		||||
      message = disabled
 | 
			
		||||
        ? i18n('youDisabledDisappearingMessages')
 | 
			
		||||
        : i18n('youChangedTheTimer', {
 | 
			
		||||
            time: timespan,
 | 
			
		||||
          });
 | 
			
		||||
        : i18n('youChangedTheTimer', { time: timespan });
 | 
			
		||||
      break;
 | 
			
		||||
    case 'fromSync':
 | 
			
		||||
      message = disabled
 | 
			
		||||
        ? i18n('disappearingMessagesDisabled')
 | 
			
		||||
        : i18n('timerSetOnSync', {
 | 
			
		||||
            time: timespan,
 | 
			
		||||
          });
 | 
			
		||||
        : i18n('timerSetOnSync', { time: timespan });
 | 
			
		||||
      break;
 | 
			
		||||
    case 'fromMember':
 | 
			
		||||
      message = disabled
 | 
			
		||||
        ? i18n('disappearingMessagesDisabledByMember')
 | 
			
		||||
        : i18n('timerSetByMember', {
 | 
			
		||||
            time: timespan,
 | 
			
		||||
          });
 | 
			
		||||
        : i18n('timerSetByMember', { time: timespan });
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      log.warn('TimerNotification: unsupported type provided:', type);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -30,42 +30,64 @@ type PropsHousekeeping = {
 | 
			
		|||
 | 
			
		||||
export type Props = PropsData & PropsHousekeeping;
 | 
			
		||||
 | 
			
		||||
export function UnsupportedMessage({
 | 
			
		||||
  canProcessNow,
 | 
			
		||||
  contact,
 | 
			
		||||
  i18n,
 | 
			
		||||
}: Props): JSX.Element {
 | 
			
		||||
function UnsupportedMessageContents({ canProcessNow, contact, i18n }: Props) {
 | 
			
		||||
  const { isMe } = contact;
 | 
			
		||||
 | 
			
		||||
  const otherStringId = canProcessNow
 | 
			
		||||
    ? 'Message--unsupported-message-ask-to-resend'
 | 
			
		||||
    : 'Message--unsupported-message';
 | 
			
		||||
  const meStringId = canProcessNow
 | 
			
		||||
    ? 'Message--from-me-unsupported-message-ask-to-resend'
 | 
			
		||||
    : 'Message--from-me-unsupported-message';
 | 
			
		||||
  const stringId = isMe ? meStringId : otherStringId;
 | 
			
		||||
  const icon = canProcessNow ? 'unsupported--can-process' : 'unsupported';
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <SystemMessage
 | 
			
		||||
      icon={icon}
 | 
			
		||||
      contents={
 | 
			
		||||
        // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
        <Intl
 | 
			
		||||
          id={stringId}
 | 
			
		||||
          components={{
 | 
			
		||||
            contact: (
 | 
			
		||||
              <span
 | 
			
		||||
                key="external-1"
 | 
			
		||||
                className="module-unsupported-message__contact"
 | 
			
		||||
              >
 | 
			
		||||
  const contactName = (
 | 
			
		||||
    <span key="external-1" className="module-unsupported-message__contact">
 | 
			
		||||
      <ContactName
 | 
			
		||||
        title={contact.title}
 | 
			
		||||
        module="module-unsupported-message__contact"
 | 
			
		||||
      />
 | 
			
		||||
    </span>
 | 
			
		||||
            ),
 | 
			
		||||
          }}
 | 
			
		||||
  );
 | 
			
		||||
  if (isMe) {
 | 
			
		||||
    if (canProcessNow) {
 | 
			
		||||
      return (
 | 
			
		||||
        <Intl
 | 
			
		||||
          id="Message--unsupported-message-ask-to-resend"
 | 
			
		||||
          components={{ contact: contactName }}
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
        />
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <Intl
 | 
			
		||||
        id="Message--from-me-unsupported-message"
 | 
			
		||||
        components={{ contact: contactName }}
 | 
			
		||||
        i18n={i18n}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  if (canProcessNow) {
 | 
			
		||||
    return (
 | 
			
		||||
      <Intl
 | 
			
		||||
        id="Message--from-me-unsupported-message-ask-to-resend"
 | 
			
		||||
        components={{ contact: contactName }}
 | 
			
		||||
        i18n={i18n}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    <Intl
 | 
			
		||||
      id="Message--from-me-unsupported-message"
 | 
			
		||||
      components={{ contact: contactName }}
 | 
			
		||||
      i18n={i18n}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function UnsupportedMessage({
 | 
			
		||||
  canProcessNow,
 | 
			
		||||
  contact,
 | 
			
		||||
  i18n,
 | 
			
		||||
}: Props): JSX.Element {
 | 
			
		||||
  return (
 | 
			
		||||
    <SystemMessage
 | 
			
		||||
      icon={canProcessNow ? 'unsupported--can-process' : 'unsupported'}
 | 
			
		||||
      contents={
 | 
			
		||||
        <UnsupportedMessageContents
 | 
			
		||||
          canProcessNow={canProcessNow}
 | 
			
		||||
          contact={contact}
 | 
			
		||||
          i18n={i18n}
 | 
			
		||||
        />
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,43 +25,41 @@ type PropsHousekeeping = {
 | 
			
		|||
export type Props = PropsData & PropsHousekeeping;
 | 
			
		||||
 | 
			
		||||
export class VerificationNotification extends React.Component<Props> {
 | 
			
		||||
  public getStringId(): string {
 | 
			
		||||
    const { isLocal, type } = this.props;
 | 
			
		||||
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'markVerified':
 | 
			
		||||
        return isLocal
 | 
			
		||||
          ? 'youMarkedAsVerified'
 | 
			
		||||
          : 'youMarkedAsVerifiedOtherDevice';
 | 
			
		||||
      case 'markNotVerified':
 | 
			
		||||
        return isLocal
 | 
			
		||||
          ? 'youMarkedAsNotVerified'
 | 
			
		||||
          : 'youMarkedAsNotVerifiedOtherDevice';
 | 
			
		||||
      default:
 | 
			
		||||
        throw missingCaseError(type);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public renderContents(): JSX.Element {
 | 
			
		||||
    const { contact, i18n } = this.props;
 | 
			
		||||
    const id = this.getStringId();
 | 
			
		||||
    const { contact, isLocal, type, i18n } = this.props;
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
      <Intl
 | 
			
		||||
        id={id}
 | 
			
		||||
        components={{
 | 
			
		||||
          name: (
 | 
			
		||||
    const name = (
 | 
			
		||||
      <ContactName
 | 
			
		||||
        key="external-1"
 | 
			
		||||
        title={contact.title}
 | 
			
		||||
        module="module-verification-notification__contact"
 | 
			
		||||
      />
 | 
			
		||||
          ),
 | 
			
		||||
        }}
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'markVerified':
 | 
			
		||||
        return isLocal ? (
 | 
			
		||||
          <Intl id="youMarkedAsVerified" components={{ name }} i18n={i18n} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Intl
 | 
			
		||||
            id="youMarkedAsVerifiedOtherDevice"
 | 
			
		||||
            components={{ name }}
 | 
			
		||||
            i18n={i18n}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      case 'markNotVerified':
 | 
			
		||||
        return isLocal ? (
 | 
			
		||||
          <Intl id="youMarkedAsNotVerified" components={{ name }} i18n={i18n} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <Intl
 | 
			
		||||
            id="youMarkedAsNotVerifiedOtherDevice"
 | 
			
		||||
            components={{ name }}
 | 
			
		||||
            i18n={i18n}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        throw missingCaseError(type);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public override render(): JSX.Element {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,6 @@
 | 
			
		|||
// Copyright 2021 Signal Messenger, LLC
 | 
			
		||||
// SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
/* eslint-disable local-rules/valid-i18n-keys */
 | 
			
		||||
 | 
			
		||||
import React, {
 | 
			
		||||
  useEffect,
 | 
			
		||||
  useMemo,
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +210,8 @@ export function ChooseGroupMembersModal({
 | 
			
		|||
      if (virtualIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'contactsHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('contactsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +249,8 @@ export function ChooseGroupMembersModal({
 | 
			
		|||
      if (virtualIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      if (virtualIndex === 1) {
 | 
			
		||||
| 
						 | 
				
			
			@ -268,7 +268,8 @@ export function ChooseGroupMembersModal({
 | 
			
		|||
      if (virtualIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          // eslint-disable-next-line @typescript-eslint/no-shadow
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      if (virtualIndex === 1) {
 | 
			
		||||
| 
						 | 
				
			
			@ -307,16 +308,18 @@ export function ChooseGroupMembersModal({
 | 
			
		|||
 | 
			
		||||
    let item;
 | 
			
		||||
    switch (row?.type) {
 | 
			
		||||
      case RowType.Header:
 | 
			
		||||
      case RowType.Header: {
 | 
			
		||||
        const headerText = row.getHeaderText(i18n);
 | 
			
		||||
        item = (
 | 
			
		||||
          <div
 | 
			
		||||
            className="module-conversation-list__item--header"
 | 
			
		||||
            aria-label={i18n(row.i18nKey)}
 | 
			
		||||
            aria-label={headerText}
 | 
			
		||||
          >
 | 
			
		||||
            {i18n(row.i18nKey)}
 | 
			
		||||
            {headerText}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case RowType.ContactCheckbox:
 | 
			
		||||
        item = (
 | 
			
		||||
          <ContactCheckbox
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -618,8 +618,7 @@ function ConversationDetailsCallButton({
 | 
			
		|||
      onClick={onClick}
 | 
			
		||||
      variant={ButtonVariant.Details}
 | 
			
		||||
    >
 | 
			
		||||
      {/* eslint-disable-next-line local-rules/valid-i18n-keys */}
 | 
			
		||||
      {i18n(type)}
 | 
			
		||||
      {type === 'audio' ? i18n('audio') : i18n('video')}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -71,11 +71,25 @@ function MediaSection({
 | 
			
		|||
    const first = section.mediaItems[0];
 | 
			
		||||
    const { message } = first;
 | 
			
		||||
    const date = moment(getMessageTimestamp(message));
 | 
			
		||||
    const header =
 | 
			
		||||
      section.type === 'yearMonth'
 | 
			
		||||
        ? date.format(MONTH_FORMAT)
 | 
			
		||||
        : // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
          i18n(section.type);
 | 
			
		||||
 | 
			
		||||
    function getHeader(): string {
 | 
			
		||||
      switch (section.type) {
 | 
			
		||||
        case 'yearMonth':
 | 
			
		||||
          return date.format(MONTH_FORMAT);
 | 
			
		||||
        case 'today':
 | 
			
		||||
          return i18n('today');
 | 
			
		||||
        case 'yesterday':
 | 
			
		||||
          return i18n('yesterday');
 | 
			
		||||
        case 'thisWeek':
 | 
			
		||||
          return i18n('thisWeek');
 | 
			
		||||
        case 'thisMonth':
 | 
			
		||||
          return i18n('thisMonth');
 | 
			
		||||
        default:
 | 
			
		||||
          throw missingCaseError(section);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const header = getHeader();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <AttachmentSection
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,6 +24,7 @@ import { Emoji } from './Emoji';
 | 
			
		|||
import { dataByCategory, search } from './lib';
 | 
			
		||||
import type { LocalizerType } from '../../types/Util';
 | 
			
		||||
import { isSingleGrapheme } from '../../util/grapheme';
 | 
			
		||||
import { missingCaseError } from '../../util';
 | 
			
		||||
 | 
			
		||||
export type EmojiPickDataType = {
 | 
			
		||||
  skinTone?: number;
 | 
			
		||||
| 
						 | 
				
			
			@ -61,7 +62,9 @@ const categories = [
 | 
			
		|||
  'object',
 | 
			
		||||
  'symbol',
 | 
			
		||||
  'flag',
 | 
			
		||||
];
 | 
			
		||||
] as const;
 | 
			
		||||
 | 
			
		||||
type Category = typeof categories[number];
 | 
			
		||||
 | 
			
		||||
export const EmojiPicker = React.memo(
 | 
			
		||||
  React.forwardRef<HTMLDivElement, Props>(
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +83,7 @@ export const EmojiPicker = React.memo(
 | 
			
		|||
      ref
 | 
			
		||||
    ) => {
 | 
			
		||||
      const [firstRecent] = React.useState(recentEmojis);
 | 
			
		||||
      const [selectedCategory, setSelectedCategory] = React.useState(
 | 
			
		||||
      const [selectedCategory, setSelectedCategory] = React.useState<Category>(
 | 
			
		||||
        categories[0]
 | 
			
		||||
      );
 | 
			
		||||
      const [searchMode, setSearchMode] = React.useState(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -277,7 +280,7 @@ export const EmojiPicker = React.memo(
 | 
			
		|||
 | 
			
		||||
          const { category } = e.currentTarget.dataset;
 | 
			
		||||
          if (category) {
 | 
			
		||||
            setSelectedCategory(category);
 | 
			
		||||
            setSelectedCategory(category as Category);
 | 
			
		||||
            setScrollToRow(catToRowOffsets[category]);
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
| 
						 | 
				
			
			@ -332,11 +335,36 @@ export const EmojiPicker = React.memo(
 | 
			
		|||
              findLast(catOffsetEntries, ([, row]) => rowStartIndex >= row) ||
 | 
			
		||||
              categories;
 | 
			
		||||
 | 
			
		||||
            setSelectedCategory(cat);
 | 
			
		||||
            setSelectedCategory(cat as Category);
 | 
			
		||||
          }, 10),
 | 
			
		||||
        [catOffsetEntries]
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      function getCategoryButtonLabel(category: Category): string {
 | 
			
		||||
        switch (category) {
 | 
			
		||||
          case 'recents':
 | 
			
		||||
            return i18n('EmojiPicker__button--recents');
 | 
			
		||||
          case 'emoji':
 | 
			
		||||
            return i18n('EmojiPicker__button--emoji');
 | 
			
		||||
          case 'animal':
 | 
			
		||||
            return i18n('EmojiPicker__button--animal');
 | 
			
		||||
          case 'food':
 | 
			
		||||
            return i18n('EmojiPicker__button--food');
 | 
			
		||||
          case 'activity':
 | 
			
		||||
            return i18n('EmojiPicker__button--activity');
 | 
			
		||||
          case 'travel':
 | 
			
		||||
            return i18n('EmojiPicker__button--travel');
 | 
			
		||||
          case 'object':
 | 
			
		||||
            return i18n('EmojiPicker__button--object');
 | 
			
		||||
          case 'symbol':
 | 
			
		||||
            return i18n('EmojiPicker__button--symbol');
 | 
			
		||||
          case 'flag':
 | 
			
		||||
            return i18n('EmojiPicker__button--flag');
 | 
			
		||||
          default:
 | 
			
		||||
            throw missingCaseError(category);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return (
 | 
			
		||||
        <FocusTrap
 | 
			
		||||
          focusTrapOptions={{
 | 
			
		||||
| 
						 | 
				
			
			@ -394,7 +422,7 @@ export const EmojiPicker = React.memo(
 | 
			
		|||
                          ? 'module-emoji-picker__button--selected'
 | 
			
		||||
                          : null
 | 
			
		||||
                      )}
 | 
			
		||||
                      aria-label={i18n(`EmojiPicker__button--${cat}`)}
 | 
			
		||||
                      aria-label={getCategoryButtonLabel(cat)}
 | 
			
		||||
                    />
 | 
			
		||||
                  )
 | 
			
		||||
                )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -314,7 +314,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'contactsHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('contactsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -342,7 +342,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      if (virtualRowIndex === 1) {
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +363,7 @@ export class LeftPaneChooseGroupMembersHelper extends LeftPaneHelper<LeftPaneCho
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      if (virtualRowIndex === 1) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -194,7 +194,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'contactsHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('contactsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -215,7 +215,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'groupsHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('groupsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -236,7 +236,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByUsernameHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('findByUsernameHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -258,7 +258,7 @@ export class LeftPaneComposeHelper extends LeftPaneHelper<LeftPaneComposePropsTy
 | 
			
		|||
      if (virtualRowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('findByPhoneNumberHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -150,12 +150,12 @@ export class LeftPaneInboxHelper extends LeftPaneHelper<LeftPaneInboxPropsType>
 | 
			
		|||
        case 0:
 | 
			
		||||
          return {
 | 
			
		||||
            type: RowType.Header,
 | 
			
		||||
            i18nKey: 'LeftPane--pinned',
 | 
			
		||||
            getHeaderText: i18n => i18n('LeftPane--pinned'),
 | 
			
		||||
          };
 | 
			
		||||
        case pinnedConversations.length + 1:
 | 
			
		||||
          return {
 | 
			
		||||
            type: RowType.Header,
 | 
			
		||||
            i18nKey: 'LeftPane--chats',
 | 
			
		||||
            getHeaderText: i18n => i18n('LeftPane--chats'),
 | 
			
		||||
          };
 | 
			
		||||
        case pinnedConversations.length + conversations.length + 2:
 | 
			
		||||
          if (archivedConversationsCount) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -229,7 +229,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
 | 
			
		|||
      if (rowIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'conversationsHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('conversationsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      assertDev(
 | 
			
		||||
| 
						 | 
				
			
			@ -250,7 +250,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
 | 
			
		|||
      if (localIndex === 0) {
 | 
			
		||||
        return {
 | 
			
		||||
          type: RowType.Header,
 | 
			
		||||
          i18nKey: 'contactsHeader',
 | 
			
		||||
          getHeaderText: i18n => i18n('contactsHeader'),
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
      assertDev(
 | 
			
		||||
| 
						 | 
				
			
			@ -274,7 +274,7 @@ export class LeftPaneSearchHelper extends LeftPaneHelper<LeftPaneSearchPropsType
 | 
			
		|||
    if (localIndex === 0) {
 | 
			
		||||
      return {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'messagesHeader',
 | 
			
		||||
        getHeaderText: i18n => i18n('messagesHeader'),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    assertDev(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -258,7 +258,7 @@ export class LeftPaneSetGroupMetadataHelper extends LeftPaneHelper<LeftPaneSetGr
 | 
			
		|||
    if (rowIndex === 0) {
 | 
			
		||||
      return {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'setGroupMetadata__members-header',
 | 
			
		||||
        getHeaderText: i18n => i18n('setGroupMetadata__members-header'),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,7 +67,18 @@ export function renderChangeDetail<T>(
 | 
			
		|||
  detail: GroupV2ChangeDetailType,
 | 
			
		||||
  options: RenderOptionsType<T>
 | 
			
		||||
): T | string | ReadonlyArray<T | string> {
 | 
			
		||||
  const { from, i18n, ourACI, ourPNI, renderContact, renderString } = options;
 | 
			
		||||
  const {
 | 
			
		||||
    from,
 | 
			
		||||
    i18n: localizer,
 | 
			
		||||
    ourACI,
 | 
			
		||||
    ourPNI,
 | 
			
		||||
    renderContact,
 | 
			
		||||
    renderString,
 | 
			
		||||
  } = options;
 | 
			
		||||
 | 
			
		||||
  function i18n(id: string, components?: ReplacementValuesType<T | string>) {
 | 
			
		||||
    return renderString(id, localizer, components);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const isOurUuid = (uuid?: UUIDStringType): boolean => {
 | 
			
		||||
    if (!uuid) {
 | 
			
		||||
| 
						 | 
				
			
			@ -79,88 +90,88 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
  if (detail.type === 'create') {
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--create--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--create--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--create--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--create--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--create--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--create--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'title') {
 | 
			
		||||
    const { newTitle } = detail;
 | 
			
		||||
 | 
			
		||||
    if (newTitle) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--title--change--you', i18n, { newTitle });
 | 
			
		||||
        return i18n('GroupV2--title--change--you', { newTitle });
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--title--change--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--title--change--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
          newTitle,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--title--change--unknown', i18n, {
 | 
			
		||||
      return i18n('GroupV2--title--change--unknown', {
 | 
			
		||||
        newTitle,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--title--remove--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--title--remove--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--title--remove--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--title--remove--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--title--remove--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--title--remove--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'avatar') {
 | 
			
		||||
    if (detail.removed) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--avatar--remove--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--avatar--remove--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--avatar--remove--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--avatar--remove--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--avatar--remove--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--avatar--remove--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--avatar--change--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--avatar--change--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--avatar--change--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--avatar--change--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--avatar--change--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--avatar--change--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'access-attributes') {
 | 
			
		||||
    const { newPrivilege } = detail;
 | 
			
		||||
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-attributes--admins--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-attributes--admins--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--access-attributes--admins--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--access-attributes--admins--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--access-attributes--admins--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--access-attributes--admins--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.MEMBER) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-attributes--all--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-attributes--all--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--access-attributes--all--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--access-attributes--all--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--access-attributes--all--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--access-attributes--all--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    log.warn(
 | 
			
		||||
      `access-attributes change type, privilege ${newPrivilege} is unknown`
 | 
			
		||||
| 
						 | 
				
			
			@ -172,25 +183,25 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-members--admins--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-members--admins--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--access-members--admins--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--access-members--admins--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--access-members--admins--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--access-members--admins--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.MEMBER) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-members--all--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-members--all--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--access-members--all--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--access-members--all--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--access-members--all--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--access-members--all--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    log.warn(
 | 
			
		||||
      `access-members change type, privilege ${newPrivilege} is unknown`
 | 
			
		||||
| 
						 | 
				
			
			@ -202,35 +213,29 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.ADMINISTRATOR) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-invite-link--enabled--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-invite-link--enabled--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--access-invite-link--enabled--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { adminName: renderContact(from) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--access-invite-link--enabled--unknown',
 | 
			
		||||
        i18n
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--access-invite-link--enabled--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (newPrivilege === AccessControlEnum.ANY) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--access-invite-link--disabled--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--access-invite-link--disabled--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--access-invite-link--disabled--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { adminName: renderContact(from) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--access-invite-link--disabled--unknown',
 | 
			
		||||
        i18n
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--access-invite-link--disabled--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    log.warn(
 | 
			
		||||
      `access-invite-link change type, privilege ${newPrivilege} is unknown`
 | 
			
		||||
| 
						 | 
				
			
			@ -243,27 +248,27 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreJoiner) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--member-add--you--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--member-add--you--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--member-add--you--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--member-add--you--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--member-add--you--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--member-add--you--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--member-add--other--you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add--other--you', {
 | 
			
		||||
        memberName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--member-add--other--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add--other--other', {
 | 
			
		||||
        adderName: renderContact(from),
 | 
			
		||||
        addeeName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--member-add--other--unknown', i18n, {
 | 
			
		||||
    return i18n('GroupV2--member-add--other--unknown', {
 | 
			
		||||
      memberName: renderContact(uuid),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -276,54 +281,51 @@ export function renderChangeDetail<T>(
 | 
			
		|||
      if (weAreJoiner) {
 | 
			
		||||
        // They can't be the same, no fromYou check here
 | 
			
		||||
        if (from) {
 | 
			
		||||
          return renderString('GroupV2--member-add--you--other', i18n, {
 | 
			
		||||
          return i18n('GroupV2--member-add--you--other', {
 | 
			
		||||
            memberName: renderContact(from),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        return renderString('GroupV2--member-add--you--unknown', i18n);
 | 
			
		||||
        return i18n('GroupV2--member-add--you--unknown');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--member-add--invited--you', i18n, {
 | 
			
		||||
        return i18n('GroupV2--member-add--invited--you', {
 | 
			
		||||
          inviteeName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--member-add--invited--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--member-add--invited--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
          inviteeName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--member-add--invited--unknown', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add--invited--unknown', {
 | 
			
		||||
        inviteeName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (weAreJoiner) {
 | 
			
		||||
      if (inviter) {
 | 
			
		||||
        return renderString('GroupV2--member-add--from-invite--you', i18n, {
 | 
			
		||||
        return i18n('GroupV2--member-add--from-invite--you', {
 | 
			
		||||
          inviterName: renderContact(inviter),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--member-add--from-invite--you-no-from',
 | 
			
		||||
        i18n
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--member-add--from-invite--you-no-from');
 | 
			
		||||
    }
 | 
			
		||||
    if (weAreInviter) {
 | 
			
		||||
      return renderString('GroupV2--member-add--from-invite--from-you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add--from-invite--from-you', {
 | 
			
		||||
        inviteeName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (inviter) {
 | 
			
		||||
      return renderString('GroupV2--member-add--from-invite--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add--from-invite--other', {
 | 
			
		||||
        inviteeName: renderContact(uuid),
 | 
			
		||||
        inviterName: renderContact(inviter),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString(
 | 
			
		||||
    return i18n(
 | 
			
		||||
      'GroupV2--member-add--from-invite--other-no-from',
 | 
			
		||||
      i18n,
 | 
			
		||||
 | 
			
		||||
      {
 | 
			
		||||
        inviteeName: renderContact(uuid),
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -333,10 +335,10 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    const { uuid } = detail;
 | 
			
		||||
 | 
			
		||||
    if (fromYou && isOurUuid(uuid)) {
 | 
			
		||||
      return renderString('GroupV2--member-add-from-link--you--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--member-add-from-link--you--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from && uuid === from) {
 | 
			
		||||
      return renderString('GroupV2--member-add-from-link--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-add-from-link--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -344,7 +346,7 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    // Note: this shouldn't happen, because we only capture 'add-from-link' status
 | 
			
		||||
    //   from group change events, which always have a sender.
 | 
			
		||||
    log.warn('member-add-from-link change type; we have no from!');
 | 
			
		||||
    return renderString('GroupV2--member-add--other--unknown', i18n, {
 | 
			
		||||
    return i18n('GroupV2--member-add--other--unknown', {
 | 
			
		||||
      memberName: renderContact(uuid),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -354,9 +356,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreJoiner) {
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--member-add-from-admin-approval--you--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { adminName: renderContact(from) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
| 
						 | 
				
			
			@ -366,23 +368,20 @@ export function renderChangeDetail<T>(
 | 
			
		|||
      log.warn(
 | 
			
		||||
        'member-add-from-admin-approval change type; we have no from, and we are joiner!'
 | 
			
		||||
      );
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--member-add-from-admin-approval--you--unknown',
 | 
			
		||||
        i18n
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--member-add-from-admin-approval--you--unknown');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--member-add-from-admin-approval--other--you',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { joinerName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--member-add-from-admin-approval--other--other',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
          joinerName: renderContact(uuid),
 | 
			
		||||
| 
						 | 
				
			
			@ -393,9 +392,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    // Note: this shouldn't happen, because we only capture 'add-from-admin-approval'
 | 
			
		||||
    //   status from group change events, which always have a sender.
 | 
			
		||||
    log.warn('member-add-from-admin-approval change type; we have no from');
 | 
			
		||||
    return renderString(
 | 
			
		||||
    return i18n(
 | 
			
		||||
      'GroupV2--member-add-from-admin-approval--other--unknown',
 | 
			
		||||
      i18n,
 | 
			
		||||
 | 
			
		||||
      { joinerName: renderContact(uuid) }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -405,33 +404,33 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreLeaver) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--member-remove--you--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--member-remove--you--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--member-remove--you--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--member-remove--you--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--member-remove--you--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--member-remove--you--unknown');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--member-remove--other--you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-remove--other--you', {
 | 
			
		||||
        memberName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from && from === uuid) {
 | 
			
		||||
      return renderString('GroupV2--member-remove--other--self', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-remove--other--self', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--member-remove--other--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--member-remove--other--other', {
 | 
			
		||||
        adminName: renderContact(from),
 | 
			
		||||
        memberName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--member-remove--other--unknown', i18n, {
 | 
			
		||||
    return i18n('GroupV2--member-remove--other--unknown', {
 | 
			
		||||
      memberName: renderContact(uuid),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -442,77 +441,59 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    if (newPrivilege === RoleEnum.ADMINISTRATOR) {
 | 
			
		||||
      if (weAreMember) {
 | 
			
		||||
        if (from) {
 | 
			
		||||
          return renderString(
 | 
			
		||||
          return i18n(
 | 
			
		||||
            'GroupV2--member-privilege--promote--you--other',
 | 
			
		||||
            i18n,
 | 
			
		||||
 | 
			
		||||
            { adminName: renderContact(from) }
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--member-privilege--promote--you--unknown',
 | 
			
		||||
          i18n
 | 
			
		||||
        );
 | 
			
		||||
        return i18n('GroupV2--member-privilege--promote--you--unknown');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--member-privilege--promote--other--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
          { memberName: renderContact(uuid) }
 | 
			
		||||
        );
 | 
			
		||||
        return i18n('GroupV2--member-privilege--promote--other--you', {
 | 
			
		||||
          memberName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--member-privilege--promote--other--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
          {
 | 
			
		||||
        return i18n('GroupV2--member-privilege--promote--other--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
          memberName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--member-privilege--promote--other--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
        { memberName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--member-privilege--promote--other--unknown', {
 | 
			
		||||
        memberName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (newPrivilege === RoleEnum.DEFAULT) {
 | 
			
		||||
      if (weAreMember) {
 | 
			
		||||
        if (from) {
 | 
			
		||||
          return renderString(
 | 
			
		||||
            'GroupV2--member-privilege--demote--you--other',
 | 
			
		||||
            i18n,
 | 
			
		||||
            { adminName: renderContact(from) }
 | 
			
		||||
          );
 | 
			
		||||
          return i18n('GroupV2--member-privilege--demote--you--other', {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--member-privilege--demote--you--unknown',
 | 
			
		||||
          i18n
 | 
			
		||||
        );
 | 
			
		||||
        return i18n('GroupV2--member-privilege--demote--you--unknown');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--member-privilege--demote--other--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
          { memberName: renderContact(uuid) }
 | 
			
		||||
        );
 | 
			
		||||
        return i18n('GroupV2--member-privilege--demote--other--you', {
 | 
			
		||||
          memberName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--member-privilege--demote--other--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
            memberName: renderContact(uuid),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--member-privilege--demote--other--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { memberName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -526,39 +507,39 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    const weAreInvited = isOurUuid(uuid);
 | 
			
		||||
    if (weAreInvited) {
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--pending-add--one--you--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--pending-add--one--you--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--pending-add--one--you--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--pending-add--one--you--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--pending-add--one--other--you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-add--one--other--you', {
 | 
			
		||||
        inviteeName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--pending-add--one--other--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-add--one--other--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--pending-add--one--other--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--pending-add--one--other--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'pending-add-many') {
 | 
			
		||||
    const { count } = detail;
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--pending-add--many--you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-add--many--you', {
 | 
			
		||||
        count: count.toString(),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--pending-add--many--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-add--many--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
        count: count.toString(),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--pending-add--many--unknown', i18n, {
 | 
			
		||||
    return i18n('GroupV2--pending-add--many--unknown', {
 | 
			
		||||
      count: count.toString(),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -571,91 +552,91 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreInviter) {
 | 
			
		||||
      if (sentByInvited) {
 | 
			
		||||
        return renderString('GroupV2--pending-remove--decline--you', i18n, {
 | 
			
		||||
        return i18n('GroupV2--pending-remove--decline--you', {
 | 
			
		||||
          inviteeName: renderContact(uuid),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from-you--one--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { inviteeName: renderContact(uuid) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from-you--one--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
            inviteeName: renderContact(uuid),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke-invite-from-you--one--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { inviteeName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (sentByInvited) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--pending-remove--decline--from-you', i18n);
 | 
			
		||||
        return i18n('GroupV2--pending-remove--decline--from-you');
 | 
			
		||||
      }
 | 
			
		||||
      if (inviter) {
 | 
			
		||||
        return renderString('GroupV2--pending-remove--decline--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--pending-remove--decline--other', {
 | 
			
		||||
          memberName: renderContact(inviter),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--pending-remove--decline--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--pending-remove--decline--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (inviter && sentByInviter) {
 | 
			
		||||
      if (weAreInvited) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-own--to-you',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { inviterName: renderContact(inviter) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke-own--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { inviterName: renderContact(inviter) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (inviter) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from--one--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { memberName: renderContact(inviter) }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from--one--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
            memberName: renderContact(inviter),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke-invite-from--one--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { memberName: renderContact(inviter) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--pending-remove--revoke--one--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--pending-remove--revoke--one--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--pending-remove--revoke--one--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-remove--revoke--one--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--pending-remove--revoke--one--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--pending-remove--revoke--one--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'pending-remove-many') {
 | 
			
		||||
    const { count, inviter } = detail;
 | 
			
		||||
| 
						 | 
				
			
			@ -663,33 +644,33 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreInviter) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from-you--many--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          { count: count.toString() }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from-you--many--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
            count: count.toString(),
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke-invite-from-you--many--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { count: count.toString() }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (inviter) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from--many--you',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            count: count.toString(),
 | 
			
		||||
            memberName: renderContact(inviter),
 | 
			
		||||
| 
						 | 
				
			
			@ -697,9 +678,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
        return i18n(
 | 
			
		||||
          'GroupV2--pending-remove--revoke-invite-from--many--other',
 | 
			
		||||
          i18n,
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            adminName: renderContact(from),
 | 
			
		||||
            count: count.toString(),
 | 
			
		||||
| 
						 | 
				
			
			@ -707,9 +688,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke-invite-from--many--unknown',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          count: count.toString(),
 | 
			
		||||
          memberName: renderContact(inviter),
 | 
			
		||||
| 
						 | 
				
			
			@ -717,23 +698,23 @@ export function renderChangeDetail<T>(
 | 
			
		|||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--pending-remove--revoke--many--you', i18n, {
 | 
			
		||||
      return i18n('GroupV2--pending-remove--revoke--many--you', {
 | 
			
		||||
        count: count.toString(),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--pending-remove--revoke--many--other',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
          count: count.toString(),
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return renderString(
 | 
			
		||||
    return i18n(
 | 
			
		||||
      'GroupV2--pending-remove--revoke--many--unknown',
 | 
			
		||||
      i18n,
 | 
			
		||||
 | 
			
		||||
      { count: count.toString() }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -742,9 +723,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
    const weAreJoiner = isOurUuid(uuid);
 | 
			
		||||
 | 
			
		||||
    if (weAreJoiner) {
 | 
			
		||||
      return renderString('GroupV2--admin-approval-add-one--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--admin-approval-add-one--you');
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--admin-approval-add-one--other', i18n, {
 | 
			
		||||
    return i18n('GroupV2--admin-approval-add-one--other', {
 | 
			
		||||
      joinerName: renderContact(uuid),
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -754,35 +735,29 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (weAreJoiner) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString(
 | 
			
		||||
          'GroupV2--admin-approval-remove-one--you--you',
 | 
			
		||||
          i18n
 | 
			
		||||
        );
 | 
			
		||||
        return i18n('GroupV2--admin-approval-remove-one--you--you');
 | 
			
		||||
      }
 | 
			
		||||
      return renderString(
 | 
			
		||||
        'GroupV2--admin-approval-remove-one--you--unknown',
 | 
			
		||||
        i18n
 | 
			
		||||
      );
 | 
			
		||||
      return i18n('GroupV2--admin-approval-remove-one--you--unknown');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--admin-approval-remove-one--other--you',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { joinerName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (from && from === uuid) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--admin-approval-remove-one--other--own',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        { joinerName: renderContact(uuid) }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString(
 | 
			
		||||
      return i18n(
 | 
			
		||||
        'GroupV2--admin-approval-remove-one--other--other',
 | 
			
		||||
        i18n,
 | 
			
		||||
 | 
			
		||||
        {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
          joinerName: renderContact(uuid),
 | 
			
		||||
| 
						 | 
				
			
			@ -792,9 +767,9 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    // We default to the user canceling their request, because it is far more likely that
 | 
			
		||||
    //   if an admin does the denial, we'll get a change event from them.
 | 
			
		||||
    return renderString(
 | 
			
		||||
    return i18n(
 | 
			
		||||
      'GroupV2--admin-approval-remove-one--other--own',
 | 
			
		||||
      i18n,
 | 
			
		||||
 | 
			
		||||
      { joinerName: renderContact(uuid) }
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
| 
						 | 
				
			
			@ -803,11 +778,11 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    let firstMessage: T | string;
 | 
			
		||||
    if (times === 1) {
 | 
			
		||||
      firstMessage = renderString('GroupV2--admin-approval-bounce--one', i18n, {
 | 
			
		||||
      firstMessage = i18n('GroupV2--admin-approval-bounce--one', {
 | 
			
		||||
        joinerName: renderContact(uuid),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      firstMessage = renderString('GroupV2--admin-approval-bounce', i18n, {
 | 
			
		||||
      firstMessage = i18n('GroupV2--admin-approval-bounce', {
 | 
			
		||||
        joinerName: renderContact(uuid),
 | 
			
		||||
        numberOfRequests: String(times),
 | 
			
		||||
      });
 | 
			
		||||
| 
						 | 
				
			
			@ -835,99 +810,99 @@ export function renderChangeDetail<T>(
 | 
			
		|||
 | 
			
		||||
    if (privilege === AccessControlEnum.ADMINISTRATOR) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--group-link-add--enabled--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--group-link-add--enabled--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--group-link-add--enabled--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--group-link-add--enabled--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--group-link-add--enabled--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--group-link-add--enabled--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    if (privilege === AccessControlEnum.ANY) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--group-link-add--disabled--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--group-link-add--disabled--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--group-link-add--disabled--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--group-link-add--disabled--other', {
 | 
			
		||||
          adminName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--group-link-add--disabled--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--group-link-add--disabled--unknown');
 | 
			
		||||
    }
 | 
			
		||||
    log.warn(`group-link-add change type, privilege ${privilege} is unknown`);
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'group-link-reset') {
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--group-link-reset--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--group-link-reset--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--group-link-reset--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--group-link-reset--other', {
 | 
			
		||||
        adminName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--group-link-reset--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--group-link-reset--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'group-link-remove') {
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--group-link-remove--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--group-link-remove--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--group-link-remove--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--group-link-remove--other', {
 | 
			
		||||
        adminName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--group-link-remove--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--group-link-remove--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'description') {
 | 
			
		||||
    if (detail.removed) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--description--remove--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--description--remove--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--description--remove--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--description--remove--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--description--remove--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--description--remove--unknown');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--description--change--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--description--change--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--description--change--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--description--change--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--description--change--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--description--change--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'announcements-only') {
 | 
			
		||||
    if (detail.announcementsOnly) {
 | 
			
		||||
      if (fromYou) {
 | 
			
		||||
        return renderString('GroupV2--announcements--admin--you', i18n);
 | 
			
		||||
        return i18n('GroupV2--announcements--admin--you');
 | 
			
		||||
      }
 | 
			
		||||
      if (from) {
 | 
			
		||||
        return renderString('GroupV2--announcements--admin--other', i18n, {
 | 
			
		||||
        return i18n('GroupV2--announcements--admin--other', {
 | 
			
		||||
          memberName: renderContact(from),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return renderString('GroupV2--announcements--admin--unknown', i18n);
 | 
			
		||||
      return i18n('GroupV2--announcements--admin--unknown');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (fromYou) {
 | 
			
		||||
      return renderString('GroupV2--announcements--member--you', i18n);
 | 
			
		||||
      return i18n('GroupV2--announcements--member--you');
 | 
			
		||||
    }
 | 
			
		||||
    if (from) {
 | 
			
		||||
      return renderString('GroupV2--announcements--member--other', i18n, {
 | 
			
		||||
      return i18n('GroupV2--announcements--member--other', {
 | 
			
		||||
        memberName: renderContact(from),
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return renderString('GroupV2--announcements--member--unknown', i18n);
 | 
			
		||||
    return i18n('GroupV2--announcements--member--unknown');
 | 
			
		||||
  }
 | 
			
		||||
  if (detail.type === 'summary') {
 | 
			
		||||
    return renderString('icu:GroupV2--summary', i18n);
 | 
			
		||||
    return i18n('icu:GroupV2--summary');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  throw missingCaseError(detail);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -511,7 +511,10 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
 | 
			
		|||
          key: string,
 | 
			
		||||
          _i18n: unknown,
 | 
			
		||||
          components: ReplacementValuesType<string> | undefined
 | 
			
		||||
        ) => window.i18n(key, components),
 | 
			
		||||
        ) => {
 | 
			
		||||
          // eslint-disable-next-line local-rules/valid-i18n-keys
 | 
			
		||||
          return window.i18n(key, components);
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return { text: changes.map(({ text }) => text).join(' ') };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
import { assert } from 'chai';
 | 
			
		||||
import * as sinon from 'sinon';
 | 
			
		||||
import { times } from 'lodash';
 | 
			
		||||
import { RowType } from '../../../components/ConversationList';
 | 
			
		||||
import { RowType, _testHeaderText } from '../../../components/ConversationList';
 | 
			
		||||
import { ContactCheckboxDisabledReason } from '../../../components/conversationList/ContactCheckbox';
 | 
			
		||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -117,10 +117,7 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
 | 
			
		|||
        selectedContacts: [candidateContacts[1]],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(0)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.ContactCheckbox,
 | 
			
		||||
        contact: candidateContacts[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -167,10 +164,10 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
 | 
			
		|||
        selectedContacts: [],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'findByPhoneNumberHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.PhoneNumberCheckbox,
 | 
			
		||||
        phoneNumber: {
 | 
			
		||||
| 
						 | 
				
			
			@ -192,10 +189,10 @@ describe('LeftPaneChooseGroupMembersHelper', () => {
 | 
			
		|||
        selectedContacts: [],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'findByUsernameHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'findByUsernameHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.UsernameCheckbox,
 | 
			
		||||
        username: 'signal',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 | 
			
		||||
import { assert } from 'chai';
 | 
			
		||||
import * as sinon from 'sinon';
 | 
			
		||||
import { RowType } from '../../../components/ConversationList';
 | 
			
		||||
import { RowType, _testHeaderText } from '../../../components/ConversationList';
 | 
			
		||||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
 | 
			
		||||
import {
 | 
			
		||||
  getDefaultConversation,
 | 
			
		||||
| 
						 | 
				
			
			@ -223,10 +223,7 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.CreateNewGroup,
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(1)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(2), {
 | 
			
		||||
        type: RowType.Contact,
 | 
			
		||||
        contact: composeContacts[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -258,11 +255,7 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.CreateNewGroup,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(1)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(2), {
 | 
			
		||||
        type: RowType.Contact,
 | 
			
		||||
        contact: composeContacts[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -271,10 +264,7 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
        type: RowType.Contact,
 | 
			
		||||
        contact: composeContacts[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'groupsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(4)), 'groupsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(5), {
 | 
			
		||||
        type: RowType.SelectSingleGroup,
 | 
			
		||||
        group: composeGroups[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -333,10 +323,10 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
        uuidFetchState: {},
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'findByPhoneNumberHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.StartNewConversation,
 | 
			
		||||
        phoneNumber: {
 | 
			
		||||
| 
						 | 
				
			
			@ -363,10 +353,10 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'findByUsernameHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'findByUsernameHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.UsernameSearchResult,
 | 
			
		||||
        username,
 | 
			
		||||
| 
						 | 
				
			
			@ -389,10 +379,7 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
        uuidFetchState: {},
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(0)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Contact,
 | 
			
		||||
        contact: composeContacts[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -401,10 +388,10 @@ describe('LeftPaneComposeHelper', () => {
 | 
			
		|||
        type: RowType.Contact,
 | 
			
		||||
        contact: composeContacts[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'findByPhoneNumberHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(3)),
 | 
			
		||||
        'findByPhoneNumberHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.StartNewConversation,
 | 
			
		||||
        phoneNumber: {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 | 
			
		||||
import { assert } from 'chai';
 | 
			
		||||
import * as sinon from 'sinon';
 | 
			
		||||
import { RowType } from '../../../components/ConversationList';
 | 
			
		||||
import { RowType, _testHeaderText } from '../../../components/ConversationList';
 | 
			
		||||
import { FindDirection } from '../../../components/leftPane/LeftPaneHelper';
 | 
			
		||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -339,10 +339,7 @@ describe('LeftPaneInboxHelper', () => {
 | 
			
		|||
        pinnedConversations,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'LeftPane--pinned',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(0)), 'LeftPane--pinned');
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: pinnedConversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -351,10 +348,7 @@ describe('LeftPaneInboxHelper', () => {
 | 
			
		|||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: pinnedConversations[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'LeftPane--chats',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(3)), 'LeftPane--chats');
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -388,10 +382,7 @@ describe('LeftPaneInboxHelper', () => {
 | 
			
		|||
        archivedConversations: [getDefaultConversation()],
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'LeftPane--pinned',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(0)), 'LeftPane--pinned');
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: pinnedConversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -400,10 +391,7 @@ describe('LeftPaneInboxHelper', () => {
 | 
			
		|||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: pinnedConversations[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'LeftPane--chats',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(3)), 'LeftPane--chats');
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[0],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,7 +4,7 @@
 | 
			
		|||
import { assert } from 'chai';
 | 
			
		||||
import * as sinon from 'sinon';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { RowType } from '../../../components/ConversationList';
 | 
			
		||||
import { RowType, _testHeaderText } from '../../../components/ConversationList';
 | 
			
		||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
 | 
			
		||||
 | 
			
		||||
import { LeftPaneSearchHelper } from '../../../components/leftPane/LeftPaneSearchHelper';
 | 
			
		||||
| 
						 | 
				
			
			@ -192,10 +192,10 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
        startSearchCounter: 0,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'conversationsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'conversationsHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -204,18 +204,12 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(3)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: contacts[0],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(5), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'messagesHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(5)), 'messagesHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(6), {
 | 
			
		||||
        type: RowType.MessageSearchResult,
 | 
			
		||||
        messageId: messages[0].id,
 | 
			
		||||
| 
						 | 
				
			
			@ -244,18 +238,12 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
        startSearchCounter: 0,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'contactsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(0)), 'contactsHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: contacts[0],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(2), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'messagesHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(2)), 'messagesHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.MessageSearchResult,
 | 
			
		||||
        messageId: messages[0].id,
 | 
			
		||||
| 
						 | 
				
			
			@ -287,10 +275,10 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
        startSearchCounter: 0,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'conversationsHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'conversationsHeader'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -299,10 +287,7 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
        type: RowType.Conversation,
 | 
			
		||||
        conversation: conversations[1],
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'messagesHeader',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(_testHeaderText(helper.getRow(3)), 'messagesHeader');
 | 
			
		||||
      assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
        type: RowType.MessageSearchResult,
 | 
			
		||||
        messageId: messages[0].id,
 | 
			
		||||
| 
						 | 
				
			
			@ -332,10 +317,7 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
      startSearchCounter: 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
      type: RowType.Header,
 | 
			
		||||
      i18nKey: 'conversationsHeader',
 | 
			
		||||
    });
 | 
			
		||||
    assert.deepEqual(_testHeaderText(helper.getRow(0)), 'conversationsHeader');
 | 
			
		||||
    assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
      type: RowType.Conversation,
 | 
			
		||||
      conversation: conversations[0],
 | 
			
		||||
| 
						 | 
				
			
			@ -344,10 +326,7 @@ describe('LeftPaneSearchHelper', () => {
 | 
			
		|||
      type: RowType.Conversation,
 | 
			
		||||
      conversation: conversations[1],
 | 
			
		||||
    });
 | 
			
		||||
    assert.deepEqual(helper.getRow(3), {
 | 
			
		||||
      type: RowType.Header,
 | 
			
		||||
      i18nKey: 'contactsHeader',
 | 
			
		||||
    });
 | 
			
		||||
    assert.deepEqual(_testHeaderText(helper.getRow(3)), 'contactsHeader');
 | 
			
		||||
    assert.deepEqual(helper.getRow(4), {
 | 
			
		||||
      type: RowType.Conversation,
 | 
			
		||||
      conversation: contacts[0],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@
 | 
			
		|||
 | 
			
		||||
import { assert } from 'chai';
 | 
			
		||||
import * as sinon from 'sinon';
 | 
			
		||||
import { RowType } from '../../../components/ConversationList';
 | 
			
		||||
import { RowType, _testHeaderText } from '../../../components/ConversationList';
 | 
			
		||||
import { getDefaultConversation } from '../../../test-both/helpers/getDefaultConversation';
 | 
			
		||||
import { DurationInSeconds } from '../../../util/durations';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -92,10 +92,10 @@ describe('LeftPaneSetGroupMetadataHelper', () => {
 | 
			
		|||
        selectedContacts,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      assert.deepEqual(helper.getRow(0), {
 | 
			
		||||
        type: RowType.Header,
 | 
			
		||||
        i18nKey: 'setGroupMetadata__members-header',
 | 
			
		||||
      });
 | 
			
		||||
      assert.deepEqual(
 | 
			
		||||
        _testHeaderText(helper.getRow(0)),
 | 
			
		||||
        'setGroupMetadata__members-header'
 | 
			
		||||
      );
 | 
			
		||||
      assert.deepEqual(helper.getRow(1), {
 | 
			
		||||
        type: RowType.Contact,
 | 
			
		||||
        contact: selectedContacts[0],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue