Improve caption editor usability, new 'add attachment' affordance

pull/272/head
Scott Nonnenberg 5 years ago
parent ac1a6d197a
commit 0de54e125c

@ -952,6 +952,11 @@
"descripton":
"Used as the placeholder text in the caption editor text field"
},
"save": {
"message": "Save",
"descripton":
"Used as a 'commit changes' button in the Caption Editor for outgoing image attachments"
},
"fileIconAlt": {
"message": "File icon",
"description":

@ -0,0 +1 @@
<svg id="Export" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><title>plus-36</title><polygon points="32 17.25 18.75 17.25 18.75 4 17.25 4 17.25 17.25 4 17.25 4 18.75 17.25 18.75 17.25 32 18.75 32 18.75 18.75 32 18.75 32 17.25"/></svg>

After

Width:  |  Height:  |  Size: 244 B

@ -156,6 +156,11 @@
'attachments-changed',
this.toggleMicrophone
);
this.listenTo(
this.fileInput,
'choose-attachment',
this.onChooseAttachment
);
const getHeaderProps = () => {
const expireTimer = this.model.get('expireTimer');
@ -275,8 +280,10 @@
},
onChooseAttachment(e) {
e.stopPropagation();
e.preventDefault();
if (e) {
e.stopPropagation();
e.preventDefault();
}
this.$('input.file-input').click();
},

