diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f137e744a..b2c390a81 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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": diff --git a/images/plus-36.svg b/images/plus-36.svg new file mode 100644 index 000000000..a2a8920a4 --- /dev/null +++ b/images/plus-36.svg @@ -0,0 +1 @@ +plus-36 \ No newline at end of file diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 55fb28160..c719e18ee 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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(); }, diff --git a/js/views/file_input_view.js b/js/views/file_input_view.js index 3b21f9a3f..dcd46c768 100644 --- a/js/views/file_input_view.js +++ b/js/views/file_input_view.js @@ -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'); diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 77c1c6e96..12a144919 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -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; diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index a87509a69..8f967947c 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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 { diff --git a/stylesheets/_theme_dark.scss b/stylesheets/_theme_dark.scss index 1b93bad35..2fe02da7d 100644 --- a/stylesheets/_theme_dark.scss +++ b/stylesheets/_theme_dark.scss @@ -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 { diff --git a/ts/components/CaptionEditor.md b/ts/components/CaptionEditor.md index 61d25d709..4da5142a0 100644 --- a/ts/components/CaptionEditor.md +++ b/ts/components/CaptionEditor.md @@ -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} /> ; @@ -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} /> ; @@ -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} /> ; @@ -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} /> ; diff --git a/ts/components/CaptionEditor.tsx b/ts/components/CaptionEditor.tsx index db47c0168..1902a51c0 100644 --- a/ts/components/CaptionEditor.tsx +++ b/ts/components/CaptionEditor.tsx @@ -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 { - private handleKeyUpBound: () => void; +interface State { + caption: string; +} + +export class CaptionEditor extends React.Component { + private handleKeyUpBound: ( + event: React.KeyboardEvent + ) => 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 | 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) { - 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 { }, 200); } + public onSave() { + const { onSave } = this.props; + const { caption } = this.state; + + if (onSave) { + onSave(caption); + } + } + + public onChange(event: React.FormEvent) { + // @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 { } public render() { - const { caption, i18n, close, onChangeCaption } = this.props; + const { i18n, close } = this.props; + const { caption } = this.state; return (
{ {this.renderObject()}
-
- { - if (onChangeCaption) { - onChangeCaption(event.target.value); - } - }} - /> +
+ + {caption ? ( +
+ {i18n('save')} +
+ ) : null} +
); diff --git a/ts/components/conversation/AttachmentList.md b/ts/components/conversation/AttachmentList.md index 4ef3d2570..52254575a 100644 --- a/ts/components/conversation/AttachmentList.md +++ b/ts/components/conversation/AttachmentList.md @@ -19,8 +19,9 @@ const attachments = [ onCloseAttachment={attachment => { console.log('onCloseAttachment', attachment); }} + onAddAttachment={() => console.log('onAddAttachment')} i18n={util.i18n} - />; + /> ; ``` @@ -64,6 +65,7 @@ const attachments = [ onCloseAttachment={attachment => { console.log('onCloseAttachment', attachment); }} + onAddAttachment={() => console.log('onAddAttachment')} i18n={util.i18n} /> ; @@ -101,6 +103,7 @@ const attachments = [ onCloseAttachment={attachment => { console.log('onCloseAttachment', attachment); }} + onAddAttachment={() => console.log('onAddAttachment')} i18n={util.i18n} /> ; diff --git a/ts/components/conversation/AttachmentList.tsx b/ts/components/conversation/AttachmentList.tsx index c7486b2f5..532ce6bca 100644 --- a/ts/components/conversation/AttachmentList.tsx +++ b/ts/components/conversation/AttachmentList.tsx @@ -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 { const { attachments, i18n, + onAddAttachment, onClickAttachment, onCloseAttachment, onClose, @@ -36,6 +40,8 @@ export class AttachmentList extends React.Component { return null; } + const allVisualAttachments = areAllAttachmentsVisual(attachments); + return (
{attachments.length > 1 ? ( @@ -85,6 +91,9 @@ export class AttachmentList extends React.Component { /> ); })} + {allVisualAttachments ? ( + + ) : null}
); diff --git a/ts/components/conversation/ImageGrid.tsx b/ts/components/conversation/ImageGrid.tsx index ddb3b80ed..c63a84efc 100644 --- a/ts/components/conversation/ImageGrid.tsx +++ b/ts/components/conversation/ImageGrid.tsx @@ -389,7 +389,9 @@ function getImageDimensions(attachment: AttachmentType): DimensionsType { }; } -function areAllAttachmentsVisual(attachments?: Array): boolean { +export function areAllAttachmentsVisual( + attachments?: Array +): boolean { if (!attachments) { return false; } @@ -397,7 +399,7 @@ function areAllAttachmentsVisual(attachments?: Array): 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; } } diff --git a/ts/components/conversation/StagedPlaceholderAttachment.md b/ts/components/conversation/StagedPlaceholderAttachment.md new file mode 100644 index 000000000..a7d79ef7d --- /dev/null +++ b/ts/components/conversation/StagedPlaceholderAttachment.md @@ -0,0 +1,10 @@ +```js +const attachment = { + contentType: 'text/plain', + fileName: 'manifesto.txt', +}; + + + console.log('onClick')} /> +; +``` diff --git a/ts/components/conversation/StagedPlaceholderAttachment.tsx b/ts/components/conversation/StagedPlaceholderAttachment.tsx new file mode 100644 index 000000000..b036777de --- /dev/null +++ b/ts/components/conversation/StagedPlaceholderAttachment.tsx @@ -0,0 +1,21 @@ +import React from 'react'; + +interface Props { + onClick: () => void; +} + +export class StagedPlaceholderAttachment extends React.Component { + public render() { + const { onClick } = this.props; + + return ( +
+
+
+ ); + } +}