Do not download media if in call

This commit is contained in:
Josh Perez 2021-01-29 17:58:28 -05:00 committed by GitHub
parent d22add261b
commit a096220990
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 47 deletions

View file

@ -4964,6 +4964,86 @@ button.module-conversation-details__action-button {
overflow: hidden;
}
.module-image--not-downloaded {
align-items: center;
display: flex;
justify-content: center;
i {
align-items: center;
display: flex;
justify-content: center;
background-color: $color-gray-75;
border-radius: 48px;
height: 48px;
width: 48px;
&:after {
content: '';
height: 17px;
width: 17px;
@include color-svg('../images/icons/v2/arrow-down-24.svg', $color-white);
}
}
&:hover {
i {
background-color: $color-black;
}
}
&:focus {
i {
background-color: $color-gray-75;
border: 4px solid $ultramarine-ui-light;
box-sizing: border-box;
outline: none;
}
}
}
.module-image__download-pending {
position: relative;
&--spinner-container {
align-items: center;
display: flex;
height: 100%;
justify-content: center;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
&--spinner {
background-color: $color-gray-75;
border-radius: 48px;
height: 48px;
width: 48px;
.module-image-spinner {
&__container {
margin: 12px auto;
}
&__arc {
background-color: $color-gray-75;
}
&__circle {
background-color: $color-white;
}
@include dark-theme {
&__arc {
background-color: $color-gray-75;
}
}
}
}
}
.module-image--with-background {
@include light-theme {
background-color: $color-white;

View file

@ -17,18 +17,25 @@ export const SpinnerDirections = [
export type SpinnerDirection = typeof SpinnerDirections[number];
export type Props = {
moduleClassName?: string;
direction?: SpinnerDirection;
size?: string;
svgSize: SpinnerSvgSize;
direction?: SpinnerDirection;
};
export const Spinner = ({ size, svgSize, direction }: Props): JSX.Element => (
export const Spinner = ({
moduleClassName,
size,
svgSize,
direction,
}: Props): JSX.Element => (
<div
className={classNames(
'module-spinner__container',
`module-spinner__container--${svgSize}`,
direction ? `module-spinner__container--${direction}` : null,
direction ? `module-spinner__container--${svgSize}-${direction}` : null
direction ? `module-spinner__container--${svgSize}-${direction}` : null,
moduleClassName ? `${moduleClassName}__container` : null
)}
style={{
height: size,
@ -40,7 +47,8 @@ export const Spinner = ({ size, svgSize, direction }: Props): JSX.Element => (
'module-spinner__circle',
`module-spinner__circle--${svgSize}`,
direction ? `module-spinner__circle--${direction}` : null,
direction ? `module-spinner__circle--${svgSize}-${direction}` : null
direction ? `module-spinner__circle--${svgSize}-${direction}` : null,
moduleClassName ? `${moduleClassName}__circle` : null
)}
/>
<div
@ -48,7 +56,8 @@ export const Spinner = ({ size, svgSize, direction }: Props): JSX.Element => (
'module-spinner__arc',
`module-spinner__arc--${svgSize}`,
direction ? `module-spinner__arc--${direction}` : null,
direction ? `module-spinner__arc--${svgSize}-${direction}` : null
direction ? `module-spinner__arc--${svgSize}-${direction}` : null,
moduleClassName ? `${moduleClassName}__arc` : null
)}
/>
</div>

View file

@ -121,6 +121,20 @@ story.add('Pending', () => {
return <Image {...props} />;
});
story.add('Pending w/blurhash', () => {
const props = createProps();
props.attachment.pending = true;
return (
<Image
{...props}
blurHash="LDA,FDBnm+I=p{tkIUI;~UkpELV]"
width={300}
height={400}
/>
);
});
story.add('Curved Corners', () => {
const props = createProps({
curveBottomLeft: true,
@ -176,6 +190,7 @@ story.add('Blurhash', () => {
return <Image {...props} />;
});
story.add('Missing Image', () => {
const defaultProps = createProps();
const props = {

View file

@ -7,7 +7,7 @@ import { Blurhash } from 'react-blurhash';
import { Spinner } from '../Spinner';
import { LocalizerType } from '../../types/Util';
import { AttachmentType } from '../../types/Attachment';
import { AttachmentType, hasNotDownloaded } from '../../types/Attachment';
export type Props = {
alt: string;
@ -44,10 +44,10 @@ export type Props = {
export class Image extends React.Component<Props> {
private canClick() {
const { onClick, attachment, url } = this.props;
const { onClick, attachment, blurHash, url } = this.props;
const { pending } = attachment || { pending: true };
return Boolean(onClick && !pending && url);
return Boolean(onClick && !pending && (url || blurHash));
}
public handleClick = (event: React.MouseEvent): void => {
@ -87,6 +87,46 @@ export class Image extends React.Component<Props> {
}
};
public renderPending = (): JSX.Element => {
const { blurHash, height, i18n, width } = this.props;
if (blurHash) {
return (
<div className="module-image__download-pending">
<Blurhash
hash={blurHash}
width={width}
height={height}
style={{ display: 'block' }}
/>
<div className="module-image__download-pending--spinner-container">
<div
className="module-image__download-pending--spinner"
title={i18n('loading')}
>
<Spinner moduleClassName="module-image-spinner" svgSize="small" />
</div>
</div>
</div>
);
}
return (
<div
className="module-image__loading-placeholder"
style={{
height: `${height}px`,
width: `${width}px`,
lineHeight: `${height}px`,
textAlign: 'center',
}}
title={i18n('loading')}
>
<Spinner svgSize="normal" />
</div>
);
};
public render(): JSX.Element {
const {
alt,
@ -116,19 +156,20 @@ export class Image extends React.Component<Props> {
const { caption, pending } = attachment || { caption: null, pending: true };
const canClick = this.canClick();
const imgNotDownloaded = hasNotDownloaded(attachment);
const overlayClassName = classNames(
'module-image__border-overlay',
noBorder ? null : 'module-image__border-overlay--with-border',
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,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
softCorners ? 'module-image--soft-corners' : null,
darkOverlay ? 'module-image__border-overlay--dark' : null
);
const overlayClassName = classNames('module-image__border-overlay', {
'module-image__border-overlay--with-border': !noBorder,
'module-image__border-overlay--with-click-handler': canClick,
'module-image--curved-top-left': curveTopLeft,
'module-image--curved-top-right': curveTopRight,
'module-image--curved-bottom-left': curveBottomLeft,
'module-image--curved-bottom-right': curveBottomRight,
'module-image--small-curved-top-left': smallCurveTopLeft,
'module-image--soft-corners': softCorners,
'module-image__border-overlay--dark': darkOverlay,
'module-image--not-downloaded': imgNotDownloaded,
});
const overlay = canClick ? (
// Not sure what this button does.
@ -139,7 +180,9 @@ export class Image extends React.Component<Props> {
onClick={this.handleClick}
onKeyDown={this.handleKeyDown}
tabIndex={tabIndex}
/>
>
{imgNotDownloaded ? <i /> : null}
</button>
) : null;
/* eslint-disable no-nested-ternary */
@ -157,18 +200,7 @@ export class Image extends React.Component<Props> {
)}
>
{pending ? (
<div
className="module-image__loading-placeholder"
style={{
height: `${height}px`,
width: `${width}px`,
lineHeight: `${height}px`,
textAlign: 'center',
}}
title={i18n('loading')}
>
<Spinner svgSize="normal" />
</div>
this.renderPending()
) : url ? (
<img
onError={onError}

View file

@ -75,6 +75,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
isTapToView: overrideProps.isTapToView,
isTapToViewError: overrideProps.isTapToViewError,
isTapToViewExpired: overrideProps.isTapToViewExpired,
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
openConversation: action('openConversation'),
openLink: action('openLink'),
previews: overrideProps.previews || [],

View file

@ -35,6 +35,8 @@ import {
getGridDimensions,
getImageDimensions,
hasImage,
hasNotDownloaded,
hasVideoBlurHash,
hasVideoScreenshot,
isAudio,
isImage,
@ -162,6 +164,10 @@ export type PropsActions = {
}) => void;
showContactModal: (contactId: string) => void;
kickOffAttachmentDownload: (options: {
attachment: AttachmentType;
messageId: string;
}) => void;
showVisualAttachment: (options: {
attachment: AttachmentType;
messageId: string;
@ -657,6 +663,7 @@ export class Message extends React.PureComponent<Props, State> {
direction,
i18n,
id,
kickOffAttachmentDownload,
quote,
showVisualAttachment,
isSticker,
@ -680,7 +687,8 @@ export class Message extends React.PureComponent<Props, State> {
displayImage &&
!imageBroken &&
((isImage(attachments) && hasImage(attachments)) ||
(isVideo(attachments) && hasVideoScreenshot(attachments)))
(isVideo(attachments) &&
(hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments))))
) {
const prefix = isSticker ? 'sticker' : 'attachment';
const bottomOverlay = !isSticker && !collapseMetadata;
@ -713,7 +721,11 @@ export class Message extends React.PureComponent<Props, State> {
onError={this.handleImageError}
tabIndex={tabIndex}
onClick={attachment => {
showVisualAttachment({ attachment, messageId: id });
if (hasNotDownloaded(attachment)) {
kickOffAttachmentDownload({ attachment, messageId: id });
} else {
showVisualAttachment({ attachment, messageId: id });
}
}}
/>
</div>
@ -1517,7 +1529,8 @@ export class Message extends React.PureComponent<Props, State> {
return (
displayImage &&
((isImage(attachments) && hasImage(attachments)) ||
(isVideo(attachments) && hasVideoScreenshot(attachments)))
(isVideo(attachments) &&
(hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments))))
);
}
@ -1922,6 +1935,7 @@ export class Message extends React.PureComponent<Props, State> {
id,
isTapToView,
isTapToViewExpired,
kickOffAttachmentDownload,
openConversation,
showContactDetail,
showVisualAttachment,
@ -1953,6 +1967,24 @@ export class Message extends React.PureComponent<Props, State> {
return;
}
if (
!imageBroken &&
attachments &&
attachments.length > 0 &&
!isAttachmentPending &&
(isImage(attachments) || isVideo(attachments)) &&
hasNotDownloaded(attachments[0])
) {
event.preventDefault();
event.stopPropagation();
const attachment = attachments[0];
kickOffAttachmentDownload({ attachment, messageId: id });
return;
}
if (
!imageBroken &&
attachments &&
@ -1960,7 +1992,8 @@ export class Message extends React.PureComponent<Props, State> {
!isAttachmentPending &&
canDisplayImage(attachments) &&
((isImage(attachments) && hasImage(attachments)) ||
(isVideo(attachments) && hasVideoScreenshot(attachments)))
(isVideo(attachments) &&
(hasVideoBlurHash(attachments) || hasVideoScreenshot(attachments))))
) {
event.preventDefault();
event.stopPropagation();

View file

@ -33,6 +33,7 @@ const defaultMessage: MessageProps = {
i18n,
id: 'my-message',
interactionMode: 'keyboard',
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
openConversation: () => null,
openLink: () => null,
previews: [],

View file

@ -36,6 +36,7 @@ const defaultMessageProps: MessagesProps = {
i18n,
id: 'messageId',
interactionMode: 'keyboard',
kickOffAttachmentDownload: () => null,
openConversation: () => null,
openLink: () => null,
previews: [],

View file

@ -231,6 +231,7 @@ const actions = () => ({
openConversation: action('openConversation'),
showContactDetail: action('showContactDetail'),
showContactModal: action('showContactModal'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
showVisualAttachment: action('showVisualAttachment'),
downloadAttachment: action('downloadAttachment'),
displayTapToViewMessage: action('displayTapToViewMessage'),

View file

@ -45,6 +45,7 @@ const getDefaultProps = () => ({
retrySend: action('retrySend'),
deleteMessage: action('deleteMessage'),
deleteMessageForEveryone: action('deleteMessageForEveryone'),
kickOffAttachmentDownload: action('kickOffAttachmentDownload'),
showMessageDetail: action('showMessageDetail'),
openConversation: action('openConversation'),
showContactDetail: action('showContactDetail'),

View file

@ -13,7 +13,7 @@ import {
ConversationType,
} from '../state/ducks/conversations';
import { getActiveCall } from '../state/ducks/calling';
import { getCallSelector } from '../state/selectors/calling';
import { getCallSelector, isInCall } from '../state/selectors/calling';
import { PropsData } from '../components/conversation/Message';
import { CallbackResultType } from '../textsecure/SendMessage';
import { ExpirationTimerOptions } from '../util/ExpirationTimerOptions';
@ -38,6 +38,7 @@ import {
getCallingNotificationText,
} from '../util/callingNotification';
import { PropsType as ProfileChangeNotificationPropsType } from '../components/conversation/ProfileChangeNotification';
import { isImage, isVideo } from '../types/Attachment';
/* eslint-disable camelcase */
/* eslint-disable more/no-then */
@ -2136,14 +2137,23 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
}
canDownload(): boolean {
const conversation = this.getConversation();
const isAccepted = Boolean(conversation && conversation.getAccepted());
if (this.isOutgoing()) {
return true;
}
return isAccepted;
const conversation = this.getConversation();
const isAccepted = Boolean(conversation && conversation.getAccepted());
if (!isAccepted) {
return false;
}
// Ensure that all attachments are downloadable
const attachments = this.get('attachments');
if (attachments && attachments.length) {
return attachments.every(attachment => Boolean(attachment.path));
}
return true;
}
canReply(): boolean {
@ -3527,8 +3537,16 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
// Only queue attachments for downloads if this is an outgoing message
// or we've accepted the conversation
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (this.getConversation()!.getAccepted() || message.isOutgoing()) {
const reduxState = window.reduxStore.getState();
const attachments = this.get('attachments') || [];
const shouldHoldOffDownload =
(isImage(attachments) || isVideo(attachments)) &&
isInCall(reduxState);
if (
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
(this.getConversation()!.getAccepted() || message.isOutgoing()) &&
!shouldHoldOffDownload
) {
await message.queueAttachmentDownloads();
}

View file

@ -8,12 +8,18 @@ import {
CallingStateType,
CallsByConversationType,
DirectCallStateType,
getActiveCall,
} from '../ducks/calling';
import { CallMode, CallState } from '../../types/Calling';
import { getOwn } from '../../util/getOwn';
const getCalling = (state: StateType): CallingStateType => state.calling;
export const isInCall = createSelector(
getCalling,
(state: CallingStateType): boolean => Boolean(getActiveCall(state))
);
export const getCallsByConversation = createSelector(
getCalling,
(state: CallingStateType): CallsByConversationType =>

View file

@ -9,6 +9,7 @@ import {
getCallsByConversation,
getCallSelector,
getIncomingCall,
isInCall,
} from '../../../state/selectors/calling';
import { getEmptyState, CallingStateType } from '../../../state/ducks/calling';
@ -132,4 +133,14 @@ describe('state/selectors/calling', () => {
);
});
});
describe('isInCall', () => {
it('returns should be false if we are not in a call', () => {
assert.isFalse(isInCall(getEmptyRootState()));
});
it('should be true if we are in a call', () => {
assert.isTrue(isInCall(getCallingState(stateWithActiveDirectCall)));
});
});
});

View file

@ -169,6 +169,16 @@ export function isVideoAttachment(
);
}
export function hasNotDownloaded(attachment?: AttachmentType): boolean {
return Boolean(attachment && !attachment.url && attachment.blurHash);
}
export function hasVideoBlurHash(attachments?: Array<AttachmentType>): boolean {
const firstAttachment = attachments ? attachments[0] : null;
return Boolean(firstAttachment && firstAttachment.blurHash);
}
export function hasVideoScreenshot(
attachments?: Array<AttachmentType>
): string | null | undefined {

View file

@ -14841,7 +14841,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public audioRef: React.RefObject<HTMLAudioElement> = React.createRef();",
"lineNumber": 214,
"lineNumber": 220,
"reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z"
},
@ -14849,7 +14849,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " public focusRef: React.RefObject<HTMLDivElement> = React.createRef();",
"lineNumber": 216,
"lineNumber": 222,
"reasonCategory": "usageTrusted",
"updated": "2020-09-08T20:19:01.913Z"
},
@ -14857,7 +14857,7 @@
"rule": "React-createRef",
"path": "ts/components/conversation/Message.tsx",
"line": " > = React.createRef();",
"lineNumber": 220,
"lineNumber": 226,
"reasonCategory": "usageTrusted",
"updated": "2020-08-28T19:36:40.817Z"
},

View file

@ -742,6 +742,13 @@ Whisper.ConversationView = Whisper.View.extend({
const showContactDetail = (options: any) => {
this.showContactDetail(options);
};
const kickOffAttachmentDownload = async (options: any) => {
if (!this.model.messageCollection) {
throw new Error('Message collection does not exist');
}
const message = this.model.messageCollection.get(options.messageId);
await message.queueAttachmentDownloads();
};
const showVisualAttachment = (options: any) => {
this.showLightbox(options);
};
@ -924,6 +931,7 @@ Whisper.ConversationView = Whisper.View.extend({
displayTapToViewMessage,
downloadAttachment,
downloadNewVersion,
kickOffAttachmentDownload,
loadNewerMessages,
loadNewestMessages: this.loadNewestMessages.bind(this),
loadAndScroll: this.loadAndScroll.bind(this),