Message Requests
This commit is contained in:
parent
4d4b7a26a5
commit
83574eb067
60 changed files with 2566 additions and 216 deletions
94
ts/RemoteConfig.ts
Normal file
94
ts/RemoteConfig.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
// tslint:disable: no-backbone-get-set-outside-model
|
||||
import { get, throttle } from 'lodash';
|
||||
import { WebAPIType } from './textsecure/WebAPI';
|
||||
|
||||
type ConfigKeyType = 'desktop.messageRequests';
|
||||
type ConfigValueType = {
|
||||
name: ConfigKeyType;
|
||||
enabled: boolean;
|
||||
enabledAt?: number;
|
||||
};
|
||||
type ConfigMapType = { [key: string]: ConfigValueType };
|
||||
type ConfigListenerType = (value: ConfigValueType) => unknown;
|
||||
type ConfigListenersMapType = {
|
||||
[key: string]: Array<ConfigListenerType>;
|
||||
};
|
||||
|
||||
function getServer(): WebAPIType {
|
||||
const OLD_USERNAME = window.storage.get<string>('number_id');
|
||||
const USERNAME = window.storage.get<string>('uuid_id');
|
||||
const PASSWORD = window.storage.get<string>('password');
|
||||
|
||||
return window.WebAPI.connect({
|
||||
username: (USERNAME || OLD_USERNAME) as string,
|
||||
password: PASSWORD as string,
|
||||
});
|
||||
}
|
||||
|
||||
let config: ConfigMapType = {};
|
||||
const listeners: ConfigListenersMapType = {};
|
||||
|
||||
export async function initRemoteConfig() {
|
||||
config = window.storage.get('remoteConfig') || {};
|
||||
await maybeRefreshRemoteConfig();
|
||||
}
|
||||
|
||||
export function onChange(key: ConfigKeyType, fn: ConfigListenerType) {
|
||||
const keyListeners: Array<ConfigListenerType> = get(listeners, key, []);
|
||||
keyListeners.push(fn);
|
||||
listeners[key] = keyListeners;
|
||||
|
||||
return () => {
|
||||
listeners[key] = listeners[key].filter(l => l !== fn);
|
||||
};
|
||||
}
|
||||
|
||||
const refreshRemoteConfig = async () => {
|
||||
const now = Date.now();
|
||||
const server = getServer();
|
||||
const newConfig = await server.getConfig();
|
||||
|
||||
// Process new configuration in light of the old configuration
|
||||
// The old configuration is not set as the initial value in reduce because
|
||||
// flags may have been deleted
|
||||
const oldConfig = config;
|
||||
config = newConfig.reduce((previous, { name, enabled }) => {
|
||||
const previouslyEnabled: boolean = get(oldConfig, [name, 'enabled'], false);
|
||||
// If a flag was previously not enabled and is now enabled, record the time it was enabled
|
||||
const enabledAt: number | undefined =
|
||||
previouslyEnabled && enabled ? now : get(oldConfig, [name, 'enabledAt']);
|
||||
|
||||
const value = {
|
||||
name: name as ConfigKeyType,
|
||||
enabled,
|
||||
enabledAt,
|
||||
};
|
||||
|
||||
// If enablement changes at all, notify listeners
|
||||
const currentListeners = listeners[name] || [];
|
||||
if (previouslyEnabled !== enabled) {
|
||||
currentListeners.forEach(listener => {
|
||||
listener(value);
|
||||
});
|
||||
}
|
||||
|
||||
// Return new configuration object
|
||||
return {
|
||||
...previous,
|
||||
[name]: value,
|
||||
};
|
||||
}, {});
|
||||
|
||||
window.storage.put('remoteConfig', config);
|
||||
};
|
||||
|
||||
export const maybeRefreshRemoteConfig = throttle(
|
||||
refreshRemoteConfig,
|
||||
// Only fetch remote configuration if the last fetch was more than two hours ago
|
||||
2 * 60 * 60 * 1000,
|
||||
{ trailing: false }
|
||||
);
|
||||
|
||||
export function isEnabled(name: ConfigKeyType): boolean {
|
||||
return get(config, [name, 'enabled'], false);
|
||||
}
|
|
@ -2,6 +2,13 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="blue"
|
||||
avatarPath={util.gifObjectUrl}
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar
|
||||
size={80}
|
||||
color="blue"
|
||||
|
@ -90,6 +97,13 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="blue"
|
||||
name="One"
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar
|
||||
size={80}
|
||||
color="blue"
|
||||
|
@ -143,6 +157,14 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="pink"
|
||||
noteToSelf={true}
|
||||
phoneNumber="(555) 353-3433"
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar
|
||||
size={80}
|
||||
color="pink"
|
||||
|
@ -174,6 +196,7 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar size={112} color="blue" conversationType="group" i18n={util.i18n} />
|
||||
<Avatar size={80} color="blue" conversationType="group" i18n={util.i18n} />
|
||||
<Avatar size={52} color="blue" conversationType="group" i18n={util.i18n} />
|
||||
<Avatar size={28} color="blue" conversationType="group" i18n={util.i18n} />
|
||||
|
@ -184,6 +207,7 @@
|
|||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar size={112} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||
<Avatar size={80} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||
<Avatar size={52} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||
<Avatar size={28} color="blue" conversationType="direct" i18n={util.i18n} />
|
||||
|
@ -361,6 +385,43 @@
|
|||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### 112px
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme} ios={util.ios}>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="teal"
|
||||
name="John Smith"
|
||||
avatarPath={util.gifObjectUrl}
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="teal"
|
||||
name="John"
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar
|
||||
size={112}
|
||||
color="teal"
|
||||
name="John Smith"
|
||||
conversationType="direct"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
<Avatar size={112} color="teal" conversationType="direct" i18n={util.i18n} />
|
||||
<Avatar
|
||||
size={112}
|
||||
color="teal"
|
||||
name="Pupplies"
|
||||
conversationType="group"
|
||||
i18n={util.i18n}
|
||||
/>
|
||||
</util.ConversationContext>
|
||||
```
|
||||
|
||||
### Broken color
|
||||
|
||||
```jsx
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React from 'react';
|
||||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { getInitials } from '../util/getInitials';
|
||||
import { ColorType, LocalizerType } from '../types/Util';
|
||||
|
||||
export interface Props {
|
||||
export type Props = {
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
|
||||
|
@ -13,7 +13,7 @@ export interface Props {
|
|||
name?: string;
|
||||
phoneNumber?: string;
|
||||
profileName?: string;
|
||||
size: 28 | 32 | 52 | 80;
|
||||
size: 28 | 32 | 52 | 80 | 112;
|
||||
|
||||
onClick?: () => unknown;
|
||||
|
||||
|
@ -21,7 +21,7 @@ export interface Props {
|
|||
innerRef?: React.Ref<HTMLDivElement>;
|
||||
|
||||
i18n: LocalizerType;
|
||||
}
|
||||
} & Pick<React.HTMLProps<HTMLDivElement>, 'className'>;
|
||||
|
||||
interface State {
|
||||
imageBroken: boolean;
|
||||
|
@ -139,12 +139,13 @@ export class Avatar extends React.Component<Props, State> {
|
|||
noteToSelf,
|
||||
onClick,
|
||||
size,
|
||||
className,
|
||||
} = this.props;
|
||||
const { imageBroken } = this.state;
|
||||
|
||||
const hasImage = !noteToSelf && avatarPath && !imageBroken;
|
||||
|
||||
if (![28, 32, 52, 80].includes(size)) {
|
||||
if (![28, 32, 52, 80, 112].includes(size)) {
|
||||
throw new Error(`Size ${size} is not supported!`);
|
||||
}
|
||||
|
||||
|
@ -166,7 +167,8 @@ export class Avatar extends React.Component<Props, State> {
|
|||
'module-avatar',
|
||||
`module-avatar--${size}`,
|
||||
hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
|
||||
!hasImage ? `module-avatar--${color}` : null
|
||||
!hasImage ? `module-avatar--${color}` : null,
|
||||
className
|
||||
)}
|
||||
ref={innerRef}
|
||||
>
|
||||
|
|
|
@ -16,11 +16,17 @@ import {
|
|||
InputApi,
|
||||
Props as CompositionInputProps,
|
||||
} from './CompositionInput';
|
||||
import {
|
||||
MessageRequestActions,
|
||||
Props as MessageRequestActionsProps,
|
||||
} from './conversation/MessageRequestActions';
|
||||
import { countStickers } from './stickers/lib';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly messageRequestsEnabled?: boolean;
|
||||
readonly acceptedMessageRequest?: boolean;
|
||||
readonly compositionApi?: React.MutableRefObject<{
|
||||
focusInput: () => void;
|
||||
isDirty: () => boolean;
|
||||
|
@ -66,6 +72,7 @@ export type Props = Pick<
|
|||
| 'showPickerHint'
|
||||
| 'clearShowPickerHint'
|
||||
> &
|
||||
MessageRequestActionsProps &
|
||||
OwnProps;
|
||||
|
||||
const emptyElement = (el: HTMLElement) => {
|
||||
|
@ -73,7 +80,7 @@ const emptyElement = (el: HTMLElement) => {
|
|||
el.innerHTML = '';
|
||||
};
|
||||
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
|
||||
export const CompositionArea = ({
|
||||
i18n,
|
||||
attachmentListEl,
|
||||
|
@ -86,6 +93,8 @@ export const CompositionArea = ({
|
|||
onEditorStateChange,
|
||||
onTextTooLong,
|
||||
startingText,
|
||||
clearQuotedMessage,
|
||||
getQuotedMessage,
|
||||
// EmojiButton
|
||||
onPickEmoji,
|
||||
onSetSkinTone,
|
||||
|
@ -104,8 +113,19 @@ export const CompositionArea = ({
|
|||
clearShowIntroduction,
|
||||
showPickerHint,
|
||||
clearShowPickerHint,
|
||||
clearQuotedMessage,
|
||||
getQuotedMessage,
|
||||
// Message Requests
|
||||
messageRequestsEnabled,
|
||||
acceptedMessageRequest,
|
||||
conversationType,
|
||||
isBlocked,
|
||||
name,
|
||||
onAccept,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onUnblock,
|
||||
onDelete,
|
||||
profileName,
|
||||
phoneNumber,
|
||||
}: Props) => {
|
||||
const [disabled, setDisabled] = React.useState(false);
|
||||
const [showMic, setShowMic] = React.useState(!startingText);
|
||||
|
@ -299,6 +319,24 @@ export const CompositionArea = ({
|
|||
};
|
||||
}, [setLarge]);
|
||||
|
||||
if ((!acceptedMessageRequest || isBlocked) && messageRequestsEnabled) {
|
||||
return (
|
||||
<MessageRequestActions
|
||||
i18n={i18n}
|
||||
conversationType={conversationType}
|
||||
isBlocked={isBlocked}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onUnblock={onUnblock}
|
||||
onDelete={onDelete}
|
||||
onAccept={onAccept}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="module-composition-area">
|
||||
<div className="module-composition-area__toggle-large">
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
#### All Options
|
||||
|
||||
```jsx
|
||||
<util.ConversationContext theme={util.theme}>
|
||||
<ConfirmationDialog
|
||||
i18n={util.i18n}
|
||||
onClose={() => console.log('onClose')}
|
||||
onAffirmative={() => console.log('onAffirmative')}
|
||||
affirmativeText="Affirm"
|
||||
onNegative={() => console.log('onNegative')}
|
||||
negativeText="Negate"
|
||||
>
|
||||
asdf child
|
||||
</ConfirmationDialog>
|
||||
</util.ConversationContext>
|
||||
```
|
40
ts/components/ConfirmationDialog.stories.tsx
Normal file
40
ts/components/ConfirmationDialog.stories.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import * as React from 'react';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../_locales/en/messages.json';
|
||||
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
storiesOf('Components/ConfirmationDialog', module).add(
|
||||
'ConfirmationDialog',
|
||||
() => {
|
||||
return (
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
onClose={action('onClose')}
|
||||
title={text('Title', 'Foo bar banana baz?')}
|
||||
actions={[
|
||||
{
|
||||
text: 'Negate',
|
||||
style: 'negative',
|
||||
action: action('negative'),
|
||||
},
|
||||
{
|
||||
text: 'Affirm',
|
||||
style: 'affirmative',
|
||||
action: action('affirmative'),
|
||||
},
|
||||
]}
|
||||
>
|
||||
{text('Child text', 'asdf blip')}
|
||||
</ConfirmationDialog>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -2,14 +2,18 @@ import * as React from 'react';
|
|||
import classNames from 'classnames';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type ActionSpec = {
|
||||
text: string;
|
||||
action: () => unknown;
|
||||
style?: 'affirmative' | 'negative';
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly children: React.ReactNode;
|
||||
readonly affirmativeText?: string;
|
||||
readonly onAffirmative?: () => unknown;
|
||||
readonly title?: string | React.ReactNode;
|
||||
readonly actions: Array<ActionSpec>;
|
||||
readonly onClose: () => unknown;
|
||||
readonly negativeText?: string;
|
||||
readonly onNegative?: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
|
@ -21,15 +25,7 @@ function focusRef(el: HTMLElement | null) {
|
|||
}
|
||||
|
||||
export const ConfirmationDialog = React.memo(
|
||||
({
|
||||
i18n,
|
||||
onClose,
|
||||
children,
|
||||
onAffirmative,
|
||||
onNegative,
|
||||
affirmativeText,
|
||||
negativeText,
|
||||
}: Props) => {
|
||||
({ i18n, onClose, children, title, actions }: Props) => {
|
||||
React.useEffect(() => {
|
||||
const handler = ({ key }: KeyboardEvent) => {
|
||||
if (key === 'Escape') {
|
||||
|
@ -52,22 +48,25 @@ export const ConfirmationDialog = React.memo(
|
|||
[onClose]
|
||||
);
|
||||
|
||||
const handleNegative = React.useCallback(() => {
|
||||
onClose();
|
||||
if (onNegative) {
|
||||
onNegative();
|
||||
}
|
||||
}, [onClose, onNegative]);
|
||||
|
||||
const handleAffirmative = React.useCallback(() => {
|
||||
onClose();
|
||||
if (onAffirmative) {
|
||||
onAffirmative();
|
||||
}
|
||||
}, [onClose, onAffirmative]);
|
||||
const handleAction = React.useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
onClose();
|
||||
if (e.currentTarget.dataset.action) {
|
||||
const actionIndex = parseInt(e.currentTarget.dataset.action, 10);
|
||||
const { action } = actions[actionIndex];
|
||||
action();
|
||||
}
|
||||
},
|
||||
[onClose, actions]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="module-confirmation-dialog__container">
|
||||
{title ? (
|
||||
<h1 className="module-confirmation-dialog__container__title">
|
||||
{title}
|
||||
</h1>
|
||||
) : null}
|
||||
<div className="module-confirmation-dialog__container__content">
|
||||
{children}
|
||||
</div>
|
||||
|
@ -79,28 +78,24 @@ export const ConfirmationDialog = React.memo(
|
|||
>
|
||||
{i18n('confirmation-dialog--Cancel')}
|
||||
</button>
|
||||
{onNegative && negativeText ? (
|
||||
{actions.map((action, i) => (
|
||||
<button
|
||||
onClick={handleNegative}
|
||||
key={i}
|
||||
onClick={handleAction}
|
||||
data-action={i}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
'module-confirmation-dialog__container__buttons__button--negative'
|
||||
action.style === 'affirmative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||
: null,
|
||||
action.style === 'negative'
|
||||
? 'module-confirmation-dialog__container__buttons__button--negative'
|
||||
: null
|
||||
)}
|
||||
>
|
||||
{negativeText}
|
||||
{action.text}
|
||||
</button>
|
||||
) : null}
|
||||
{onAffirmative && affirmativeText ? (
|
||||
<button
|
||||
onClick={handleAffirmative}
|
||||
className={classNames(
|
||||
'module-confirmation-dialog__container__buttons__button',
|
||||
'module-confirmation-dialog__container__buttons__button--affirmative'
|
||||
)}
|
||||
>
|
||||
{affirmativeText}
|
||||
</button>
|
||||
) : null}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,31 +1,21 @@
|
|||
import * as React from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ConfirmationDialog } from './ConfirmationDialog';
|
||||
import {
|
||||
ConfirmationDialog,
|
||||
Props as ConfirmationDialogProps,
|
||||
} from './ConfirmationDialog';
|
||||
import { LocalizerType } from '../types/Util';
|
||||
|
||||
export type OwnProps = {
|
||||
readonly i18n: LocalizerType;
|
||||
readonly children: React.ReactNode;
|
||||
readonly affirmativeText?: string;
|
||||
readonly onAffirmative?: () => unknown;
|
||||
readonly onClose: () => unknown;
|
||||
readonly negativeText?: string;
|
||||
readonly onNegative?: () => unknown;
|
||||
};
|
||||
|
||||
export type Props = OwnProps;
|
||||
export type Props = OwnProps & ConfirmationDialogProps;
|
||||
|
||||
export const ConfirmationModal = React.memo(
|
||||
// tslint:disable-next-line max-func-body-length
|
||||
({
|
||||
i18n,
|
||||
onClose,
|
||||
children,
|
||||
onAffirmative,
|
||||
onNegative,
|
||||
affirmativeText,
|
||||
negativeText,
|
||||
}: Props) => {
|
||||
({ i18n, onClose, children, ...rest }: Props) => {
|
||||
const [root, setRoot] = React.useState<HTMLElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -72,14 +62,7 @@ export const ConfirmationModal = React.memo(
|
|||
className="module-confirmation-dialog__overlay"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
onAffirmative={onAffirmative}
|
||||
onNegative={onNegative}
|
||||
affirmativeText={affirmativeText}
|
||||
negativeText={negativeText}
|
||||
>
|
||||
<ConfirmationDialog i18n={i18n} {...rest} onClose={onClose}>
|
||||
{children}
|
||||
</ConfirmationDialog>
|
||||
</div>,
|
||||
|
|
|
@ -25,7 +25,7 @@ export interface PropsType {
|
|||
phoneNumber: string;
|
||||
isMe: boolean;
|
||||
name?: string;
|
||||
color: ColorType;
|
||||
color?: ColorType;
|
||||
verified: boolean;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
|
||||
import { Emojify } from './Emojify';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
phoneNumber?: string;
|
||||
name?: string;
|
||||
profileName?: string;
|
||||
|
|
|
@ -64,6 +64,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
phoneNumber: '(202) 555-0001',
|
||||
id: '1',
|
||||
profileName: '🔥Flames🔥',
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -76,6 +77,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
name: 'Someone 🔥 Somewhere',
|
||||
phoneNumber: '(202) 555-0002',
|
||||
id: '2',
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -88,6 +90,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
phoneNumber: '(202) 555-0003',
|
||||
id: '3',
|
||||
profileName: '🔥Flames🔥',
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -97,6 +100,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
props: {
|
||||
phoneNumber: '(202) 555-0011',
|
||||
id: '11',
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -108,6 +112,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
color: 'deep_orange',
|
||||
phoneNumber: '(202) 555-0004',
|
||||
id: '4',
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -129,6 +134,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
value: 10,
|
||||
},
|
||||
],
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -159,6 +165,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
value: 10,
|
||||
},
|
||||
],
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -183,6 +190,7 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
value: 10,
|
||||
},
|
||||
],
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
@ -200,6 +208,25 @@ const stories: Array<ConversationHeaderStory> = [
|
|||
phoneNumber: '(202) 555-0007',
|
||||
id: '7',
|
||||
isMe: true,
|
||||
isAccepted: true,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Unaccepted',
|
||||
description: 'No safety number entry.',
|
||||
items: [
|
||||
{
|
||||
title: '1:1 conversation',
|
||||
props: {
|
||||
color: 'blue',
|
||||
phoneNumber: '(202) 555-0007',
|
||||
id: '7',
|
||||
isMe: false,
|
||||
isAccepted: false,
|
||||
...actionProps,
|
||||
...housekeepingProps,
|
||||
},
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface PropsData {
|
|||
color?: ColorType;
|
||||
avatarPath?: string;
|
||||
|
||||
isAccepted?: boolean;
|
||||
isVerified?: boolean;
|
||||
isMe?: boolean;
|
||||
isGroup?: boolean;
|
||||
|
@ -222,6 +223,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
public renderMenu(triggerId: string) {
|
||||
const {
|
||||
i18n,
|
||||
isAccepted,
|
||||
isMe,
|
||||
isGroup,
|
||||
isArchived,
|
||||
|
@ -241,7 +243,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
|
||||
return (
|
||||
<ContextMenu id={triggerId}>
|
||||
{leftGroup ? null : (
|
||||
{!leftGroup && isAccepted ? (
|
||||
<SubMenu title={disappearingTitle}>
|
||||
{(timerOptions || []).map(item => (
|
||||
<MenuItem
|
||||
|
@ -254,7 +256,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
</MenuItem>
|
||||
))}
|
||||
</SubMenu>
|
||||
)}
|
||||
) : null}
|
||||
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
|
||||
{isGroup ? (
|
||||
<MenuItem onClick={onShowGroupMembers}>
|
||||
|
@ -266,7 +268,7 @@ export class ConversationHeader extends React.Component<Props> {
|
|||
{i18n('showSafetyNumber')}
|
||||
</MenuItem>
|
||||
) : null}
|
||||
{!isGroup ? (
|
||||
{!isGroup && isAccepted ? (
|
||||
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
||||
) : null}
|
||||
{isArchived ? (
|
||||
|
|
130
ts/components/conversation/ConversationHero.stories.tsx
Normal file
130
ts/components/conversation/ConversationHero.stories.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { number as numberKnob, text } from '@storybook/addon-knobs';
|
||||
import { ConversationHero } from './ConversationHero';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const getName = () => text('name', 'Cayce Bollard');
|
||||
const getProfileName = () => text('profileName', 'Cayce Bollard');
|
||||
const getAvatarPath = () =>
|
||||
text('avatarPath', '/fixtures/kitten-4-112-112.jpg');
|
||||
const getPhoneNumber = () => text('phoneNumber', '+1 (646) 327-2700');
|
||||
|
||||
storiesOf('Components/Conversation/ConversationHero', module)
|
||||
.add('Direct (Three Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
groups={['NYC Rock Climbers', 'Dinner Party', 'Friends 🌿']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Direct (Two Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
groups={['NYC Rock Climbers', 'Dinner Party']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Direct (One Other Group)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
groups={['NYC Rock Climbers']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Direct (No Other Groups)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
avatarPath={getAvatarPath()}
|
||||
name={getName()}
|
||||
profileName={getProfileName()}
|
||||
phoneNumber={getPhoneNumber()}
|
||||
conversationType="direct"
|
||||
groups={[]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group (many members)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||
conversationType="group"
|
||||
membersCount={numberKnob('membersCount', 22)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group (one member)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||
conversationType="group"
|
||||
membersCount={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group (zero members)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
name={text('groupName', 'NYC Rock Climbers')}
|
||||
phoneNumber={text('phoneNumber', '+1 (646) 327-2700')}
|
||||
conversationType="group"
|
||||
membersCount={0}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Note to Self', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<ConversationHero
|
||||
i18n={i18n}
|
||||
isMe={true}
|
||||
conversationType="direct"
|
||||
phoneNumber={getPhoneNumber()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
125
ts/components/conversation/ConversationHero.tsx
Normal file
125
ts/components/conversation/ConversationHero.tsx
Normal file
|
@ -0,0 +1,125 @@
|
|||
import * as React from 'react';
|
||||
import { take } from 'lodash';
|
||||
import { Avatar, Props as AvatarProps } from '../Avatar';
|
||||
import { ContactName } from './ContactName';
|
||||
import { Emojify } from './Emojify';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
isMe?: boolean;
|
||||
groups?: Array<string>;
|
||||
membersCount?: number;
|
||||
phoneNumber: string;
|
||||
onHeightChange?: () => unknown;
|
||||
} & Omit<AvatarProps, 'onClick' | 'size' | 'noteToSelf'>;
|
||||
|
||||
const renderMembershipRow = ({
|
||||
i18n,
|
||||
groups,
|
||||
conversationType,
|
||||
isMe,
|
||||
}: Pick<Props, 'i18n' | 'groups' | 'conversationType' | 'isMe'>) => {
|
||||
const className = 'module-conversation-hero__membership';
|
||||
const nameClassName = `${className}__name`;
|
||||
|
||||
if (isMe) {
|
||||
return <div className={className}>{i18n('noteToSelfHero')}</div>;
|
||||
}
|
||||
|
||||
if (conversationType === 'direct' && groups && groups.length > 0) {
|
||||
const firstThreeGroups = take(groups, 3).map((group, i) => (
|
||||
<strong key={i} className={nameClassName}>
|
||||
<Emojify text={group} />
|
||||
</strong>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={`ConversationHero--membership-${firstThreeGroups.length}`}
|
||||
components={firstThreeGroups}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const ConversationHero = ({
|
||||
i18n,
|
||||
avatarPath,
|
||||
color,
|
||||
conversationType,
|
||||
isMe,
|
||||
membersCount,
|
||||
groups = [],
|
||||
name,
|
||||
phoneNumber,
|
||||
profileName,
|
||||
onHeightChange,
|
||||
}: Props) => {
|
||||
const firstRenderRef = React.useRef(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
// If any of the depenencies for this hook change then the height of this
|
||||
// component may have changed. The cleanup function notifies listeners of
|
||||
// any potential height changes.
|
||||
return () => {
|
||||
if (onHeightChange && !firstRenderRef.current) {
|
||||
onHeightChange();
|
||||
} else {
|
||||
firstRenderRef.current = false;
|
||||
}
|
||||
};
|
||||
}, [
|
||||
firstRenderRef,
|
||||
onHeightChange,
|
||||
// Avoid collisions in these dependencies by prefixing them
|
||||
// These dependencies may be dynamic, and therefore may cause height changes
|
||||
`mc-${membersCount}`,
|
||||
`n-${name}`,
|
||||
`pn-${profileName}`,
|
||||
...groups.map(g => `g-${g}`),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="module-conversation-hero">
|
||||
<Avatar
|
||||
i18n={i18n}
|
||||
color={color}
|
||||
noteToSelf={isMe}
|
||||
avatarPath={avatarPath}
|
||||
conversationType={conversationType}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
size={112}
|
||||
className="module-conversation-hero__avatar"
|
||||
/>
|
||||
<h1 className="module-conversation-hero__profile-name">
|
||||
{isMe ? (
|
||||
i18n('noteToSelf')
|
||||
) : (
|
||||
<ContactName
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>
|
||||
)}
|
||||
</h1>
|
||||
{!isMe ? (
|
||||
<div className="module-conversation-hero__with">
|
||||
{membersCount === 1
|
||||
? i18n('ConversationHero--members-1')
|
||||
: membersCount !== undefined
|
||||
? i18n('ConversationHero--members', [`${membersCount}`])
|
||||
: phoneNumber}
|
||||
</div>
|
||||
) : null}
|
||||
{renderMembershipRow({ isMe, groups, conversationType, i18n })}
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Blurhash } from 'react-blurhash';
|
||||
|
||||
import { Spinner } from '../Spinner';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
@ -30,6 +31,7 @@ interface Props {
|
|||
darkOverlay?: boolean;
|
||||
playIconOverlay?: boolean;
|
||||
softCorners?: boolean;
|
||||
blurHash?: string;
|
||||
|
||||
i18n: LocalizerType;
|
||||
onClick?: (attachment: AttachmentType) => void;
|
||||
|
@ -38,7 +40,21 @@ interface Props {
|
|||
}
|
||||
|
||||
export class Image extends React.Component<Props> {
|
||||
private canClick() {
|
||||
const { onClick, attachment, url } = this.props;
|
||||
const { pending } = attachment || { pending: true };
|
||||
|
||||
return Boolean(onClick && !pending && url);
|
||||
}
|
||||
|
||||
public handleClick = (event: React.MouseEvent) => {
|
||||
if (!this.canClick()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { onClick, attachment } = this.props;
|
||||
|
||||
if (onClick) {
|
||||
|
@ -50,6 +66,13 @@ export class Image extends React.Component<Props> {
|
|||
};
|
||||
|
||||
public handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (!this.canClick()) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { onClick, attachment } = this.props;
|
||||
|
||||
if (onClick && (event.key === 'Enter' || event.key === 'Space')) {
|
||||
|
@ -64,6 +87,7 @@ export class Image extends React.Component<Props> {
|
|||
const {
|
||||
alt,
|
||||
attachment,
|
||||
blurHash,
|
||||
bottomOverlay,
|
||||
closeButton,
|
||||
curveBottomLeft,
|
||||
|
@ -71,11 +95,10 @@ export class Image extends React.Component<Props> {
|
|||
curveTopLeft,
|
||||
curveTopRight,
|
||||
darkOverlay,
|
||||
height,
|
||||
height = 0,
|
||||
i18n,
|
||||
noBackground,
|
||||
noBorder,
|
||||
onClick,
|
||||
onClickClose,
|
||||
onError,
|
||||
overlayText,
|
||||
|
@ -84,18 +107,16 @@ export class Image extends React.Component<Props> {
|
|||
softCorners,
|
||||
tabIndex,
|
||||
url,
|
||||
width,
|
||||
width = 0,
|
||||
} = this.props;
|
||||
|
||||
const { caption, pending } = attachment || { caption: null, pending: true };
|
||||
const canClick = onClick && !pending;
|
||||
const canClick = this.canClick();
|
||||
|
||||
const overlayClassName = classNames(
|
||||
'module-image__border-overlay',
|
||||
noBorder ? null : 'module-image__border-overlay--with-border',
|
||||
canClick && onClick
|
||||
? 'module-image__border-overlay--with-click-handler'
|
||||
: null,
|
||||
canClick ? 'module-image__border-overlay--with-click-handler' : null,
|
||||
curveTopLeft ? 'module-image--curved-top-left' : null,
|
||||
curveTopRight ? 'module-image--curved-top-right' : null,
|
||||
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
|
||||
|
@ -105,19 +126,14 @@ export class Image extends React.Component<Props> {
|
|||
darkOverlay ? 'module-image__border-overlay--dark' : null
|
||||
);
|
||||
|
||||
let overlay;
|
||||
if (canClick && onClick) {
|
||||
overlay = (
|
||||
<button
|
||||
className={overlayClassName}
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
overlay = <div className={overlayClassName} />;
|
||||
}
|
||||
const overlay = canClick ? (
|
||||
<button
|
||||
className={overlayClassName}
|
||||
onClick={this.handleClick}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -145,7 +161,7 @@ export class Image extends React.Component<Props> {
|
|||
>
|
||||
<Spinner svgSize="normal" />
|
||||
</div>
|
||||
) : (
|
||||
) : url ? (
|
||||
<img
|
||||
onError={onError}
|
||||
className="module-image__image"
|
||||
|
@ -154,7 +170,14 @@ export class Image extends React.Component<Props> {
|
|||
width={width}
|
||||
src={url}
|
||||
/>
|
||||
)}
|
||||
) : blurHash ? (
|
||||
<Blurhash
|
||||
hash={blurHash}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
) : null}
|
||||
{caption ? (
|
||||
<img
|
||||
className="module-image__caption-icon"
|
||||
|
|
|
@ -76,6 +76,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[0].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
noBackground={isSticker}
|
||||
|
@ -103,6 +104,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
attachment={attachments[0]}
|
||||
blurHash={attachments[0].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveTopLeft={curveTopLeft}
|
||||
|
@ -117,6 +119,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[1].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveTopRight={curveTopRight}
|
||||
|
@ -139,6 +142,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[0].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveTopLeft={curveTopLeft}
|
||||
|
@ -155,6 +159,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[1].blurHash}
|
||||
curveTopRight={curveTopRight}
|
||||
height={99}
|
||||
width={99}
|
||||
|
@ -167,6 +172,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[2].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveBottomRight={curveBottomRight}
|
||||
|
@ -191,6 +197,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[0].blurHash}
|
||||
curveTopLeft={curveTopLeft}
|
||||
noBorder={false}
|
||||
attachment={attachments[0]}
|
||||
|
@ -204,6 +211,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[1].blurHash}
|
||||
curveTopRight={curveTopRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
noBorder={false}
|
||||
|
@ -219,6 +227,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[2].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
|
@ -233,6 +242,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[3].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={false}
|
||||
curveBottomRight={curveBottomRight}
|
||||
|
@ -262,6 +272,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[0], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[0].blurHash}
|
||||
curveTopLeft={curveTopLeft}
|
||||
attachment={attachments[0]}
|
||||
playIconOverlay={isVideoAttachment(attachments[0])}
|
||||
|
@ -274,6 +285,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[1], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[1].blurHash}
|
||||
curveTopRight={curveTopRight}
|
||||
playIconOverlay={isVideoAttachment(attachments[1])}
|
||||
height={149}
|
||||
|
@ -288,6 +300,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[2], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[2].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomLeft={curveBottomLeft}
|
||||
|
@ -302,6 +315,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[3], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[3].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
playIconOverlay={isVideoAttachment(attachments[3])}
|
||||
|
@ -315,6 +329,7 @@ export class ImageGrid extends React.Component<Props> {
|
|||
<Image
|
||||
alt={getAlt(attachments[4], i18n)}
|
||||
i18n={i18n}
|
||||
blurHash={attachments[4].blurHash}
|
||||
bottomOverlay={withBottomOverlay}
|
||||
noBorder={isSticker}
|
||||
curveBottomRight={curveBottomRight}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
PropsHousekeeping,
|
||||
} from './Message';
|
||||
import { EmojiPicker } from '../emoji/EmojiPicker';
|
||||
import { MIMEType } from '../../types/MIME';
|
||||
|
||||
const book = storiesOf('Components/Conversation/Message', module);
|
||||
|
||||
|
@ -1272,6 +1273,28 @@ const stories: Array<MessageStory> = [
|
|||
})),
|
||||
],
|
||||
],
|
||||
[
|
||||
'BlurHash',
|
||||
[
|
||||
{
|
||||
title: 'Incoming BlurHash',
|
||||
makeDataProps: () => ({
|
||||
...baseDataProps,
|
||||
direction: 'incoming',
|
||||
attachments: [
|
||||
{
|
||||
blurHash: 'LEHV6nWB2yk8pyo0adR*.7kCMdnj',
|
||||
width: 300,
|
||||
height: 600,
|
||||
fileName: 'foo.jpg',
|
||||
contentType: 'image/jpeg' as MIMEType,
|
||||
url: '',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
const renderEmojiPicker: AllProps['renderEmojiPicker'] = ({
|
||||
|
|
|
@ -573,6 +573,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
withContentBelow
|
||||
? 'module-message__attachment-container--with-content-below'
|
||||
: null,
|
||||
collapseMetadata
|
||||
? 'module-message__attachment-container--with-collapsed-metadata'
|
||||
: null,
|
||||
isSticker && !collapseMetadata
|
||||
? 'module-message__sticker-container--with-content-below'
|
||||
: null
|
||||
|
@ -627,6 +630,9 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
: null,
|
||||
withContentAbove
|
||||
? 'module-message__generic-attachment--with-content-above'
|
||||
: null,
|
||||
!firstAttachment.url
|
||||
? 'module-message__generic-attachment--not-active'
|
||||
: null
|
||||
)}
|
||||
// There's only ever one of these, so we don't want users to tab into it
|
||||
|
@ -635,6 +641,10 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
|
||||
if (!firstAttachment.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openGenericAttachment();
|
||||
}}
|
||||
>
|
||||
|
@ -1117,7 +1127,7 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
)}
|
||||
>
|
||||
{canReply ? reactButton : null}
|
||||
{downloadButton}
|
||||
{canReply ? downloadButton : null}
|
||||
{canReply ? replyButton : null}
|
||||
{menuButton}
|
||||
</div>
|
||||
|
@ -1881,6 +1891,15 @@ export class Message extends React.PureComponent<Props, State> {
|
|||
return;
|
||||
}
|
||||
|
||||
// If there an incomplete attachment, do not execute the default action
|
||||
const { attachments } = this.props;
|
||||
if (attachments && attachments.length > 0) {
|
||||
const [firstAttachment] = attachments;
|
||||
if (!firstAttachment.url) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.handleOpen(event);
|
||||
};
|
||||
|
||||
|
|
59
ts/components/conversation/MessageRequestActions.stories.tsx
Normal file
59
ts/components/conversation/MessageRequestActions.stories.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import * as React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import {
|
||||
MessageRequestActions,
|
||||
Props as MessageRequestActionsProps,
|
||||
} from './MessageRequestActions';
|
||||
|
||||
// @ts-ignore
|
||||
import { setup as setupI18n } from '../../../js/modules/i18n';
|
||||
// @ts-ignore
|
||||
import enMessages from '../../../_locales/en/messages.json';
|
||||
|
||||
const i18n = setupI18n('en', enMessages);
|
||||
|
||||
const getBaseProps = (isGroup = false): MessageRequestActionsProps => ({
|
||||
i18n,
|
||||
conversationType: isGroup ? 'group' : 'direct',
|
||||
profileName: isGroup ? undefined : text('profileName', 'Cayce Bollard'),
|
||||
name: isGroup
|
||||
? text('name', 'NYC Rock Climbers')
|
||||
: text('name', 'Cayce Bollard'),
|
||||
onBlock: action('block'),
|
||||
onDelete: action('delete'),
|
||||
onBlockAndDelete: action('blockAndDelete'),
|
||||
onUnblock: action('unblock'),
|
||||
onAccept: action('accept'),
|
||||
});
|
||||
|
||||
storiesOf('Components/Conversation/MessageRequestActions', module)
|
||||
.add('Direct', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MessageRequestActions {...getBaseProps()} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Direct (Blocked)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MessageRequestActions {...getBaseProps()} isBlocked={true} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MessageRequestActions {...getBaseProps(true)} />
|
||||
</div>
|
||||
);
|
||||
})
|
||||
.add('Group (Blocked)', () => {
|
||||
return (
|
||||
<div style={{ width: '480px' }}>
|
||||
<MessageRequestActions {...getBaseProps(true)} isBlocked={true} />
|
||||
</div>
|
||||
);
|
||||
});
|
130
ts/components/conversation/MessageRequestActions.tsx
Normal file
130
ts/components/conversation/MessageRequestActions.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import * as React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { ContactName, Props as ContactNameProps } from './ContactName';
|
||||
import {
|
||||
MessageRequestActionsConfirmation,
|
||||
MessageRequestState,
|
||||
Props as MessageRequestActionsConfirmationProps,
|
||||
} from './MessageRequestActionsConfirmation';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
onAccept(): unknown;
|
||||
} & Omit<ContactNameProps, 'module'> &
|
||||
Omit<
|
||||
MessageRequestActionsConfirmationProps,
|
||||
'i18n' | 'state' | 'onChangeState'
|
||||
>;
|
||||
|
||||
export const MessageRequestActions = ({
|
||||
i18n,
|
||||
name,
|
||||
profileName,
|
||||
phoneNumber,
|
||||
conversationType,
|
||||
isBlocked,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onUnblock,
|
||||
onDelete,
|
||||
onAccept,
|
||||
}: Props) => {
|
||||
const [mrState, setMrState] = React.useState(MessageRequestState.default);
|
||||
|
||||
return (
|
||||
<>
|
||||
{mrState !== MessageRequestState.default ? (
|
||||
<MessageRequestActionsConfirmation
|
||||
i18n={i18n}
|
||||
onBlock={onBlock}
|
||||
onBlockAndDelete={onBlockAndDelete}
|
||||
onUnblock={onUnblock}
|
||||
onDelete={onDelete}
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
conversationType={conversationType}
|
||||
state={mrState}
|
||||
onChangeState={setMrState}
|
||||
/>
|
||||
) : null}
|
||||
<div className="module-message-request-actions">
|
||||
<p className="module-message-request-actions__message">
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={`MessageRequests--message-${conversationType}${
|
||||
isBlocked ? '-blocked' : ''
|
||||
}`}
|
||||
components={[
|
||||
<strong
|
||||
key="name"
|
||||
className="module-message-request-actions__message__name"
|
||||
>
|
||||
<ContactName
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>
|
||||
</strong>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<div className="module-message-request-actions__buttons">
|
||||
{isBlocked ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.unblocking);
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--accept'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--unblock')}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.blocking);
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--deny'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--block')}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setMrState(MessageRequestState.deleting);
|
||||
}}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--deny'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--delete')}
|
||||
</button>
|
||||
{!isBlocked ? (
|
||||
<button
|
||||
onClick={onAccept}
|
||||
tabIndex={0}
|
||||
className={classNames(
|
||||
'module-message-request-actions__buttons__button',
|
||||
'module-message-request-actions__buttons__button--accept'
|
||||
)}
|
||||
>
|
||||
{i18n('MessageRequests--accept')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
156
ts/components/conversation/MessageRequestActionsConfirmation.tsx
Normal file
156
ts/components/conversation/MessageRequestActionsConfirmation.tsx
Normal file
|
@ -0,0 +1,156 @@
|
|||
import * as React from 'react';
|
||||
import { ContactName, Props as ContactNameProps } from './ContactName';
|
||||
import { ConfirmationModal } from '../ConfirmationModal';
|
||||
import { Intl } from '../Intl';
|
||||
import { LocalizerType } from '../../types/Util';
|
||||
|
||||
export enum MessageRequestState {
|
||||
blocking,
|
||||
deleting,
|
||||
unblocking,
|
||||
default,
|
||||
}
|
||||
|
||||
export type Props = {
|
||||
i18n: LocalizerType;
|
||||
conversationType: 'group' | 'direct';
|
||||
isBlocked?: boolean;
|
||||
onBlock(): unknown;
|
||||
onBlockAndDelete(): unknown;
|
||||
onUnblock(): unknown;
|
||||
onDelete(): unknown;
|
||||
state: MessageRequestState;
|
||||
onChangeState(state: MessageRequestState): unknown;
|
||||
} & Omit<ContactNameProps, 'module'>;
|
||||
|
||||
// tslint:disable-next-line: max-func-body-length
|
||||
export const MessageRequestActionsConfirmation = ({
|
||||
i18n,
|
||||
name,
|
||||
profileName,
|
||||
phoneNumber,
|
||||
conversationType,
|
||||
onBlock,
|
||||
onBlockAndDelete,
|
||||
onUnblock,
|
||||
onDelete,
|
||||
state,
|
||||
onChangeState,
|
||||
}: Props) => {
|
||||
if (state === MessageRequestState.blocking) {
|
||||
return (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<ConfirmationModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={`MessageRequests--block-${conversationType}-confirm-title`}
|
||||
components={[
|
||||
<ContactName
|
||||
key="name"
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('MessageRequests--block'),
|
||||
action: onBlock,
|
||||
style: 'negative',
|
||||
},
|
||||
{
|
||||
text: i18n('MessageRequests--block-and-delete'),
|
||||
action: onBlockAndDelete,
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n(`MessageRequests--block-${conversationType}-confirm-body`)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.unblocking) {
|
||||
return (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<ConfirmationModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={'MessageRequests--unblock-confirm-title'}
|
||||
components={[
|
||||
<ContactName
|
||||
key="name"
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: i18n('MessageRequests--unblock'),
|
||||
action: onUnblock,
|
||||
style: 'affirmative',
|
||||
},
|
||||
{
|
||||
text: i18n('MessageRequests--delete'),
|
||||
action: onDelete,
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n(`MessageRequests--unblock-${conversationType}-confirm-body`)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
}
|
||||
|
||||
if (state === MessageRequestState.deleting) {
|
||||
return (
|
||||
// tslint:disable-next-line: use-simple-attributes
|
||||
<ConfirmationModal
|
||||
i18n={i18n}
|
||||
onClose={() => {
|
||||
onChangeState(MessageRequestState.default);
|
||||
}}
|
||||
title={
|
||||
<Intl
|
||||
i18n={i18n}
|
||||
id={`MessageRequests--delete-${conversationType}-confirm-title`}
|
||||
components={[
|
||||
<ContactName
|
||||
key="name"
|
||||
name={name}
|
||||
profileName={profileName}
|
||||
phoneNumber={phoneNumber}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
text: i18n(`MessageRequests--delete-${conversationType}`),
|
||||
action: onDelete,
|
||||
style: 'negative',
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n(`MessageRequests--delete-${conversationType}-confirm-body`)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -50,6 +50,7 @@ type PropsHousekeepingType = {
|
|||
actions: Object
|
||||
) => JSX.Element;
|
||||
renderLastSeenIndicator: (id: string) => JSX.Element;
|
||||
renderHeroRow: (id: string, resizeHeroRow: () => unknown) => JSX.Element;
|
||||
renderLoadingRow: (id: string) => JSX.Element;
|
||||
renderTypingBubble: (id: string) => JSX.Element;
|
||||
};
|
||||
|
@ -250,6 +251,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
this.recomputeRowHeights(row || 0);
|
||||
};
|
||||
|
||||
public resizeHeroRow = () => {
|
||||
this.resize(0);
|
||||
};
|
||||
|
||||
public onScroll = (data: OnScrollParamsType) => {
|
||||
// Ignore scroll events generated as react-virtualized recursively scrolls and
|
||||
// re-measures to get us where we want to go.
|
||||
|
@ -501,6 +506,7 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
haveOldest,
|
||||
items,
|
||||
renderItem,
|
||||
renderHeroRow,
|
||||
renderLoadingRow,
|
||||
renderLastSeenIndicator,
|
||||
renderTypingBubble,
|
||||
|
@ -515,7 +521,13 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
const typingBubbleRow = this.getTypingBubbleRow();
|
||||
let rowContents;
|
||||
|
||||
if (!haveOldest && row === 0) {
|
||||
if (haveOldest && row === 0) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={styleWithWidth} role="row">
|
||||
{renderHeroRow(id, this.resizeHeroRow)}
|
||||
</div>
|
||||
);
|
||||
} else if (!haveOldest && row === 0) {
|
||||
rowContents = (
|
||||
<div data-row={row} style={styleWithWidth} role="row">
|
||||
{renderLoadingRow(id)}
|
||||
|
@ -574,13 +586,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
};
|
||||
|
||||
public fromItemIndexToRow(index: number) {
|
||||
const { haveOldest, oldestUnreadIndex } = this.props;
|
||||
const { oldestUnreadIndex } = this.props;
|
||||
|
||||
let addition = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
addition += 1;
|
||||
}
|
||||
// We will always render either the hero row or the loading row
|
||||
let addition = 1;
|
||||
|
||||
if (isNumber(oldestUnreadIndex) && index >= oldestUnreadIndex) {
|
||||
addition += 1;
|
||||
|
@ -590,15 +599,12 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public getRowCount() {
|
||||
const { haveOldest, oldestUnreadIndex, typingContact } = this.props;
|
||||
const { oldestUnreadIndex, typingContact } = this.props;
|
||||
const { items } = this.props;
|
||||
const itemsCount = items && items.length ? items.length : 0;
|
||||
|
||||
let extraRows = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
extraRows += 1;
|
||||
}
|
||||
// We will always render either the hero row or the loading row
|
||||
let extraRows = 1;
|
||||
|
||||
if (isNumber(oldestUnreadIndex)) {
|
||||
extraRows += 1;
|
||||
|
@ -612,13 +618,10 @@ export class Timeline extends React.PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public fromRowToItemIndex(row: number, props?: Props): number | undefined {
|
||||
const { haveOldest, items } = props || this.props;
|
||||
const { items } = props || this.props;
|
||||
|
||||
let subtraction = 0;
|
||||
|
||||
if (!haveOldest) {
|
||||
subtraction += 1;
|
||||
}
|
||||
// We will always render either the hero row or the loading row
|
||||
let subtraction = 1;
|
||||
|
||||
const oldestUnreadRow = this.getLastSeenIndicatorRow();
|
||||
if (isNumber(oldestUnreadRow) && row > oldestUnreadRow) {
|
||||
|
|
|
@ -30,6 +30,7 @@ const renderEmojiPicker: TimelineItemProps['renderEmojiPicker'] = ({
|
|||
|
||||
const getDefaultProps = () => ({
|
||||
conversationId: 'conversation-id',
|
||||
conversationAccepted: true,
|
||||
id: 'asdf',
|
||||
isSelected: false,
|
||||
selectMessage: action('selectMessage'),
|
||||
|
|
|
@ -77,6 +77,7 @@ export type TimelineItemType =
|
|||
|
||||
type PropsLocalType = {
|
||||
conversationId: string;
|
||||
conversationAccepted: boolean;
|
||||
item?: TimelineItemType;
|
||||
id: string;
|
||||
isSelected: boolean;
|
||||
|
|
|
@ -92,8 +92,13 @@ export const StickerManagerPackRow = React.memo(
|
|||
<ConfirmationModal
|
||||
i18n={i18n}
|
||||
onClose={clearUninstalling}
|
||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||
onNegative={handleConfirmUninstall}
|
||||
actions={[
|
||||
{
|
||||
style: 'negative',
|
||||
text: i18n('stickers--StickerManager--Uninstall'),
|
||||
action: handleConfirmUninstall,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||
</ConfirmationModal>
|
||||
|
|
|
@ -174,8 +174,13 @@ export const StickerPreviewModal = React.memo(
|
|||
<ConfirmationDialog
|
||||
i18n={i18n}
|
||||
onClose={onClose}
|
||||
negativeText={i18n('stickers--StickerManager--Uninstall')}
|
||||
onNegative={handleUninstall}
|
||||
actions={[
|
||||
{
|
||||
style: 'negative',
|
||||
text: i18n('stickers--StickerManager--Uninstall'),
|
||||
action: handleUninstall,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{i18n('stickers--StickerManager--UninstallWarning')}
|
||||
</ConfirmationDialog>
|
||||
|
|
|
@ -852,8 +852,8 @@ async function searchMessagesInConversation(
|
|||
|
||||
// Message
|
||||
|
||||
async function getMessageCount() {
|
||||
return channels.getMessageCount();
|
||||
async function getMessageCount(conversationId?: string) {
|
||||
return channels.getMessageCount(conversationId);
|
||||
}
|
||||
|
||||
async function saveMessage(
|
||||
|
|
|
@ -85,7 +85,7 @@ export interface DataInterface {
|
|||
options?: { limit?: number }
|
||||
) => Promise<Array<SearchResultMessageType>>;
|
||||
|
||||
getMessageCount: () => Promise<number>;
|
||||
getMessageCount: (conversationId?: string) => Promise<number>;
|
||||
saveMessages: (
|
||||
arrayOfMessages: Array<MessageType>,
|
||||
options: { forceSave?: boolean }
|
||||
|
|
|
@ -1542,6 +1542,40 @@ async function updateToSchemaVersion20(
|
|||
}
|
||||
}
|
||||
|
||||
async function updateToSchemaVersion21(
|
||||
currentVersion: number,
|
||||
instance: PromisifiedSQLDatabase
|
||||
) {
|
||||
if (currentVersion >= 21) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await instance.run('BEGIN TRANSACTION;');
|
||||
await instance.run(`
|
||||
UPDATE conversations
|
||||
SET json = json_set(
|
||||
json,
|
||||
'$.messageCount',
|
||||
(SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id)
|
||||
);
|
||||
`);
|
||||
await instance.run(`
|
||||
UPDATE conversations
|
||||
SET json = json_set(
|
||||
json,
|
||||
'$.sentMessageCount',
|
||||
(SELECT count(*) FROM messages WHERE messages.conversationId = conversations.id AND messages.type = 'outgoing')
|
||||
);
|
||||
`);
|
||||
await instance.run('PRAGMA user_version = 21;');
|
||||
await instance.run('COMMIT TRANSACTION;');
|
||||
console.log('updateToSchemaVersion21: success!');
|
||||
} catch (error) {
|
||||
await instance.run('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const SCHEMA_VERSIONS = [
|
||||
updateToSchemaVersion1,
|
||||
updateToSchemaVersion2,
|
||||
|
@ -1563,6 +1597,7 @@ const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion18,
|
||||
updateToSchemaVersion19,
|
||||
updateToSchemaVersion20,
|
||||
updateToSchemaVersion21,
|
||||
];
|
||||
|
||||
async function updateSchema(instance: PromisifiedSQLDatabase) {
|
||||
|
@ -2326,9 +2361,14 @@ async function searchMessagesInConversation(
|
|||
}));
|
||||
}
|
||||
|
||||
async function getMessageCount() {
|
||||
async function getMessageCount(conversationId?: string) {
|
||||
const db = getInstance();
|
||||
const row = await db.get('SELECT count(*) from messages;');
|
||||
const row = conversationId
|
||||
? await db.get(
|
||||
'SELECT count(*) from messages WHERE conversationId = $conversationId;',
|
||||
{ $conversationId: conversationId }
|
||||
)
|
||||
: await db.get('SELECT count(*) from messages;');
|
||||
|
||||
if (!row) {
|
||||
throw new Error('getMessageCount: Unable to get count of messages');
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
import { trigger } from '../../shims/events';
|
||||
import { NoopActionType } from './noop';
|
||||
import { AttachmentType } from '../../types/Attachment';
|
||||
import { ColorType } from '../../types/Util';
|
||||
|
||||
// State
|
||||
|
||||
|
@ -24,7 +25,11 @@ export type DBConversationType = {
|
|||
export type ConversationType = {
|
||||
id: string;
|
||||
name?: string;
|
||||
isArchived: boolean;
|
||||
profileName?: string;
|
||||
avatarPath?: string;
|
||||
color?: ColorType;
|
||||
isArchived?: boolean;
|
||||
isBlocked?: boolean;
|
||||
activeAt?: number;
|
||||
timestamp: number;
|
||||
inboxPosition: number;
|
||||
|
@ -33,6 +38,7 @@ export type ConversationType = {
|
|||
text: string;
|
||||
};
|
||||
phoneNumber: string;
|
||||
membersCount?: number;
|
||||
type: 'direct' | 'group';
|
||||
isMe: boolean;
|
||||
lastUpdated: number;
|
||||
|
@ -49,6 +55,9 @@ export type ConversationType = {
|
|||
shouldShowDraft?: boolean;
|
||||
draftText?: string;
|
||||
draftPreview?: string;
|
||||
|
||||
messageRequestsEnabled?: boolean;
|
||||
acceptedMessageRequest?: boolean;
|
||||
};
|
||||
export type ConversationLookupType = {
|
||||
[key: string]: ConversationType;
|
||||
|
|
|
@ -71,6 +71,14 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
recentStickers,
|
||||
showIntroduction,
|
||||
showPickerHint,
|
||||
// Message Requests
|
||||
messageRequestsEnabled: conversation.messageRequestsEnabled,
|
||||
acceptedMessageRequest: conversation.acceptedMessageRequest,
|
||||
isBlocked: conversation.isBlocked,
|
||||
conversationType: conversation.type,
|
||||
name: conversation.name,
|
||||
profileName: conversation.profileName,
|
||||
phoneNumber: conversation.phoneNumber,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
37
ts/state/smart/HeroRow.tsx
Normal file
37
ts/state/smart/HeroRow.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { mapDispatchToProps } from '../actions';
|
||||
|
||||
import { ConversationHero } from '../../components/conversation/ConversationHero';
|
||||
|
||||
import { StateType } from '../reducer';
|
||||
import { getIntl } from '../selectors/user';
|
||||
|
||||
type ExternalProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
||||
const { id } = props;
|
||||
|
||||
const conversation = state.conversations.conversationLookup[id];
|
||||
|
||||
if (!conversation) {
|
||||
throw new Error(`Did not find conversation ${id} in state!`);
|
||||
}
|
||||
|
||||
return {
|
||||
i18n: getIntl(state),
|
||||
avatarPath: conversation.avatarPath,
|
||||
color: conversation.color,
|
||||
conversationType: conversation.type,
|
||||
isMe: conversation.isMe,
|
||||
membersCount: conversation.membersCount,
|
||||
name: conversation.name,
|
||||
phoneNumber: conversation.phoneNumber,
|
||||
profileName: conversation.profileName,
|
||||
};
|
||||
};
|
||||
|
||||
const smart = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export const SmartHeroRow = smart(ConversationHero);
|
|
@ -16,6 +16,7 @@ import {
|
|||
import { SmartTimelineItem } from './TimelineItem';
|
||||
import { SmartTypingBubble } from './TypingBubble';
|
||||
import { SmartLastSeenIndicator } from './LastSeenIndicator';
|
||||
import { SmartHeroRow } from './HeroRow';
|
||||
import { SmartTimelineLoadingRow } from './TimelineLoadingRow';
|
||||
import { SmartEmojiPicker } from './EmojiPicker';
|
||||
|
||||
|
@ -24,6 +25,7 @@ import { SmartEmojiPicker } from './EmojiPicker';
|
|||
const FilteredSmartTimelineItem = SmartTimelineItem as any;
|
||||
const FilteredSmartTypingBubble = SmartTypingBubble as any;
|
||||
const FilteredSmartLastSeenIndicator = SmartLastSeenIndicator as any;
|
||||
const FilteredSmartHeroRow = SmartHeroRow as any;
|
||||
const FilteredSmartTimelineLoadingRow = SmartTimelineLoadingRow as any;
|
||||
|
||||
type ExternalProps = {
|
||||
|
@ -66,6 +68,9 @@ function renderEmojiPicker({
|
|||
function renderLastSeenIndicator(id: string): JSX.Element {
|
||||
return <FilteredSmartLastSeenIndicator id={id} />;
|
||||
}
|
||||
function renderHeroRow(id: string, onHeightChange: () => unknown): JSX.Element {
|
||||
return <FilteredSmartHeroRow id={id} onHeightChange={onHeightChange} />;
|
||||
}
|
||||
function renderLoadingRow(id: string): JSX.Element {
|
||||
return <FilteredSmartTimelineLoadingRow id={id} />;
|
||||
}
|
||||
|
@ -88,6 +93,7 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
|
|||
i18n: getIntl(state),
|
||||
renderItem,
|
||||
renderLastSeenIndicator,
|
||||
renderHeroRow,
|
||||
renderLoadingRow,
|
||||
renderTypingBubble,
|
||||
...actions,
|
||||
|
|
|
@ -31,6 +31,8 @@ describe('state/selectors/conversations', () => {
|
|||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
},
|
||||
id2: {
|
||||
id: 'id2',
|
||||
|
@ -51,6 +53,8 @@ describe('state/selectors/conversations', () => {
|
|||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
},
|
||||
id3: {
|
||||
id: 'id3',
|
||||
|
@ -71,6 +75,8 @@ describe('state/selectors/conversations', () => {
|
|||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
},
|
||||
id4: {
|
||||
id: 'id4',
|
||||
|
@ -91,6 +97,8 @@ describe('state/selectors/conversations', () => {
|
|||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
},
|
||||
id5: {
|
||||
id: 'id5',
|
||||
|
@ -111,6 +119,8 @@ describe('state/selectors/conversations', () => {
|
|||
color: 'blue',
|
||||
phoneNumber: '+18005551111',
|
||||
},
|
||||
|
||||
acceptedMessageRequest: true,
|
||||
},
|
||||
};
|
||||
const comparator = _getConversationComparator(i18n, regionCode);
|
||||
|
|
18
ts/textsecure.d.ts
vendored
18
ts/textsecure.d.ts
vendored
|
@ -531,6 +531,7 @@ export declare class SyncMessageClass {
|
|||
padding?: ProtoBinaryType;
|
||||
stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>;
|
||||
viewOnceOpen?: SyncMessageClass.ViewOnceOpen;
|
||||
messageRequestResponse?: SyncMessageClass.MessageRequestResponse;
|
||||
}
|
||||
|
||||
// Note: we need to use namespaces to express nested classes in Typescript
|
||||
|
@ -582,6 +583,13 @@ export declare namespace SyncMessageClass {
|
|||
senderUuid?: string;
|
||||
timestamp?: ProtoBinaryType;
|
||||
}
|
||||
|
||||
class MessageRequestResponse {
|
||||
threadE164?: string;
|
||||
threadUuid?: string;
|
||||
groupId?: ProtoBinaryType;
|
||||
type?: number;
|
||||
}
|
||||
}
|
||||
|
||||
// Note: we need to use namespaces to express nested classes in Typescript
|
||||
|
@ -612,6 +620,16 @@ export declare namespace SyncMessageClass.StickerPackOperation {
|
|||
}
|
||||
}
|
||||
|
||||
export declare namespace SyncMessageClass.MessageRequestResponse {
|
||||
class Type {
|
||||
static UNKNOWN: number;
|
||||
static ACCEPT: number;
|
||||
static DELETE: number;
|
||||
static BLOCK: number;
|
||||
static BLOCK_AND_DELETE: number;
|
||||
}
|
||||
}
|
||||
|
||||
export declare class TypingMessageClass {
|
||||
static decode: (
|
||||
data: ArrayBuffer | ByteBufferClass,
|
||||
|
|
|
@ -42,6 +42,8 @@ declare global {
|
|||
deliveryReceipt?: any;
|
||||
error?: any;
|
||||
groupDetails?: any;
|
||||
groupId?: string;
|
||||
messageRequestResponseType?: number;
|
||||
proto?: any;
|
||||
read?: any;
|
||||
reason?: any;
|
||||
|
@ -51,6 +53,8 @@ declare global {
|
|||
source?: any;
|
||||
sourceUuid?: any;
|
||||
stickerPacks?: any;
|
||||
threadE164?: string;
|
||||
threadUuid?: string;
|
||||
timestamp?: any;
|
||||
typing?: any;
|
||||
verified?: any;
|
||||
|
@ -1119,6 +1123,22 @@ class MessageReceiverInner extends EventTarget {
|
|||
) {
|
||||
p = this.handleEndSession(destination);
|
||||
}
|
||||
|
||||
if (
|
||||
msg.flags &&
|
||||
msg.flags &
|
||||
window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE
|
||||
) {
|
||||
const ev = new Event('profileKeyUpdate');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.data = {
|
||||
source: envelope.source,
|
||||
sourceUuid: envelope.sourceUuid,
|
||||
profileKey: msg.profileKey.toString('base64'),
|
||||
};
|
||||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
|
||||
return p.then(async () =>
|
||||
this.processDecrypted(envelope, msg).then(message => {
|
||||
const groupId = message.group && message.group.id;
|
||||
|
@ -1373,6 +1393,11 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
} else if (syncMessage.viewOnceOpen) {
|
||||
return this.handleViewOnceOpen(envelope, syncMessage.viewOnceOpen);
|
||||
} else if (syncMessage.messageRequestResponse) {
|
||||
return this.handleMessageRequestResponse(
|
||||
envelope,
|
||||
syncMessage.messageRequestResponse
|
||||
);
|
||||
}
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
|
@ -1408,6 +1433,27 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
async handleMessageRequestResponse(
|
||||
envelope: EnvelopeClass,
|
||||
sync: SyncMessageClass.MessageRequestResponse
|
||||
) {
|
||||
window.log.info('got message request response sync message');
|
||||
|
||||
const ev = new Event('messageRequestResponse');
|
||||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.threadE164 = sync.threadE164;
|
||||
ev.threadUuid = sync.threadUuid;
|
||||
ev.groupId = sync.groupId ? sync.groupId.toString('binary') : null;
|
||||
ev.messageRequestResponseType = sync.type;
|
||||
|
||||
window.normalizeUuids(
|
||||
ev,
|
||||
['threadUuid'],
|
||||
'MessageReceiver::handleMessageRequestResponse'
|
||||
);
|
||||
|
||||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
async handleStickerPackOperation(
|
||||
envelope: EnvelopeClass,
|
||||
operations: Array<SyncMessageClass.StickerPackOperation>
|
||||
|
|
|
@ -74,7 +74,7 @@ type MessageOptionsType = {
|
|||
};
|
||||
needsSync?: boolean;
|
||||
preview?: Array<PreviewType> | null;
|
||||
profileKey?: string;
|
||||
profileKey?: ArrayBuffer;
|
||||
quote?: any;
|
||||
recipients: Array<string>;
|
||||
sticker?: any;
|
||||
|
@ -93,7 +93,7 @@ class Message {
|
|||
};
|
||||
needsSync?: boolean;
|
||||
preview: any;
|
||||
profileKey?: string;
|
||||
profileKey?: ArrayBuffer;
|
||||
quote?: any;
|
||||
recipients: Array<string>;
|
||||
sticker?: any;
|
||||
|
@ -274,6 +274,8 @@ export type AttachmentType = {
|
|||
caption: string;
|
||||
|
||||
attachmentPointer?: AttachmentPointerClass;
|
||||
|
||||
blurHash?: string;
|
||||
};
|
||||
|
||||
export default class MessageSender {
|
||||
|
@ -348,6 +350,9 @@ export default class MessageSender {
|
|||
if (attachment.caption) {
|
||||
proto.caption = attachment.caption;
|
||||
}
|
||||
if (attachment.blurHash) {
|
||||
proto.blurHash = attachment.blurHash;
|
||||
}
|
||||
|
||||
return proto;
|
||||
}
|
||||
|
@ -862,6 +867,31 @@ export default class MessageSender {
|
|||
);
|
||||
}
|
||||
|
||||
async sendProfileKeyUpdate(
|
||||
profileKey: ArrayBuffer,
|
||||
recipients: Array<string>,
|
||||
sendOptions: SendOptionsType,
|
||||
groupId?: string
|
||||
) {
|
||||
return this.sendMessage(
|
||||
{
|
||||
recipients,
|
||||
timestamp: Date.now(),
|
||||
profileKey,
|
||||
flags: window.textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE,
|
||||
...(groupId
|
||||
? {
|
||||
group: {
|
||||
id: groupId,
|
||||
type: window.textsecure.protobuf.GroupContext.Type.DELIVER,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
||||
async sendDeliveryReceipt(
|
||||
recipientE164: string,
|
||||
recipientUuid: string,
|
||||
|
@ -985,6 +1015,48 @@ export default class MessageSender {
|
|||
);
|
||||
}
|
||||
|
||||
async syncMessageRequestResponse(
|
||||
responseArgs: {
|
||||
threadE164?: string;
|
||||
threadUuid?: string;
|
||||
groupId?: string;
|
||||
type: number;
|
||||
},
|
||||
sendOptions?: SendOptionsType
|
||||
) {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
const myUuid = window.textsecure.storage.user.getUuid();
|
||||
const myDevice = window.textsecure.storage.user.getDeviceId();
|
||||
if (myDevice === 1 || myDevice === '1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const syncMessage = this.createSyncMessage();
|
||||
|
||||
const response = new window.textsecure.protobuf.SyncMessage.MessageRequestResponse();
|
||||
response.threadE164 = responseArgs.threadE164;
|
||||
response.threadUuid = responseArgs.threadUuid;
|
||||
response.groupId = responseArgs.groupId
|
||||
? window.Signal.Crypto.fromEncodedBinaryToArrayBuffer(
|
||||
responseArgs.groupId
|
||||
)
|
||||
: null;
|
||||
response.type = responseArgs.type;
|
||||
syncMessage.messageRequestResponse = response;
|
||||
|
||||
const contentMessage = new window.textsecure.protobuf.Content();
|
||||
contentMessage.syncMessage = syncMessage;
|
||||
|
||||
const silent = true;
|
||||
return this.sendIndividualProto(
|
||||
myUuid || myNumber,
|
||||
contentMessage,
|
||||
Date.now(),
|
||||
silent,
|
||||
sendOptions
|
||||
);
|
||||
}
|
||||
|
||||
async sendStickerPackSync(
|
||||
operations: Array<{
|
||||
packId: string;
|
||||
|
@ -1152,7 +1224,7 @@ export default class MessageSender {
|
|||
reaction: any,
|
||||
timestamp: number,
|
||||
expireTimer: number | undefined,
|
||||
profileKey?: string,
|
||||
profileKey?: ArrayBuffer,
|
||||
flags?: number
|
||||
) {
|
||||
const attributes = {
|
||||
|
@ -1195,7 +1267,7 @@ export default class MessageSender {
|
|||
reaction: any,
|
||||
timestamp: number,
|
||||
expireTimer: number | undefined,
|
||||
profileKey?: string,
|
||||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
) {
|
||||
return this.sendMessage(
|
||||
|
@ -1308,7 +1380,7 @@ export default class MessageSender {
|
|||
reaction: any,
|
||||
timestamp: number,
|
||||
expireTimer: number | undefined,
|
||||
profileKey?: string,
|
||||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
) {
|
||||
const myE164 = window.textsecure.storage.user.getNumber();
|
||||
|
@ -1480,7 +1552,7 @@ export default class MessageSender {
|
|||
groupIdentifiers: Array<string>,
|
||||
expireTimer: number | undefined,
|
||||
timestamp: number,
|
||||
profileKey?: string,
|
||||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
) {
|
||||
const myNumber = window.textsecure.storage.user.getNumber();
|
||||
|
@ -1517,7 +1589,7 @@ export default class MessageSender {
|
|||
identifier: string,
|
||||
expireTimer: number | undefined,
|
||||
timestamp: number,
|
||||
profileKey?: string,
|
||||
profileKey?: ArrayBuffer,
|
||||
options?: SendOptionsType
|
||||
) {
|
||||
return this.sendMessage(
|
||||
|
|
|
@ -489,6 +489,7 @@ const URL_CALLS = {
|
|||
signed: 'v2/keys/signed',
|
||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||
whoami: 'v1/accounts/whoami',
|
||||
config: 'v1/config',
|
||||
};
|
||||
|
||||
type InitializeOptionsType = {
|
||||
|
@ -604,6 +605,7 @@ export type WebAPIType = {
|
|||
setSignedPreKey: (signedPreKey: SignedPreKeyType) => Promise<void>;
|
||||
updateDeviceName: (deviceName: string) => Promise<void>;
|
||||
whoami: () => Promise<any>;
|
||||
getConfig: () => Promise<Array<{ name: string; enabled: boolean }>>;
|
||||
};
|
||||
|
||||
export type SignedPreKeyType = {
|
||||
|
@ -724,9 +726,10 @@ export function initialize({
|
|||
setSignedPreKey,
|
||||
updateDeviceName,
|
||||
whoami,
|
||||
getConfig,
|
||||
};
|
||||
|
||||
async function _ajax(param: AjaxOptionsType) {
|
||||
async function _ajax(param: AjaxOptionsType): Promise<any> {
|
||||
if (!param.urlParameters) {
|
||||
param.urlParameters = '';
|
||||
}
|
||||
|
@ -792,6 +795,21 @@ export function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
type ResType = {
|
||||
config: Array<{ name: string; enabled: boolean }>;
|
||||
};
|
||||
const res: ResType = await _ajax({
|
||||
call: 'config',
|
||||
httpType: 'GET',
|
||||
responseType: 'json',
|
||||
});
|
||||
|
||||
return res.config.filter(({ name }: { name: string }) =>
|
||||
name.startsWith('desktop.')
|
||||
);
|
||||
}
|
||||
|
||||
async function getSenderCertificate() {
|
||||
return _ajax({
|
||||
call: 'deliveryCert',
|
||||
|
|
|
@ -18,6 +18,7 @@ const MIN_HEIGHT = 50;
|
|||
// Used for display
|
||||
|
||||
export interface AttachmentType {
|
||||
blurHash?: string;
|
||||
caption?: string;
|
||||
contentType: MIME.MIMEType;
|
||||
fileName: string;
|
||||
|
@ -133,7 +134,7 @@ export function hasImage(attachments?: Array<AttachmentType>) {
|
|||
return (
|
||||
attachments &&
|
||||
attachments[0] &&
|
||||
(attachments[0].url || attachments[0].pending)
|
||||
(attachments[0].url || attachments[0].pending || attachments[0].blurHash)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
50
ts/util/imageToBlurHash.ts
Normal file
50
ts/util/imageToBlurHash.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import loadImage from 'blueimp-load-image';
|
||||
import { encode } from 'blurhash';
|
||||
|
||||
type Input = Parameters<typeof loadImage>[0];
|
||||
|
||||
const loadImageData = async (input: Input): Promise<ImageData> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
loadImage(
|
||||
input,
|
||||
canvasOrError => {
|
||||
if (canvasOrError instanceof Event && canvasOrError.type === 'error') {
|
||||
const processError = new Error(
|
||||
'imageToBlurHash: Failed to process image'
|
||||
);
|
||||
processError.cause = canvasOrError;
|
||||
reject(processError);
|
||||
return;
|
||||
}
|
||||
if (canvasOrError instanceof HTMLCanvasElement) {
|
||||
const context = canvasOrError.getContext('2d');
|
||||
resolve(
|
||||
context?.getImageData(
|
||||
0,
|
||||
0,
|
||||
canvasOrError.width,
|
||||
canvasOrError.height
|
||||
)
|
||||
);
|
||||
}
|
||||
const error = new Error(
|
||||
'imageToBlurHash: Failed to place image on canvas'
|
||||
);
|
||||
reject(error);
|
||||
return;
|
||||
},
|
||||
// Calculating the blurhash on large images is a long-running and
|
||||
// synchronous operation, so here we ensure the images are a reasonable
|
||||
// size before calculating the blurhash. iOS uses a max size of 200x200
|
||||
// and Android uses a max size of 1/16 the original size. 200x200 is
|
||||
// easier for us.
|
||||
{ canvas: true, orientation: true, maxWidth: 200, maxHeight: 200 }
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const imageToBlurHash = async (input: Input) => {
|
||||
const { data, width, height } = await loadImageData(input);
|
||||
// 4 horizontal components and 3 vertical components
|
||||
return encode(data, width, height, 4, 3);
|
||||
};
|
|
@ -219,6 +219,14 @@
|
|||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-03-25T15:45:04.024Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-wrap(",
|
||||
"path": "js/models/conversations.js",
|
||||
"line": " await wrap(",
|
||||
"lineNumber": 641,
|
||||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-05-27T21:15:43.044Z"
|
||||
},
|
||||
{
|
||||
"rule": "jQuery-append(",
|
||||
"path": "js/modules/debuglogs.js",
|
||||
|
@ -570,7 +578,7 @@
|
|||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||
"lineNumber": 198,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Known DOM elements"
|
||||
},
|
||||
{
|
||||
|
@ -579,7 +587,7 @@
|
|||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||
"lineNumber": 201,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-10-21T22:30:15.622Z",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Hardcoded selector"
|
||||
},
|
||||
{
|
||||
|
@ -11426,18 +11434,18 @@
|
|||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.js",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 22,
|
||||
"lineNumber": 23,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-08-01T14:10:37.481Z",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
},
|
||||
{
|
||||
"rule": "DOM-innerHTML",
|
||||
"path": "ts/components/CompositionArea.tsx",
|
||||
"line": " el.innerHTML = '';",
|
||||
"lineNumber": 73,
|
||||
"lineNumber": 80,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-12-16T14:36:25.614ZZ",
|
||||
"updated": "2020-06-03T19:23:21.195Z",
|
||||
"reasonDetail": "Our code, no user input, only clearing out the dom"
|
||||
},
|
||||
{
|
||||
|
@ -11507,9 +11515,9 @@
|
|||
"rule": "React-createRef",
|
||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||
"line": " this.menuTriggerRef = React.createRef();",
|
||||
"lineNumber": 67,
|
||||
"lineNumber": 68,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2019-07-31T00:19:18.696Z",
|
||||
"updated": "2020-05-20T20:10:43.540Z",
|
||||
"reasonDetail": "Used to reference popup menu"
|
||||
},
|
||||
{
|
||||
|
@ -11553,7 +11561,7 @@
|
|||
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
|
||||
"lineNumber": 184,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-04-30T15:59:13.160Z"
|
||||
"updated": "2020-05-21T16:56:07.875Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
|
@ -11561,7 +11569,7 @@
|
|||
"line": " > = React.createRef();",
|
||||
"lineNumber": 188,
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2020-04-30T15:59:13.160Z"
|
||||
"updated": "2020-05-21T16:56:07.875Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-createRef",
|
||||
|
@ -11784,4 +11792,4 @@
|
|||
"reasonCategory": "falseMatch",
|
||||
"updated": "2020-04-05T23:45:16.746Z"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
|
6
ts/window.d.ts
vendored
6
ts/window.d.ts
vendored
|
@ -31,7 +31,7 @@ declare global {
|
|||
storage: {
|
||||
put: (key: string, value: any) => void;
|
||||
remove: (key: string) => void;
|
||||
get: (key: string) => any;
|
||||
get: <T = any>(key: string) => T | undefined;
|
||||
};
|
||||
textsecure: TextSecureType;
|
||||
|
||||
|
@ -48,6 +48,10 @@ declare global {
|
|||
WebAPI: WebAPIConnectType;
|
||||
Whisper: WhisperType;
|
||||
}
|
||||
|
||||
interface Error {
|
||||
cause?: Event;
|
||||
}
|
||||
}
|
||||
|
||||
export type ConversationType = {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue