mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-05-02 14:21:05 +01:00
Move left pane entirely to React
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { RenderTextCallback } from '../../types/Util';
|
||||
import { RenderTextCallbackType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
|
||||
renderNonNewLine?: RenderTextCallback;
|
||||
renderNonNewLine?: RenderTextCallbackType;
|
||||
}
|
||||
|
||||
export class AddNewLines extends React.Component<Props> {
|
||||
|
||||
@@ -4,16 +4,20 @@ import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../../util/GoogleChrome';
|
||||
import { AttachmentType } from './types';
|
||||
import { Image } from './Image';
|
||||
import { areAllAttachmentsVisual } from './ImageGrid';
|
||||
import { StagedGenericAttachment } from './StagedGenericAttachment';
|
||||
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
areAllAttachmentsVisual,
|
||||
AttachmentType,
|
||||
getUrl,
|
||||
isVideoAttachment,
|
||||
} from '../../types/Attachment';
|
||||
|
||||
interface Props {
|
||||
attachments: Array<AttachmentType>;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
// onError: () => void;
|
||||
onClickAttachment: (attachment: AttachmentType) => void;
|
||||
onCloseAttachment: (attachment: AttachmentType) => void;
|
||||
@@ -60,9 +64,14 @@ export class AttachmentList extends React.Component<Props> {
|
||||
isImageTypeSupported(contentType) ||
|
||||
isVideoTypeSupported(contentType)
|
||||
) {
|
||||
const imageKey =
|
||||
getUrl(attachment) || attachment.fileName || index;
|
||||
const clickCallback =
|
||||
attachments.length > 1 ? onClickAttachment : undefined;
|
||||
|
||||
return (
|
||||
<Image
|
||||
key={getUrl(attachment) || attachment.fileName || index}
|
||||
key={imageKey}
|
||||
alt={i18n('stagedImageAttachment', [
|
||||
getUrl(attachment) || attachment.fileName,
|
||||
])}
|
||||
@@ -74,17 +83,18 @@ export class AttachmentList extends React.Component<Props> {
|
||||
width={IMAGE_WIDTH}
|
||||
url={getUrl(attachment)}
|
||||
closeButton={true}
|
||||
onClick={
|
||||
attachments.length > 1 ? onClickAttachment : undefined
|
||||
}
|
||||
onClick={clickCallback}
|
||||
onClickClose={onCloseAttachment}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const genericKey =
|
||||
getUrl(attachment) || attachment.fileName || index;
|
||||
|
||||
return (
|
||||
<StagedGenericAttachment
|
||||
key={getUrl(attachment) || attachment.fileName || index}
|
||||
key={genericKey}
|
||||
attachment={attachment}
|
||||
i18n={i18n}
|
||||
onClose={onCloseAttachment}
|
||||
@@ -99,19 +109,3 @@ export class AttachmentList extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function isVideoAttachment(attachment?: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isVideoTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
|
||||
function getUrl(attachment: AttachmentType) {
|
||||
if (attachment.screenshot) {
|
||||
return attachment.screenshot.url;
|
||||
}
|
||||
|
||||
return attachment.url;
|
||||
}
|
||||
|
||||
@@ -14,18 +14,18 @@ import {
|
||||
renderAvatar,
|
||||
renderContactShorthand,
|
||||
renderName,
|
||||
} from './EmbeddedContact';
|
||||
} from './_contactUtil';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
contact: Contact;
|
||||
hasSignalAccount: boolean;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
onSendMessage: () => void;
|
||||
}
|
||||
|
||||
function getLabelForEmail(method: Email, i18n: Localizer): string {
|
||||
function getLabelForEmail(method: Email, i18n: LocalizerType): string {
|
||||
switch (method.type) {
|
||||
case ContactType.CUSTOM:
|
||||
return method.label || i18n('email');
|
||||
@@ -40,7 +40,7 @@ function getLabelForEmail(method: Email, i18n: Localizer): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelForPhone(method: Phone, i18n: Localizer): string {
|
||||
function getLabelForPhone(method: Phone, i18n: LocalizerType): string {
|
||||
switch (method.type) {
|
||||
case ContactType.CUSTOM:
|
||||
return method.label || i18n('phone');
|
||||
@@ -55,7 +55,10 @@ function getLabelForPhone(method: Phone, i18n: Localizer): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getLabelForAddress(address: PostalAddress, i18n: Localizer): string {
|
||||
function getLabelForAddress(
|
||||
address: PostalAddress,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
switch (address.type) {
|
||||
case AddressType.CUSTOM:
|
||||
return address.label || i18n('address');
|
||||
@@ -104,7 +107,7 @@ export class ContactDetail extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
public renderEmail(items: Array<Email> | undefined, i18n: Localizer) {
|
||||
public renderEmail(items: Array<Email> | undefined, i18n: LocalizerType) {
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -124,7 +127,7 @@ export class ContactDetail extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
public renderPhone(items: Array<Phone> | undefined, i18n: Localizer) {
|
||||
public renderPhone(items: Array<Phone> | undefined, i18n: LocalizerType) {
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -152,7 +155,7 @@ export class ContactDetail extends React.Component<Props> {
|
||||
return <div>{value}</div>;
|
||||
}
|
||||
|
||||
public renderPOBox(poBox: string | undefined, i18n: Localizer) {
|
||||
public renderPOBox(poBox: string | undefined, i18n: LocalizerType) {
|
||||
if (!poBox) {
|
||||
return null;
|
||||
}
|
||||
@@ -178,7 +181,7 @@ export class ContactDetail extends React.Component<Props> {
|
||||
|
||||
public renderAddresses(
|
||||
addresses: Array<PostalAddress> | undefined,
|
||||
i18n: Localizer
|
||||
i18n: LocalizerType
|
||||
) {
|
||||
if (!addresses || addresses.length === 0) {
|
||||
return;
|
||||
|
||||
@@ -2,13 +2,13 @@ import React from 'react';
|
||||
|
||||
import { Emojify } from './Emojify';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
phoneNumber: string;
|
||||
name?: string;
|
||||
profileName?: string;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
module?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { Emojify } from './Emojify';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
@@ -15,12 +15,8 @@ interface TimerOption {
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface Trigger {
|
||||
handleContextClick: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
isVerified: boolean;
|
||||
name?: string;
|
||||
id: string;
|
||||
@@ -46,24 +42,19 @@ interface Props {
|
||||
}
|
||||
|
||||
export class ConversationHeader extends React.Component<Props> {
|
||||
public captureMenuTriggerBound: (trigger: any) => void;
|
||||
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
public menuTriggerRef: Trigger | null;
|
||||
public menuTriggerRef: React.RefObject<any>;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this);
|
||||
this.menuTriggerRef = React.createRef();
|
||||
this.showMenuBound = this.showMenu.bind(this);
|
||||
this.menuTriggerRef = null;
|
||||
}
|
||||
|
||||
public captureMenuTrigger(triggerRef: Trigger) {
|
||||
this.menuTriggerRef = triggerRef;
|
||||
}
|
||||
public showMenu(event: React.MouseEvent<HTMLDivElement>) {
|
||||
if (this.menuTriggerRef) {
|
||||
this.menuTriggerRef.handleContextClick(event);
|
||||
if (this.menuTriggerRef.current) {
|
||||
this.menuTriggerRef.current.handleContextClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,12 +125,14 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
profileName,
|
||||
} = this.props;
|
||||
|
||||
const conversationType = isGroup ? 'group' : 'direct';
|
||||
|
||||
return (
|
||||
<span className="module-conversation-header__avatar">
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color={color}
|
||||
conversationType={isGroup ? 'group' : 'direct'}
|
||||
conversationType={conversationType}
|
||||
i18n={i18n}
|
||||
noteToSelf={isMe}
|
||||
name={name}
|
||||
@@ -176,7 +169,7 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ContextMenuTrigger id={triggerId} ref={this.captureMenuTriggerBound}>
|
||||
<ContextMenuTrigger id={triggerId} ref={this.menuTriggerRef}>
|
||||
<div
|
||||
role="button"
|
||||
onClick={this.showMenuBound}
|
||||
@@ -186,7 +179,6 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
/* tslint:disable:jsx-no-lambda react-this-binding-issue */
|
||||
public renderMenu(triggerId: string) {
|
||||
const {
|
||||
i18n,
|
||||
@@ -235,10 +227,10 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
/* tslint:enable */
|
||||
|
||||
public render() {
|
||||
const { id } = this.props;
|
||||
const triggerId = `conversation-${id}`;
|
||||
|
||||
return (
|
||||
<div className="module-conversation-header">
|
||||
@@ -250,8 +242,8 @@ export class ConversationHeader extends React.Component<Props> {
|
||||
</div>
|
||||
</div>
|
||||
{this.renderExpirationLength()}
|
||||
{this.renderGear(id)}
|
||||
{this.renderMenu(id)}
|
||||
{this.renderGear(triggerId)}
|
||||
{this.renderMenu(triggerId)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '../Avatar';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { Contact, getName } from '../../types/Contact';
|
||||
import { Contact } from '../../types/Contact';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import {
|
||||
renderAvatar,
|
||||
renderContactShorthand,
|
||||
renderName,
|
||||
} from './_contactUtil';
|
||||
|
||||
interface Props {
|
||||
contact: Contact;
|
||||
hasSignalAccount: boolean;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
isIncoming: boolean;
|
||||
withContentAbove: boolean;
|
||||
withContentBelow: boolean;
|
||||
@@ -53,88 +56,3 @@ export class EmbeddedContact extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: putting these below the main component so style guide picks up EmbeddedContact
|
||||
|
||||
export function renderAvatar({
|
||||
contact,
|
||||
i18n,
|
||||
size,
|
||||
direction,
|
||||
}: {
|
||||
contact: Contact;
|
||||
i18n: Localizer;
|
||||
size: number;
|
||||
direction?: string;
|
||||
}) {
|
||||
const { avatar } = contact;
|
||||
|
||||
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
|
||||
const pending = avatar && avatar.avatar && avatar.avatar.pending;
|
||||
const name = getName(contact) || '';
|
||||
|
||||
if (pending) {
|
||||
return (
|
||||
<div className="module-embedded-contact__spinner-container">
|
||||
<Spinner small={size < 50} direction={direction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color="grey"
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderName({
|
||||
contact,
|
||||
isIncoming,
|
||||
module,
|
||||
}: {
|
||||
contact: Contact;
|
||||
isIncoming: boolean;
|
||||
module: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`module-${module}__contact-name`,
|
||||
isIncoming ? `module-${module}__contact-name--incoming` : null
|
||||
)}
|
||||
>
|
||||
{getName(contact)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderContactShorthand({
|
||||
contact,
|
||||
isIncoming,
|
||||
module,
|
||||
}: {
|
||||
contact: Contact;
|
||||
isIncoming: boolean;
|
||||
module: string;
|
||||
}) {
|
||||
const { number: phoneNumber, email } = contact;
|
||||
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
|
||||
const firstEmail = email && email[0] && email[0].value;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`module-${module}__contact-method`,
|
||||
isIncoming ? `module-${module}__contact-method--incoming` : null
|
||||
)}
|
||||
>
|
||||
{firstNumber || firstEmail}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
SizeClassType,
|
||||
} from '../../util/emoji';
|
||||
|
||||
import { Localizer, RenderTextCallback } from '../../types/Util';
|
||||
import { LocalizerType, RenderTextCallbackType } from '../../types/Util';
|
||||
|
||||
// Some of this logic taken from emoji-js/replacement
|
||||
function getImageTag({
|
||||
@@ -23,7 +23,7 @@ function getImageTag({
|
||||
match: any;
|
||||
sizeClass?: SizeClassType;
|
||||
key: string | number;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}) {
|
||||
const result = getReplacementData(match[0], match[1], match[2]);
|
||||
|
||||
@@ -54,8 +54,8 @@ interface Props {
|
||||
/** A class name to be added to the generated emoji images */
|
||||
sizeClass?: SizeClassType;
|
||||
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
|
||||
renderNonEmoji?: RenderTextCallback;
|
||||
i18n: Localizer;
|
||||
renderNonEmoji?: RenderTextCallbackType;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class Emojify extends React.Component<Props> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { padStart } from 'lodash';
|
||||
import { getIncrement, getTimerBucket } from '../../util/timer';
|
||||
|
||||
interface Props {
|
||||
withImageNoCaption: boolean;
|
||||
@@ -62,25 +62,3 @@ export class ExpireTimer extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getIncrement(length: number): number {
|
||||
if (length < 0) {
|
||||
return 1000;
|
||||
}
|
||||
|
||||
return Math.ceil(length / 12);
|
||||
}
|
||||
|
||||
function getTimerBucket(expiration: number, length: number): string {
|
||||
const delta = expiration - Date.now();
|
||||
if (delta < 0) {
|
||||
return '00';
|
||||
}
|
||||
if (delta > length) {
|
||||
return '60';
|
||||
}
|
||||
|
||||
const bucket = Math.round(delta / length * 12);
|
||||
|
||||
return padStart(String(bucket * 5), 2, '0');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { compact, flatten } from 'lodash';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
@@ -23,7 +23,7 @@ interface Change {
|
||||
|
||||
interface Props {
|
||||
changes: Array<Change>;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class GroupNotification extends React.Component<Props> {
|
||||
@@ -61,15 +61,10 @@ export class GroupNotification extends React.Component<Props> {
|
||||
throw new Error('Group update is missing contacts');
|
||||
}
|
||||
|
||||
return (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={
|
||||
contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup'
|
||||
}
|
||||
components={[people]}
|
||||
/>
|
||||
);
|
||||
const joinKey =
|
||||
contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
|
||||
|
||||
return <Intl i18n={i18n} id={joinKey} components={[people]} />;
|
||||
case 'remove':
|
||||
if (isMe) {
|
||||
return i18n('youLeftTheGroup');
|
||||
@@ -79,13 +74,10 @@ export class GroupNotification extends React.Component<Props> {
|
||||
throw new Error('Group update is missing contacts');
|
||||
}
|
||||
|
||||
return (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup'}
|
||||
components={[people]}
|
||||
/>
|
||||
);
|
||||
const leftKey =
|
||||
contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
|
||||
|
||||
return <Intl i18n={i18n} id={leftKey} components={[people]} />;
|
||||
case 'general':
|
||||
return i18n('updatedTheGroup');
|
||||
default:
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { AttachmentType } from './types';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
|
||||
interface Props {
|
||||
alt: string;
|
||||
@@ -28,7 +28,7 @@ interface Props {
|
||||
playIconOverlay?: boolean;
|
||||
softCorners?: boolean;
|
||||
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
onClickClose?: (attachment: AttachmentType) => void;
|
||||
onError?: () => void;
|
||||
@@ -62,10 +62,11 @@ export class Image extends React.Component<Props> {
|
||||
|
||||
const { caption, pending } = attachment || { caption: null, pending: true };
|
||||
const canClick = onClick && !pending;
|
||||
const role = canClick ? 'button' : undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
role={canClick ? 'button' : undefined}
|
||||
role={role}
|
||||
onClick={() => {
|
||||
if (canClick && onClick) {
|
||||
onClick(attachment);
|
||||
|
||||
@@ -2,12 +2,18 @@ import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../../util/GoogleChrome';
|
||||
import { AttachmentType } from './types';
|
||||
areAllAttachmentsVisual,
|
||||
AttachmentType,
|
||||
getAlt,
|
||||
getImageDimensions,
|
||||
getThumbnailUrl,
|
||||
getUrl,
|
||||
isVideoAttachment,
|
||||
} from '../../types/Attachment';
|
||||
|
||||
import { Image } from './Image';
|
||||
import { Localizer } from '../../types/Util';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
attachments: Array<AttachmentType>;
|
||||
@@ -15,17 +21,12 @@ interface Props {
|
||||
withContentBelow?: boolean;
|
||||
bottomOverlay?: boolean;
|
||||
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
|
||||
onError: () => void;
|
||||
onClickAttachment?: (attachment: AttachmentType) => void;
|
||||
}
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = MAX_WIDTH * 1.5;
|
||||
const MIN_WIDTH = 200;
|
||||
const MIN_HEIGHT = 50;
|
||||
|
||||
export class ImageGrid extends React.Component<Props> {
|
||||
// tslint:disable-next-line max-func-body-length */
|
||||
public render() {
|
||||
@@ -46,6 +47,8 @@ export class ImageGrid extends React.Component<Props> {
|
||||
const curveBottomLeft = curveBottom;
|
||||
const curveBottomRight = curveBottom;
|
||||
|
||||
const withBottomOverlay = Boolean(bottomOverlay && curveBottom);
|
||||
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
@@ -63,7 +66,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
@@ -87,7 +90,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
attachment={attachments[0]}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
@@ -100,7 +103,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveTopRight={curveTopRight}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
@@ -121,7 +124,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveTopLeft={curveTopLeft}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
attachment={attachments[0]}
|
||||
@@ -148,7 +151,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveBottomRight={curveBottomRight}
|
||||
height={99}
|
||||
width={99}
|
||||
@@ -197,7 +200,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={149}
|
||||
@@ -210,7 +213,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={149}
|
||||
@@ -226,6 +229,11 @@ export class ImageGrid extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
const moreMessagesOverlay = attachments.length > 5;
|
||||
const moreMessagesOverlayText = moreMessagesOverlay
|
||||
? `+${attachments.length - 5}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="module-image-grid">
|
||||
<div className="module-image-grid__column">
|
||||
@@ -259,7 +267,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
playIconOverlay={isVideoAttachment(attachments[2])}
|
||||
height={99}
|
||||
@@ -272,7 +280,7 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
height={99}
|
||||
width={98}
|
||||
@@ -284,17 +292,13 @@ export class ImageGrid extends React.Component<Props> {
|
||||
<Image
|
||||
alt={getAlt(attachments[4], i18n)}
|
||||
i18n={i18n}
|
||||
bottomOverlay={bottomOverlay && curveBottom}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
curveBottomRight={curveBottomRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[4])}
|
||||
height={99}
|
||||
width={99}
|
||||
darkOverlay={attachments.length > 5}
|
||||
overlayText={
|
||||
attachments.length > 5
|
||||
? `+${attachments.length - 5}`
|
||||
: undefined
|
||||
}
|
||||
darkOverlay={moreMessagesOverlay}
|
||||
overlayText={moreMessagesOverlayText}
|
||||
attachment={attachments[4]}
|
||||
url={getThumbnailUrl(attachments[4])}
|
||||
onClick={onClickAttachment}
|
||||
@@ -306,148 +310,3 @@ export class ImageGrid extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getThumbnailUrl(attachment: AttachmentType) {
|
||||
if (attachment.thumbnail) {
|
||||
return attachment.thumbnail.url;
|
||||
}
|
||||
|
||||
return getUrl(attachment);
|
||||
}
|
||||
|
||||
function getUrl(attachment: AttachmentType) {
|
||||
if (attachment.screenshot) {
|
||||
return attachment.screenshot.url;
|
||||
}
|
||||
|
||||
return attachment.url;
|
||||
}
|
||||
|
||||
export function isImage(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
isImageTypeSupported(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
export function isImageAttachment(attachment: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isImageTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
export function hasImage(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
(attachments[0].url || attachments[0].pending)
|
||||
);
|
||||
}
|
||||
|
||||
export function isVideo(attachments?: Array<AttachmentType>) {
|
||||
return attachments && isVideoAttachment(attachments[0]);
|
||||
}
|
||||
|
||||
export function isVideoAttachment(attachment?: AttachmentType) {
|
||||
return (
|
||||
attachment &&
|
||||
attachment.contentType &&
|
||||
isVideoTypeSupported(attachment.contentType)
|
||||
);
|
||||
}
|
||||
|
||||
export function hasVideoScreenshot(attachments?: Array<AttachmentType>) {
|
||||
const firstAttachment = attachments ? attachments[0] : null;
|
||||
|
||||
return (
|
||||
firstAttachment &&
|
||||
firstAttachment.screenshot &&
|
||||
firstAttachment.screenshot.url
|
||||
);
|
||||
}
|
||||
|
||||
type DimensionsType = {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export function getImageDimensions(attachment: AttachmentType): DimensionsType {
|
||||
const { height, width } = attachment;
|
||||
if (!height || !width) {
|
||||
return {
|
||||
height: MIN_HEIGHT,
|
||||
width: MIN_WIDTH,
|
||||
};
|
||||
}
|
||||
|
||||
const aspectRatio = height / width;
|
||||
const targetWidth = Math.max(Math.min(MAX_WIDTH, width), MIN_WIDTH);
|
||||
const candidateHeight = Math.round(targetWidth * aspectRatio);
|
||||
|
||||
return {
|
||||
width: targetWidth,
|
||||
height: Math.max(Math.min(MAX_HEIGHT, candidateHeight), MIN_HEIGHT),
|
||||
};
|
||||
}
|
||||
|
||||
export function areAllAttachmentsVisual(
|
||||
attachments?: Array<AttachmentType>
|
||||
): boolean {
|
||||
if (!attachments) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const max = attachments.length;
|
||||
for (let i = 0; i < max; i += 1) {
|
||||
const attachment = attachments[i];
|
||||
if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getGridDimensions(
|
||||
attachments?: Array<AttachmentType>
|
||||
): null | DimensionsType {
|
||||
if (!attachments || !attachments.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isImage(attachments) && !isVideo(attachments)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (attachments.length === 1) {
|
||||
return getImageDimensions(attachments[0]);
|
||||
}
|
||||
|
||||
if (attachments.length === 2) {
|
||||
return {
|
||||
height: 150,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
if (attachments.length === 4) {
|
||||
return {
|
||||
height: 300,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
height: 200,
|
||||
width: 300,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAlt(attachment: AttachmentType, i18n: Localizer): string {
|
||||
return isVideoAttachment(attachment)
|
||||
? i18n('videoAttachmentAlt')
|
||||
: i18n('imageAttachmentAlt');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import LinkifyIt from 'linkify-it';
|
||||
|
||||
import { RenderTextCallback } from '../../types/Util';
|
||||
import { RenderTextCallbackType } from '../../types/Util';
|
||||
import { isLinkSneaky } from '../../../js/modules/link_previews';
|
||||
|
||||
const linkify = LinkifyIt();
|
||||
@@ -10,7 +10,7 @@ const linkify = LinkifyIt();
|
||||
interface Props {
|
||||
text: string;
|
||||
/** Allows you to customize now non-links are rendered. Simplest is just a <span>. */
|
||||
renderNonLink?: RenderTextCallback;
|
||||
renderNonLink?: RenderTextCallbackType;
|
||||
}
|
||||
|
||||
const SUPPORTED_PROTOCOLS = /^(http|https):/i;
|
||||
|
||||
@@ -4,28 +4,32 @@ import classNames from 'classnames';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { Spinner } from '../Spinner';
|
||||
import { MessageBody } from './MessageBody';
|
||||
import { ExpireTimer, getIncrement } from './ExpireTimer';
|
||||
import {
|
||||
getGridDimensions,
|
||||
getImageDimensions,
|
||||
hasImage,
|
||||
hasVideoScreenshot,
|
||||
ImageGrid,
|
||||
isImage,
|
||||
isImageAttachment,
|
||||
isVideo,
|
||||
} from './ImageGrid';
|
||||
import { ExpireTimer } from './ExpireTimer';
|
||||
import { ImageGrid } from './ImageGrid';
|
||||
import { Image } from './Image';
|
||||
import { Timestamp } from './Timestamp';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Quote, QuotedAttachmentType } from './Quote';
|
||||
import { EmbeddedContact } from './EmbeddedContact';
|
||||
import * as MIME from '../../../ts/types/MIME';
|
||||
|
||||
import { AttachmentType } from './types';
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
import {
|
||||
canDisplayImage,
|
||||
getExtensionForDisplay,
|
||||
getGridDimensions,
|
||||
getImageDimensions,
|
||||
hasImage,
|
||||
hasVideoScreenshot,
|
||||
isAudio,
|
||||
isImage,
|
||||
isImageAttachment,
|
||||
isVideo,
|
||||
} from '../../../ts/types/Attachment';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
import { Contact } from '../../types/Contact';
|
||||
import { Color, Localizer } from '../../types/Util';
|
||||
|
||||
import { getIncrement } from '../../util/timer';
|
||||
import { isFileDangerous } from '../../util/isFileDangerous';
|
||||
import { ColorType, LocalizerType } from '../../types/Util';
|
||||
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
|
||||
|
||||
interface Trigger {
|
||||
@@ -56,12 +60,12 @@ export interface Props {
|
||||
onSendMessage?: () => void;
|
||||
onClick?: () => void;
|
||||
};
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
authorName?: string;
|
||||
authorProfileName?: string;
|
||||
/** Note: this should be formatted for display */
|
||||
authorPhoneNumber: string;
|
||||
authorColor?: Color;
|
||||
authorColor?: ColorType;
|
||||
conversationType: 'group' | 'direct';
|
||||
attachments?: Array<AttachmentType>;
|
||||
quote?: {
|
||||
@@ -71,7 +75,7 @@ export interface Props {
|
||||
authorPhoneNumber: string;
|
||||
authorProfileName?: string;
|
||||
authorName?: string;
|
||||
authorColor?: Color;
|
||||
authorColor?: ColorType;
|
||||
onClick?: () => void;
|
||||
referencedMessageNotFound: boolean;
|
||||
};
|
||||
@@ -98,12 +102,12 @@ interface State {
|
||||
const EXPIRATION_CHECK_MINIMUM = 2000;
|
||||
const EXPIRED_DELAY = 600;
|
||||
|
||||
export class Message extends React.Component<Props, State> {
|
||||
export class Message extends React.PureComponent<Props, State> {
|
||||
public captureMenuTriggerBound: (trigger: any) => void;
|
||||
public showMenuBound: (event: React.MouseEvent<HTMLDivElement>) => void;
|
||||
public handleImageErrorBound: () => void;
|
||||
|
||||
public menuTriggerRef: Trigger | null;
|
||||
public menuTriggerRef: Trigger | undefined;
|
||||
public expirationCheckInterval: any;
|
||||
public expiredTimeout: any;
|
||||
|
||||
@@ -114,10 +118,6 @@ export class Message extends React.Component<Props, State> {
|
||||
this.showMenuBound = this.showMenu.bind(this);
|
||||
this.handleImageErrorBound = this.handleImageError.bind(this);
|
||||
|
||||
this.menuTriggerRef = null;
|
||||
this.expirationCheckInterval = null;
|
||||
this.expiredTimeout = null;
|
||||
|
||||
this.state = {
|
||||
expiring: false,
|
||||
expired: false,
|
||||
@@ -366,7 +366,7 @@ export class Message extends React.Component<Props, State> {
|
||||
);
|
||||
} else {
|
||||
const { pending, fileName, fileSize, contentType } = firstAttachment;
|
||||
const extension = getExtension({ contentType, fileName });
|
||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||
const isDangerous = isFileDangerous(fileName || '');
|
||||
|
||||
return (
|
||||
@@ -851,7 +851,7 @@ export class Message extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
public getWidth(): Number | undefined {
|
||||
public getWidth(): number | undefined {
|
||||
const { attachments, previews } = this.props;
|
||||
|
||||
if (attachments && attachments.length) {
|
||||
@@ -976,53 +976,3 @@ export class Message extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function getExtension({
|
||||
fileName,
|
||||
contentType,
|
||||
}: {
|
||||
fileName: string;
|
||||
contentType: MIME.MIMEType;
|
||||
}): string | null {
|
||||
if (fileName && fileName.indexOf('.') >= 0) {
|
||||
const lastPeriod = fileName.lastIndexOf('.');
|
||||
const extension = fileName.slice(lastPeriod + 1);
|
||||
if (extension.length) {
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const slash = contentType.indexOf('/');
|
||||
if (slash >= 0) {
|
||||
return contentType.slice(slash + 1);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isAudio(attachments?: Array<AttachmentType>) {
|
||||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
attachments[0].contentType &&
|
||||
MIME.isAudio(attachments[0].contentType)
|
||||
);
|
||||
}
|
||||
|
||||
function canDisplayImage(attachments?: Array<AttachmentType>) {
|
||||
const { height, width } =
|
||||
attachments && attachments[0] ? attachments[0] : { height: 0, width: 0 };
|
||||
|
||||
return (
|
||||
height &&
|
||||
height > 0 &&
|
||||
height <= 4096 &&
|
||||
width &&
|
||||
width > 0 &&
|
||||
width <= 4096
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Emojify } from './Emojify';
|
||||
import { AddNewLines } from './AddNewLines';
|
||||
import { Linkify } from './Linkify';
|
||||
|
||||
import { Localizer, RenderTextCallback } from '../../types/Util';
|
||||
import { LocalizerType, RenderTextCallbackType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
text: string;
|
||||
@@ -13,10 +13,10 @@ interface Props {
|
||||
disableJumbomoji?: boolean;
|
||||
/** If set, links will be left alone instead of turned into clickable `<a>` tags. */
|
||||
disableLinks?: boolean;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
const renderNewLines: RenderTextCallback = ({
|
||||
const renderNewLines: RenderTextCallbackType = ({
|
||||
text: textWithNewLines,
|
||||
key,
|
||||
}) => <AddNewLines key={key} text={textWithNewLines} />;
|
||||
@@ -28,11 +28,11 @@ const renderEmoji = ({
|
||||
sizeClass,
|
||||
renderNonEmoji,
|
||||
}: {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
text: string;
|
||||
key: number;
|
||||
sizeClass?: SizeClassType;
|
||||
renderNonEmoji: RenderTextCallback;
|
||||
renderNonEmoji: RenderTextCallbackType;
|
||||
}) => (
|
||||
<Emojify
|
||||
i18n={i18n}
|
||||
|
||||
@@ -5,7 +5,7 @@ import moment from 'moment';
|
||||
import { Avatar } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Message, Props as MessageProps } from './Message';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Contact {
|
||||
status: string;
|
||||
@@ -31,7 +31,7 @@ interface Props {
|
||||
errors: Array<Error>;
|
||||
contacts: Array<Contact>;
|
||||
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class MessageDetail extends React.Component<Props> {
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as MIME from '../../../ts/types/MIME';
|
||||
import * as GoogleChrome from '../../../ts/util/GoogleChrome';
|
||||
|
||||
import { MessageBody } from './MessageBody';
|
||||
import { Color, Localizer } from '../../types/Util';
|
||||
import { ColorType, LocalizerType } from '../../types/Util';
|
||||
import { ContactName } from './ContactName';
|
||||
|
||||
interface Props {
|
||||
@@ -15,8 +15,8 @@ interface Props {
|
||||
authorPhoneNumber: string;
|
||||
authorProfileName?: string;
|
||||
authorName?: string;
|
||||
authorColor?: Color;
|
||||
i18n: Localizer;
|
||||
authorColor?: ColorType;
|
||||
i18n: LocalizerType;
|
||||
isFromMe: boolean;
|
||||
isIncoming: boolean;
|
||||
withContentAbove: boolean;
|
||||
@@ -56,12 +56,12 @@ function validateQuote(quote: Props): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getObjectUrl(thumbnail: Attachment | undefined): string | null {
|
||||
function getObjectUrl(thumbnail: Attachment | undefined): string | undefined {
|
||||
if (thumbnail && thumbnail.objectUrl) {
|
||||
return thumbnail.objectUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
function getTypeLabel({
|
||||
@@ -69,10 +69,10 @@ function getTypeLabel({
|
||||
contentType,
|
||||
isVoiceMessage,
|
||||
}: {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
contentType: MIME.MIMEType;
|
||||
isVoiceMessage: boolean;
|
||||
}): string | null {
|
||||
}): string | undefined {
|
||||
if (GoogleChrome.isVideoTypeSupported(contentType)) {
|
||||
return i18n('video');
|
||||
}
|
||||
@@ -86,7 +86,7 @@ function getTypeLabel({
|
||||
return i18n('audio');
|
||||
}
|
||||
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
export class Quote extends React.Component<Props, State> {
|
||||
@@ -110,7 +110,7 @@ export class Quote extends React.Component<Props, State> {
|
||||
});
|
||||
}
|
||||
|
||||
public renderImage(url: string, i18n: Localizer, icon?: string) {
|
||||
public renderImage(url: string, i18n: LocalizerType, icon?: string) {
|
||||
const iconElement = icon ? (
|
||||
<div className="module-quote__icon-container__inner">
|
||||
<div className="module-quote__icon-container__circle-background">
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class ResetSessionNotification extends React.Component<Props> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Contact {
|
||||
phoneNumber: string;
|
||||
@@ -14,20 +14,23 @@ interface Contact {
|
||||
interface Props {
|
||||
isGroup: boolean;
|
||||
contact: Contact;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
onVerify: () => void;
|
||||
}
|
||||
|
||||
export class SafetyNumberNotification extends React.Component<Props> {
|
||||
public render() {
|
||||
const { contact, isGroup, i18n, onVerify } = this.props;
|
||||
const changeKey = isGroup
|
||||
? 'safetyNumberChangedGroup'
|
||||
: 'safetyNumberChanged';
|
||||
|
||||
return (
|
||||
<div className="module-safety-number-notification">
|
||||
<div className="module-safety-number-notification__icon" />
|
||||
<div className="module-safety-number-notification__text">
|
||||
<Intl
|
||||
id={isGroup ? 'safetyNumberChangedGroup' : 'safetyNumberChanged'}
|
||||
id={changeKey}
|
||||
components={[
|
||||
<span
|
||||
key="external-1"
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getExtension } from './Message';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { AttachmentType } from './types';
|
||||
import { AttachmentType, getExtensionForDisplay } from '../../types/Attachment';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
attachment: AttachmentType;
|
||||
onClose: (attachment: AttachmentType) => void;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class StagedGenericAttachment extends React.Component<Props> {
|
||||
public render() {
|
||||
const { attachment, onClose } = this.props;
|
||||
const { fileName, contentType } = attachment;
|
||||
const extension = getExtension({ contentType, fileName });
|
||||
const extension = getExtensionForDisplay({ contentType, fileName });
|
||||
|
||||
return (
|
||||
<div className="module-staged-generic-attachment">
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { isImageAttachment } from './ImageGrid';
|
||||
import { Image } from './Image';
|
||||
import { AttachmentType } from './types';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { AttachmentType, isImageAttachment } from '../../types/Attachment';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
isLoaded: boolean;
|
||||
@@ -13,7 +12,7 @@ interface Props {
|
||||
domain: string;
|
||||
image?: AttachmentType;
|
||||
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
@@ -14,7 +14,7 @@ interface Props {
|
||||
name?: string;
|
||||
disabled: boolean;
|
||||
timespan: string;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class TimerNotification extends React.Component<Props> {
|
||||
@@ -28,15 +28,16 @@ export class TimerNotification extends React.Component<Props> {
|
||||
type,
|
||||
disabled,
|
||||
} = this.props;
|
||||
const changeKey = disabled
|
||||
? 'disabledDisappearingMessages'
|
||||
: 'theyChangedTheTimer';
|
||||
|
||||
switch (type) {
|
||||
case 'fromOther':
|
||||
return (
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={
|
||||
disabled ? 'disabledDisappearingMessages' : 'theyChangedTheTimer'
|
||||
}
|
||||
id={changeKey}
|
||||
components={[
|
||||
<ContactName
|
||||
i18n={i18n}
|
||||
|
||||
@@ -4,15 +4,15 @@ import moment from 'moment';
|
||||
|
||||
import { formatRelativeTime } from '../../util/formatRelativeTime';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
timestamp: number | null;
|
||||
extended: boolean;
|
||||
timestamp?: number;
|
||||
extended?: boolean;
|
||||
module?: string;
|
||||
withImageNoCaption?: boolean;
|
||||
direction?: 'incoming' | 'outgoing';
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
const UPDATE_FREQUENCY = 60 * 1000;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import classNames from 'classnames';
|
||||
import { TypingAnimation } from './TypingAnimation';
|
||||
import { Avatar } from '../Avatar';
|
||||
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
interface Props {
|
||||
avatarPath?: string;
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
phoneNumber: string;
|
||||
profileName: string;
|
||||
conversationType: string;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class TypingBubble extends React.Component<Props> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
|
||||
import { ContactName } from './ContactName';
|
||||
import { Intl } from '../Intl';
|
||||
import { Localizer } from '../../types/Util';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
import { missingCaseError } from '../../util/missingCaseError';
|
||||
|
||||
@@ -17,7 +17,7 @@ interface Props {
|
||||
type: 'markVerified' | 'markNotVerified';
|
||||
isLocal: boolean;
|
||||
contact: Contact;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
export class VerificationNotification extends React.Component<Props> {
|
||||
|
||||
93
ts/components/conversation/_contactUtil.tsx
Normal file
93
ts/components/conversation/_contactUtil.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '../Avatar';
|
||||
import { Spinner } from '../Spinner';
|
||||
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
import { Contact, getName } from '../../types/Contact';
|
||||
|
||||
// This file starts with _ to keep it from showing up in the StyleGuide.
|
||||
|
||||
export function renderAvatar({
|
||||
contact,
|
||||
i18n,
|
||||
size,
|
||||
direction,
|
||||
}: {
|
||||
contact: Contact;
|
||||
i18n: LocalizerType;
|
||||
size: number;
|
||||
direction?: string;
|
||||
}) {
|
||||
const { avatar } = contact;
|
||||
|
||||
const avatarPath = avatar && avatar.avatar && avatar.avatar.path;
|
||||
const pending = avatar && avatar.avatar && avatar.avatar.pending;
|
||||
const name = getName(contact) || '';
|
||||
|
||||
if (pending) {
|
||||
return (
|
||||
<div className="module-embedded-contact__spinner-container">
|
||||
<Spinner small={size < 50} direction={direction} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarPath={avatarPath}
|
||||
color="grey"
|
||||
conversationType="direct"
|
||||
i18n={i18n}
|
||||
name={name}
|
||||
size={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderName({
|
||||
contact,
|
||||
isIncoming,
|
||||
module,
|
||||
}: {
|
||||
contact: Contact;
|
||||
isIncoming: boolean;
|
||||
module: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`module-${module}__contact-name`,
|
||||
isIncoming ? `module-${module}__contact-name--incoming` : null
|
||||
)}
|
||||
>
|
||||
{getName(contact)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderContactShorthand({
|
||||
contact,
|
||||
isIncoming,
|
||||
module,
|
||||
}: {
|
||||
contact: Contact;
|
||||
isIncoming: boolean;
|
||||
module: string;
|
||||
}) {
|
||||
const { number: phoneNumber, email } = contact;
|
||||
const firstNumber = phoneNumber && phoneNumber[0] && phoneNumber[0].value;
|
||||
const firstEmail = email && email[0] && email[0].value;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
`module-${module}__contact-method`,
|
||||
isIncoming ? `module-${module}__contact-method--incoming` : null
|
||||
)}
|
||||
>
|
||||
{firstNumber || firstEmail}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,10 +5,10 @@ import { ItemClickEvent } from './types/ItemClickEvent';
|
||||
import { MediaGridItem } from './MediaGridItem';
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import { Localizer } from '../../../types/Util';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
|
||||
interface Props {
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
header?: string;
|
||||
type: 'media' | 'documents';
|
||||
mediaItems: Array<MediaItemType>;
|
||||
@@ -64,7 +64,7 @@ export class AttachmentSection extends React.Component<Props> {
|
||||
});
|
||||
}
|
||||
|
||||
private createClickHandler = (mediaItem: MediaItemType) => () => {
|
||||
private readonly createClickHandler = (mediaItem: MediaItemType) => () => {
|
||||
const { onItemClick, type } = this.props;
|
||||
const { message, attachment } = mediaItem;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ interface Props {
|
||||
timestamp: number;
|
||||
|
||||
// Optional
|
||||
fileName?: string | null;
|
||||
fileName?: string;
|
||||
fileSize?: number;
|
||||
onClick?: () => void;
|
||||
shouldShowSeparator?: boolean;
|
||||
|
||||
@@ -8,13 +8,13 @@ import { EmptyState } from './EmptyState';
|
||||
import { groupMediaItemsByDate } from './groupMediaItemsByDate';
|
||||
import { ItemClickEvent } from './types/ItemClickEvent';
|
||||
import { missingCaseError } from '../../../util/missingCaseError';
|
||||
import { Localizer } from '../../../types/Util';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
|
||||
interface Props {
|
||||
documents: Array<MediaItemType>;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
media: Array<MediaItemType>;
|
||||
onItemClick?: (event: ItemClickEvent) => void;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ export class MediaGallery extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
private handleTabSelect = (event: TabSelectEvent): void => {
|
||||
private readonly handleTabSelect = (event: TabSelectEvent): void => {
|
||||
this.setState({ selectedTab: event.type });
|
||||
};
|
||||
|
||||
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
isImageTypeSupported,
|
||||
isVideoTypeSupported,
|
||||
} from '../../../util/GoogleChrome';
|
||||
import { Localizer } from '../../../types/Util';
|
||||
import { LocalizerType } from '../../../types/Util';
|
||||
import { MediaItemType } from '../../LightboxGallery';
|
||||
|
||||
interface Props {
|
||||
mediaItem: MediaItemType;
|
||||
onClick?: () => void;
|
||||
i18n: Localizer;
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
|
||||
interface State {
|
||||
@@ -19,7 +19,7 @@ interface State {
|
||||
}
|
||||
|
||||
export class MediaGridItem extends React.Component<Props, State> {
|
||||
private onImageErrorBound: () => void;
|
||||
private readonly onImageErrorBound: () => void;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
@@ -48,15 +48,15 @@ export const groupMediaItemsByDate = (
|
||||
|
||||
const toSection = (
|
||||
messagesWithSection: Array<MediaItemWithSection> | undefined
|
||||
): Section | null => {
|
||||
): Section | undefined => {
|
||||
if (!messagesWithSection || messagesWithSection.length === 0) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const firstMediaItemWithSection: MediaItemWithSection =
|
||||
messagesWithSection[0];
|
||||
if (!firstMediaItemWithSection) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const mediaItems = messagesWithSection.map(
|
||||
@@ -83,7 +83,7 @@ const toSection = (
|
||||
// error TS2345: Argument of type 'any' is not assignable to parameter
|
||||
// of type 'never'.
|
||||
// return missingCaseError(firstMediaItemWithSection.type);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AttachmentType } from '../../types';
|
||||
import { AttachmentType } from '../../../../types/Attachment';
|
||||
import { Message } from './Message';
|
||||
|
||||
export interface ItemClickEvent {
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { MIMEType } from '../../../ts/types/MIME';
|
||||
|
||||
export interface AttachmentType {
|
||||
caption?: string;
|
||||
contentType: MIMEType;
|
||||
fileName: string;
|
||||
/** Not included in protobuf, needs to be pulled from flags */
|
||||
isVoiceMessage?: boolean;
|
||||
/** For messages not already on disk, this will be a data url */
|
||||
url: string;
|
||||
size?: number;
|
||||
fileSize?: string;
|
||||
pending?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
screenshot?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
contentType: MIMEType;
|
||||
};
|
||||
thumbnail?: {
|
||||
height: number;
|
||||
width: number;
|
||||
url: string;
|
||||
contentType: MIMEType;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user