@ -86,6 +86,7 @@
return {
attachments,
onAddAttachment: this.onAddAttachment.bind(this),
onClickAttachment: this.onClickAttachment.bind(this),
onCloseAttachment: this.onCloseAttachment.bind(this),
onClose: this.onClose.bind(this),
@ -97,18 +98,15 @@
url: attachment.videoUrl || attachment.url,
caption: attachment.caption,
attachment,
onChangeCaption,
onSave,
});
const update = () => {
this.captionEditorView.update(getProps());
};
const onChangeCaption = caption => {
const onSave = caption => {
// eslint-disable-next-line no-param-reassign
attachment.caption = caption;
this.captionEditorView.remove();
Signal.Backbone.Views.Lightbox.hide();
this.render();
update();
};
this.captionEditorView = new Whisper.ReactWrapperView({
@ -126,6 +124,10 @@
this.render();
},
onAddAttachment() {
this.trigger('choose-attachment');
},
onClose() {
this.attachments = [];
this.trigger('attachments-changed');

@ -412,15 +412,6 @@ $loading-height: 16px;
}
}
input[type='text'],
input[type='search'],
textarea {
&:active,
&:focus {
outline: 1px solid $blue;
}
}
.expiredAlert {
background: #f3f3a7;
padding: 10px;

@ -2298,7 +2298,7 @@
height: 20px;
z-index: 2;
@include color-svg('../images/x.svg', $color-black);
@include color-svg('../images/x-16.svg', $color-black);
}
.module-attachments__rail {
@ -2416,7 +2416,7 @@
width: 30px;
height: 30px;
z-index: 2;
@include color-svg('../images/x.svg', $color-white);
@include color-svg('../images/x-16.svg', $color-white);
}
.module-caption-editor__media-container {
@ -2457,8 +2457,8 @@
.module-caption-editor__bottom-bar {
flex-grow: 0;
flex-shrink: 0;
height: 3em;
padding: 0.5em;
height: 52px;
padding: 8px;
display: inline-flex;
flex-direction: row;
@ -2468,23 +2468,23 @@
margin-right: auto;
}
.module-caption-editor__add-caption-button {
display: inline-block;
margin-left: 6px;
height: 24px;
width: 24px;
margin-right: 6px;
@include color-svg('../images/add-caption-24.svg', $color-white);
.module-caption-editor__input-container {
position: relative;
}
.module-caption-editor__caption-input {
height: 2em;
height: 36px;
width: 40em;
border: 1px solid $color-white;
border-radius: 1em;
font-size: 14px;
color: $color-white;
border: 1px solid $color-white;
border-radius: 18px;
background-color: $color-black;
padding: 0.5em;
padding: 9px;
padding-left: 12px;
padding-right: 65px;
&::placeholder {
color: $color-white-07;
@ -2495,6 +2495,54 @@
}
}
.module-caption-editor__save-button {
position: absolute;
background-color: $color-signal-blue;
color: $color-white;
cursor: pointer;
height: 28px;
border-radius: 15px;
padding: 5px;
padding-left: 12px;
padding-right: 12px;
right: 4px;
top: 4px;
}
// Module: Staged Placeholder Attachment
.module-staged-placeholder-attachment {
margin: 1px;
border-radius: 4px;
border: 1px solid $color-gray-25;
height: 120px;
width: 120px;
display: inline-block;
vertical-align: middle;
cursor: pointer;
position: relative;
&:hover {
background: $color-gray-05;
}
}
.module-staged-placeholder-attachment__plus-icon {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
height: 36px;
width: 36px;
@include color-svg('../images/plus-36.svg', $color-gray-45);
}
// Third-party module: react-contextmenu
.react-contextmenu {

@ -1350,6 +1350,20 @@ body.dark-theme {
color: $color-gray-90;
}
// Module: Staged Placeholder Attachment
.module-staged-placeholder-attachment {
border: 1px solid $color-gray-60;
&:hover {
background: $color-gray-75;
}
}
.module-staged-placeholder-attachment__plus-icon {
@include color-svg('../images/plus-36.svg', $color-gray-60);
}
// Third-party module: react-contextmenu
.react-contextmenu {

@ -9,7 +9,8 @@ let caption = null;
attachment={{
contentType: 'image/jpeg',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
onSave={caption => console.log('onSave', caption)}
close={() => console.log('close')}
i18n={util.i18n}
/>
</div>;
@ -29,7 +30,8 @@ let caption =
}}
caption={caption}
contentType="image/jpeg"
onChangeCaption={caption => console.log('onChangeCaption', caption)}
onSave={caption => console.log('onSave', caption)}
close={() => console.log('close')}
i18n={util.i18n}
/>
</div>;
@ -46,7 +48,8 @@ let caption = null;
attachment={{
contentType: 'video/mp4',
}}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
onSave={caption => console.log('onSave', caption)}
close={() => console.log('close')}
i18n={util.i18n}
/>
</div>;
@ -65,7 +68,8 @@ let caption =
contentType: 'video/mp4',
}}
caption={caption}
onChangeCaption={caption => console.log('onChangeCaption', caption)}
onSave={caption => console.log('onSave', caption)}
close={() => console.log('close')}
i18n={util.i18n}
/>
</div>;

@ -12,31 +12,52 @@ interface Props {
i18n: Localizer;
url: string;
caption?: string;
onChangeCaption?: (caption: string) => void;
onSave?: (caption: string) => void;
close?: () => void;
}
export class CaptionEditor extends React.Component<Props> {
private handleKeyUpBound: () => void;
interface State {
caption: string;
}
export class CaptionEditor extends React.Component<Props, State> {
private handleKeyUpBound: (
event: React.KeyboardEvent<HTMLInputElement>
) => void;
private setFocusBound: () => void;
// TypeScript doesn't like our React.Ref typing here, so we omit it
private captureRefBound: () => void;
private onChangeBound: () => void;
private onSaveBound: () => void;
private inputRef: React.Ref<HTMLInputElement> | null;
constructor(props: Props) {
super(props);
const { caption } = props;
this.state = {
caption: caption || '',
};
this.handleKeyUpBound = this.handleKeyUp.bind(this);
this.setFocusBound = this.setFocus.bind(this);
this.captureRefBound = this.captureRef.bind(this);
this.onChangeBound = this.onChange.bind(this);
this.onSaveBound = this.onSave.bind(this);
this.inputRef = null;
}
public handleKeyUp(event: React.KeyboardEvent<HTMLInputElement>) {
const { close } = this.props;
const { close, onSave } = this.props;
if (close && (event.key === 'Escape' || event.key === 'Enter')) {
if (close && event.key === 'Escape') {
close();
}
if (onSave && event.key === 'Enter') {
const { caption } = this.state;
onSave(caption);
}
}
public setFocus() {
@ -55,6 +76,24 @@ export class CaptionEditor extends React.Component<Props> {
}, 200);
}
public onSave() {
const { onSave } = this.props;
const { caption } = this.state;
if (onSave) {
onSave(caption);
}
}
public onChange(event: React.FormEvent<HTMLInputElement>) {
// @ts-ignore
const { value } = event.target;
this.setState({
caption: value,
});
}
public renderObject() {
const { url, i18n, attachment } = this.props;
const { contentType } = attachment || { contentType: null };
@ -83,7 +122,8 @@ export class CaptionEditor extends React.Component<Props> {
}
public render() {
const { caption, i18n, close, onChangeCaption } = this.props;
const { i18n, close } = this.props;
const { caption } = this.state;
return (
<div
@ -100,21 +140,27 @@ export class CaptionEditor extends React.Component<Props> {
{this.renderObject()}
</div>
<div className="module-caption-editor__bottom-bar">
<div className="module-caption-editor__add-caption-button" />
<input
type="text"
ref={this.captureRefBound}
onKeyUp={close ? this.handleKeyUpBound : undefined}
value={caption || ''}
maxLength={200}
placeholder={i18n('addACaption')}
className="module-caption-editor__caption-input"
onChange={event => {
if (onChangeCaption) {
onChangeCaption(event.target.value);
}
}}
/>
<div className="module-caption-editor__input-container">
<input
type="text"
ref={this.captureRefBound}
value={caption}
maxLength={200}
placeholder={i18n('addACaption')}
className="module-caption-editor__caption-input"
onKeyUp={close ? this.handleKeyUpBound : undefined}
onChange={this.onChangeBound}
/>
{caption ? (
<div
role="button"
onClick={this.onSaveBound}
className="module-caption-editor__save-button"
>
{i18n('save')}
</div>
) : null}
</div>
</div>
</div>
);

@ -19,8 +19,9 @@ const attachments = [
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
onAddAttachment={() => console.log('onAddAttachment')}
i18n={util.i18n}
/>;
/>
</util.ConversationContext>;
```
@ -64,6 +65,7 @@ const attachments = [
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
onAddAttachment={() => console.log('onAddAttachment')}
i18n={util.i18n}
/>
</util.ConversationContext>;
@ -101,6 +103,7 @@ const attachments = [
onCloseAttachment={attachment => {
console.log('onCloseAttachment', attachment);
}}
onAddAttachment={() => console.log('onAddAttachment')}
i18n={util.i18n}
/>
</util.ConversationContext>;

@ -6,7 +6,9 @@ import {
} from '../../util/GoogleChrome';
import { AttachmentType } from './types';
import { Image } from './Image';
import { areAllAttachmentsVisual } from './ImageGrid';
import { StagedGenericAttachment } from './StagedGenericAttachment';
import { StagedPlaceholderAttachment } from './StagedPlaceholderAttachment';
import { Localizer } from '../../types/Util';
interface Props {
@ -15,6 +17,7 @@ interface Props {
// onError: () => void;
onClickAttachment: (attachment: AttachmentType) => void;
onCloseAttachment: (attachment: AttachmentType) => void;
onAddAttachment: () => void;
onClose: () => void;
}
@ -27,6 +30,7 @@ export class AttachmentList extends React.Component<Props> {
const {
attachments,
i18n,
onAddAttachment,
onClickAttachment,
onCloseAttachment,
onClose,
@ -36,6 +40,8 @@ export class AttachmentList extends React.Component<Props> {
return null;
}
const allVisualAttachments = areAllAttachmentsVisual(attachments);
return (
<div className="module-attachments">
{attachments.length > 1 ? (
@ -85,6 +91,9 @@ export class AttachmentList extends React.Component<Props> {
/>
);
})}
{allVisualAttachments ? (
<StagedPlaceholderAttachment onClick={onAddAttachment} />
) : null}
</div>
</div>
);

@ -389,7 +389,9 @@ function getImageDimensions(attachment: AttachmentType): DimensionsType {
};
}
function areAllAttachmentsVisual(attachments?: Array<AttachmentType>): boolean {
export function areAllAttachmentsVisual(
attachments?: Array<AttachmentType>
): boolean {
if (!attachments) {
return false;
}
@ -397,7 +399,7 @@ function areAllAttachmentsVisual(attachments?: Array<AttachmentType>): boolean {
const max = attachments.length;
for (let i = 0; i < max; i += 1) {
const attachment = attachments[i];
if (!isImageAttachment(attachment) || !isVideoAttachment(attachment)) {
if (!isImageAttachment(attachment) && !isVideoAttachment(attachment)) {
return false;
}
}

@ -0,0 +1,10 @@
```js
const attachment = {
contentType: 'text/plain',
fileName: 'manifesto.txt',
};
<util.ConversationContext theme={util.theme}>
<StagedPlaceholderAttachment onClick={attachment => console.log('onClick')} />
</util.ConversationContext>;
```

@ -0,0 +1,21 @@
import React from 'react';
interface Props {
onClick: () => void;
}
export class StagedPlaceholderAttachment extends React.Component<Props> {
public render() {
const { onClick } = this.props;
return (
<div
className="module-staged-placeholder-attachment"
role="button"
onClick={onClick}
>
<div className="module-staged-placeholder-attachment__plus-icon" />
</div>
);
}
}
Loading…
Cancel
Save