From c71dcf0139c9d0fad6f060c5da8c01fce38e7ee7 Mon Sep 17 00:00:00 2001 From: Scott Nonnenberg Date: Wed, 18 Apr 2018 13:06:33 -0700 Subject: [PATCH] Show current quoted message above composition field Note that substantial changes will be required for the updated Android mockups, putting the quotation into the text box next to the attachment preview. --- images/close-circle.svg | 1 + js/views/conversation_view.js | 70 ++++++++++++++- stylesheets/_conversation.scss | 23 +++++ stylesheets/_ios.scss | 75 ++++++++++++++++ ts/components/conversation/Quote.md | 129 +++++++++++++++++++++++++++ ts/components/conversation/Quote.tsx | 18 ++++ 6 files changed, 314 insertions(+), 2 deletions(-) create mode 100644 images/close-circle.svg diff --git a/images/close-circle.svg b/images/close-circle.svg new file mode 100644 index 000000000..5a37396e2 --- /dev/null +++ b/images/close-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index e077e4690..bd2094081 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -130,7 +130,7 @@ 'scroll-to-message', this.scrollToMessage ); - this.listenTo(this.model.messageCollection, 'reply', this.setReplyMessage); + this.listenTo(this.model.messageCollection, 'reply', this.setQuoteMessage); this.lazyUpdateVerified = _.debounce( this.model.updateVerified.bind(this.model), @@ -275,6 +275,9 @@ if (this.scrollDownButton) { this.scrollDownButton.remove(); } + if (this.quoteView) { + this.quoteView.remove(); + } if (this.panels && this.panels.length) { for (let i = 0, max = this.panels.length; i < max; i += 1) { const panel = this.panels[i]; @@ -1062,9 +1065,66 @@ this.focusMessageField(); }, - setMessageReply(message) { + setQuoteMessage(message) { this.quotedMessage = message; console.log('setMessageReply', this.quotedMessage); + + this.renderQuotedMessage(); + }, + + makeQuote(quotedMessage) { + const contact = quotedMessage.getContact(); + const attachments = quotedMessage.get('attachments'); + const first = attachments ? attachments[0] : null; + + return { + author: contact.id, + id: quotedMessage.get('sent_at'), + text: quotedMessage.get('body'), + attachments: !first ? [] : [{ + contentType: first.contentType, + fileName: first.fileName, + }], + }; + }, + + renderQuotedMessage() { + if (this.quoteView) { + this.quoteView.remove(); + this.quoteView = null; + } + if (!this.quotedMessage) { + this.updateMessageFieldSize({}); + return; + } + + const message = new Whisper.Message({ + quote: this.makeQuote(this.quotedMessage), + }); + console.log('quoted message attributes', message.attributes); + message.quotedMessage = this.quotedMessage; + const props = Object.assign({}, message.getPropsForQuote(), { + onClose: () => { + console.log('onClose!'); + this.setQuoteMessage(null); + }, + }); + + this.listenTo(message, 'scroll-to-message', this.scrollToMessage); + + console.log('props', props); + const contact = this.quotedMessage.getContact(); + if (contact) { + this.listenTo(contact, 'change:color', this.renderQuotedMesage); + } + + this.quoteView = new Whisper.ReactWrapperView({ + className: 'quote-wrapper', + Component: window.Signal.Components.Quote, + props, + }); + this.$('.bottom-bar').prepend(this.quoteView.el); + this.updateMessageFieldSize({}); }, async sendMessage(e) { @@ -1168,9 +1228,15 @@ const $attachmentPreviews = this.$('.attachment-previews'); const $bottomBar = this.$('.bottom-bar'); + const includeMargin = true; + const quoteHeight = this.quoteView + ? this.quoteView.$el.outerHeight(includeMargin) + : 0; + const height = this.$messageField.outerHeight() + $attachmentPreviews.outerHeight() + this.$emojiPanelContainer.outerHeight() + + quoteHeight + parseInt($bottomBar.css('min-height'), 10); $bottomBar.outerHeight(height); diff --git a/stylesheets/_conversation.scss b/stylesheets/_conversation.scss index b83997d4a..968201e0b 100644 --- a/stylesheets/_conversation.scss +++ b/stylesheets/_conversation.scss @@ -701,6 +701,7 @@ span.status { cursor: auto; } + position: relative; cursor: pointer; display: flex; flex-direction: row; @@ -718,6 +719,9 @@ span.status { // Accent color border: border-left-width: 3px; border-left-style: solid; + border-top: 1px solid lightgray; + border-bottom: 1px solid lightgray; + border-right: 1px solid lightgray; .primary { flex-grow: 1; @@ -766,6 +770,16 @@ span.status { } } + .close-container { + position: absolute; + top: 0px; + right: 0px; + height: 18px; + width: 18px; + + @include color-svg('../images/x.svg', white); + } + .icon-container { flex: initial; min-width: 48px; @@ -833,6 +847,15 @@ span.status { margin-top: $android-bubble-quote-padding - $android-bubble-padding-vertical; } +.bottom-bar .quoted-message { + margin: 0px; +} + +.bottom-bar .quote-wrapper { + margin-right: 5px; + margin-bottom: 5px; +} + .incoming .quoted-message { background-color: rgba(white, 0.6); border-left-color: white; diff --git a/stylesheets/_ios.scss b/stylesheets/_ios.scss index 26bf2800b..4b7ee8e12 100644 --- a/stylesheets/_ios.scss +++ b/stylesheets/_ios.scss @@ -114,6 +114,9 @@ $ios-border-color: rgba(0,0,0,0.1); } .quoted-message { + border-top-left-radius: 15px; + border-top-right-radius: 15px; + // Not ideal, but necessary to override the specificity of the android theme color // classes used in conversations.scss background-color: white !important; @@ -184,6 +187,28 @@ $ios-border-color: rgba(0,0,0,0.1); } } + .close-container { + flex: initial; + min-width: 32px; + width: 32px; + height: 48px; + position: relative; + + display: flex; + align-items: center; + justify-content: center; + + -webkit-mask: none; + background: none; + + .close-button { + height: 20px; + width: 20px; + + @include color-svg('../images/close-circle.svg', $grey_l4); + } + } + .from-me { .primary { .text, @@ -218,6 +243,56 @@ $ios-border-color: rgba(0,0,0,0.1); background-color: lightgray !important; } + .bottom-bar { + .quote-wrapper { + margin-right: 0px; + margin-bottom: 15px; + } + + .quoted-message { + border-top-left-radius: 0px; + border-top-right-radius: 0px; + + background: none !important; + border: none !important; + + .primary { + padding: 0px; + + .ios-label { + color: $grey_l4; + } + } + + .icon-container { + height: 48px; + width: 48px; + min-width: 48px; + + .circle-background { + left: 6px; + right: 6px; + top: 6px; + bottom: 6px; + + background-color: $blue !important; + } + + .icon { + left: 12px; + right: 12px; + top: 12px; + bottom: 12px; + } + + .inner { + padding: 0px; + height: 48px; + } + } + } + } + .attachments .bubbled { border-radius: 15px; diff --git a/ts/components/conversation/Quote.md b/ts/components/conversation/Quote.md index d5cb30174..838135fce 100644 --- a/ts/components/conversation/Quote.md +++ b/ts/components/conversation/Quote.md @@ -893,3 +893,132 @@ const View = Whisper.MessageView; /> ``` + +### In bottom bar + +#### Plain text + +```jsx +
+
+ +
+
+``` + +#### With an icon + +```jsx +
+
+ +
+
+``` + +#### With an image + +```jsx +
+
+ +
+
+``` + +#### With a close button + +```jsx +
+
+ console.log('Close was clicked!')} + i18n={window.i18n} + /> +
+
+``` + +#### With a close button and icon + +```jsx +
+
+ console.log('Close was clicked!')} + i18n={window.i18n} + attachments={[{ + contentType: 'image/jpeg', + fileName: 'llama.jpg', + }]} + /> +
+
+``` + +#### With a close button and image + +```jsx +
+
+ console.log('Close was clicked!')} + i18n={window.i18n} + attachments={[{ + contentType: 'image/gif', + fileName: 'llama.gif', + thumbnail: { + objectUrl: util.gifObjectUrl + }, + }]} + /> +
+
+``` diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index dcca3a183..b2117c8e3 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -14,6 +14,7 @@ interface Props { isFromMe: string; isIncoming: boolean; onClick?: () => void; + onClose?: () => void; text: string; } @@ -153,6 +154,22 @@ export class Quote extends React.Component { return
{label}
; } + public renderClose() { + const { onClose } = this.props; + + if (!onClose) { + return null; + } + + // We need the container to give us the flexibility to implement the iOS design. + // We put the onClick on both because the Android theme juse uses close-container + return ( +
+
+
+ ); + } + public render() { const { authorTitle, @@ -186,6 +203,7 @@ export class Quote extends React.Component { {this.renderText()} {this.renderIconContainer()} + {this.renderClose()} ); }