diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0367c8743..47c653fe9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -755,6 +755,9 @@ "description": "Shown in triple-dot menu next to message to allow user to start crafting a message with a quotation", "androidKey": "conversation_context__menu_reply_to_message" }, + "replyingToMessage": { + "message": "Replying to:" + }, "originalMessageNotFound": { "message": "Original message not found", "description": "Shown in quote if reference message was not found as message was initially downloaded and processed", diff --git a/js/models/conversations.js b/js/models/conversations.js index 27b96d19f..b9e1e9202 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -1861,7 +1861,6 @@ message => message.get('received_at') <= newestUnreadDate ); - let read = await Promise.all( _.map(oldUnread, async providedM => { const m = MessageController.register(providedM.id, providedM); diff --git a/js/models/messages.js b/js/models/messages.js index c7d170465..77987e0ea 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -616,7 +616,6 @@ onSelectMessageUnchecked: () => this.selectMessageUnchecked(), onCopyPubKey: () => this.copyPubKey(), onBanUser: () => this.banUser(), - onReply: () => this.trigger('reply', this), onRetrySend: () => this.retrySend(), onShowDetail: () => this.trigger('show-message-detail', this), onDelete: () => this.trigger('delete', this), diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 803327ddc..c0cab9f9a 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -77,11 +77,6 @@ 'scroll-to-message', this.scrollToMessage ); - this.listenTo( - this.model.messageCollection, - 'reply', - this.setQuoteMessage - ); this.listenTo( this.model.messageCollection, 'show-contact-detail', diff --git a/stylesheets/_session.scss b/stylesheets/_session.scss index 865ed64d2..5cc36cdb9 100644 --- a/stylesheets/_session.scss +++ b/stylesheets/_session.scss @@ -9,7 +9,7 @@ div.spacer-lg { } .subtle { - opacity: 0.6; + opacity: $session-subtle-factor; } .soft { diff --git a/stylesheets/_session_constants.scss b/stylesheets/_session_constants.scss index af870e5a4..c0a5f000a 100644 --- a/stylesheets/_session_constants.scss +++ b/stylesheets/_session_constants.scss @@ -105,9 +105,6 @@ $session-color-light-grey: #a0a0a0; $session-color-dark-grey: #353535; $session-color-black: #000; -$session-background-overlay: #212121; -$session-background: #121212; - // Semantic Colors $session-color-info: $session-shade-11; $session-color-success: #35d388; @@ -207,15 +204,4 @@ $session-fadein-duration: 0.1s; // ///////////////// Various //////////////////// // ////////////////////////////////////////////// -// Backgrounds -@mixin session-dark-background { - background-color: $session-background; -} -@mixin session-dark-background-lighter { - background-color: $session-background-overlay; -} -@mixin session-dark-background-hover { - background-color: $session-shade-7; -} - $composition-container-height: 60px; diff --git a/stylesheets/_session_conversation.scss b/stylesheets/_session_conversation.scss index 50310d006..781ac84f8 100644 --- a/stylesheets/_session_conversation.scss +++ b/stylesheets/_session_conversation.scss @@ -1,12 +1,3 @@ -@keyframes fadein { - from { - opacity: 0; - } - to { - opacity: 1; - } -} - @keyframes toShadow { from { opacity: 1; @@ -144,7 +135,6 @@ @include themify($themes) { border-left: themed('sessionBorder'); border-top: themed('sessionBorder'); - border-bottom: themed('sessionBorder'); } &__blocking-overlay { @@ -221,6 +211,7 @@ min-height: min-content; @include themify($themes) { background: themed('composeViewBackground'); + border-top: themed('sessionBorder'); } & > .session-icon-button { diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 03e63bd9c..0962d19b0 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -36,6 +36,8 @@ import { isFileDangerous } from '../../util/isFileDangerous'; import { ColorType, LocalizerType } from '../../types/Util'; import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; +import { ReplyingToMessageProps } from '../session/conversation/SessionCompositionBox'; +import _ from 'lodash'; declare global { interface Window { @@ -65,7 +67,7 @@ export interface Props { isModerator?: boolean; text?: string; textPending?: boolean; - id?: string; + id: string; collapseMetadata?: boolean; direction: 'incoming' | 'outgoing'; timestamp: number; @@ -111,7 +113,7 @@ export interface Props { onClickLinkPreview?: (url: string) => void; onCopyText?: () => void; onSelectMessage: (messageId: string) => void; - onReply?: () => void; + onReply?: (messageProps: ReplyingToMessageProps) => void; onRetrySend?: () => void; onDownload?: (isDangerous: boolean) => void; onDelete?: () => void; @@ -145,6 +147,7 @@ export class Message extends React.PureComponent { this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this); this.showMenuBound = this.showMenu.bind(this); this.handleImageErrorBound = this.handleImageError.bind(this); + this.onReplyPrivate = this.onReplyPrivate.bind(this); this.state = { expiring: false, @@ -812,7 +815,6 @@ export class Message extends React.PureComponent { onReply, onRetrySend, onShowDetail, - onCopyPubKey, isPublic, i18n, isModerator, @@ -827,10 +829,10 @@ export class Message extends React.PureComponent { // Wraps a function to prevent event propagation, thus preventing // message selection whenever any of the menu buttons are pressed. - const wrap = (f: any) => (event: Event) => { + const wrap = (f: any, ...args: Array) => (event: Event) => { event.stopPropagation(); if (f) { - f(); + f(...args); } }; @@ -879,7 +881,7 @@ export class Message extends React.PureComponent { attributes={{ className: 'module-message__context__reply', }} - onClick={wrap(onReply)} + onClick={this.onReplyPrivate} > {i18n('replyToMessage')} @@ -1149,4 +1151,19 @@ export class Message extends React.PureComponent { ); } + + private onReplyPrivate(e: Event) { + e.stopPropagation(); + if (this.props && this.props.onReply) { + const messageProps = _.pick( + this.props, + 'id', + 'timestamp', + 'attachments', + 'text', + 'convoId' + ); + this.props.onReply(messageProps); + } + } } diff --git a/ts/components/session/Flex.tsx b/ts/components/session/Flex.tsx index e19665bc3..699e85f9d 100644 --- a/ts/components/session/Flex.tsx +++ b/ts/components/session/Flex.tsx @@ -38,77 +38,18 @@ export interface FlexProps { } export const Flex = styled.div` - ${props => - (props.container && - css` - display: flex; - `) || - css` - display: block; - `}; - ${props => - props.justifyContent && - css` - justifycontent: ${props.justifyContent || 'flex-start'}; - `}; - ${props => - props.flexDirection && - css` - flexdirection: ${props.flexDirection || 'row'}; - `}; - ${props => - props.flexGrow && - css` - flexgrow: ${props.flexGrow || '0'}; - `}; - ${props => - props.flexBasis && - css` - flexbasis: ${props.flexBasis || 'auto'}; - `}; - ${props => - props.flexShrink && - css` - flexshrink: ${props.flexShrink || '1'}; - `}; - ${props => - props.flexWrap && - css` - flexwrap: ${props.flexWrap || 'nowrap'}; - `}; - ${props => - props.flex && - css` - flex: ${props.flex || '0 1 auto'}; - `}; - ${props => - props.alignItems && - css` - alignitems: ${props.alignItems || 'stretch'}; - `}; - ${props => - props.margin && - css` - margin: ${props.margin || '0'}; - `}; - ${props => - props.padding && - css` - padding: ${props.padding || '0'}; - `}; - ${props => - props.width && - css` - width: ${props.width || 'auto'}; - `}; - ${props => - props.height && - css` - height: ${props.height || 'auto'}; - `}; - ${props => - props.maxWidth && - css` - maxwidth: ${props.maxWidth || 'none'}; - `}; + display: ${props => (props.container ? 'flex' : 'block')}; + justify-content: ${props => props.justifyContent || 'flex-start'}; + flex-direction: ${props => props.flexDirection || 'row'}; + flex-grow: ${props => props.flexGrow || '0'}; + flex-basis: ${props => props.flexBasis || 'auto'}; + flex-shrink: ${props => props.flexShrink || '1'}; + flex-wrap: ${props => props.flexWrap || 'nowrap'}; + flex: ${props => props.flex || '0 1 auto'}; + align-items: ${props => props.alignItems || 'stretch'}; + margin: ${props => props.margin || '0'}; + padding: ${props => props.padding || '0'}; + width: ${props => props.justifyContent || 'auto'}; + height: ${props => props.height || 'auto'}; + max-width: ${props => props.maxWidth || 'none'}; `; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 8bbe7048d..be0b0bcab 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -9,12 +9,23 @@ import TextareaAutosize from 'react-autosize-textarea'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionEmojiPanel } from './SessionEmojiPanel'; import { SessionRecording } from './SessionRecording'; +import { Props as MessageProps } from '../../conversation/Message'; -import { SignalService } from '../../../../ts/protobuf'; +import { SignalService } from '../../../protobuf'; import { Constants } from '../../../session'; import { toArray } from 'react-emoji-render'; +import { SessionQuotedMessageComposition } from './SessionQuotedMessageComposition'; +import { Flex } from '../Flex'; + +export interface ReplyingToMessageProps { + convoId: string; + id: string; + timestamp: number; + text?: string; + attachments?: Array; +} interface Props { placeholder?: string; @@ -28,6 +39,8 @@ interface Props { onExitVoiceNoteView: any; dropZoneFiles: FileList; + quotedMessageProps?: ReplyingToMessageProps; + removeQuotedMessage: () => void; } interface State { @@ -48,7 +61,6 @@ export class SessionCompositionBox extends React.Component { constructor(props: any) { super(props); - this.state = { message: '', attachments: [], @@ -70,6 +82,7 @@ export class SessionCompositionBox extends React.Component { this.renderRecordingView = this.renderRecordingView.bind(this); this.renderCompositionView = this.renderCompositionView.bind(this); + this.renderQuotedMessage = this.renderQuotedMessage.bind(this); // Recording view functions this.sendVoiceMessage = this.sendVoiceMessage.bind(this); @@ -102,13 +115,14 @@ export class SessionCompositionBox extends React.Component { const { showRecordingView } = this.state; return ( -
- {showRecordingView ? ( - <>{this.renderRecordingView()} - ) : ( - <>{this.renderCompositionView()} - )} -
+ + {this.renderQuotedMessage()} +
+ {showRecordingView + ? this.renderRecordingView() + : this.renderCompositionView()} +
+
); } @@ -227,6 +241,19 @@ export class SessionCompositionBox extends React.Component { ); } + private renderQuotedMessage() { + const { quotedMessageProps, removeQuotedMessage } = this.props; + if (quotedMessageProps && quotedMessageProps.id) { + return ( + + ); + } + return <>; + } + private onChooseAttachment() { this.fileInput.current?.click(); } diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 21a719219..2b4ff4edd 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -4,10 +4,13 @@ import React from 'react'; import classNames from 'classnames'; -import { SessionCompositionBox } from './SessionCompositionBox'; +import { + ReplyingToMessageProps, + SessionCompositionBox, +} from './SessionCompositionBox'; import { SessionProgress } from '../SessionProgress'; -import { Message } from '../../conversation/Message'; +import { Message, Props as MessageProps } from '../../conversation/Message'; import { TimerNotification } from '../../conversation/TimerNotification'; import { getTimestamp } from './SessionConversationManager'; @@ -21,7 +24,7 @@ import { UserUtil } from '../../../util'; import { MultiDeviceProtocol } from '../../../session/protocols'; import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader'; import { SessionRightPanelWithDetails } from './SessionRightPanel'; -import { Theme } from '../../../state/ducks/SessionTheme'; +import { SessionTheme } from '../../../state/ducks/SessionTheme'; import { DefaultTheme } from 'styled-components'; interface State { @@ -130,6 +133,8 @@ export class SessionConversation extends React.Component { this.onMessageFailure = this.onMessageFailure.bind(this); this.deleteSelectedMessages = this.deleteSelectedMessages.bind(this); + this.replyToMessage = this.replyToMessage.bind(this); + this.messagesEndRef = React.createRef(); this.messageContainerRef = React.createRef(); @@ -197,6 +202,7 @@ export class SessionConversation extends React.Component { showRecordingView, showOptionsPane, showScrollButton, + quotedMessageProps, } = this.state; const loading = !doneInitialScroll; const selectionMode = !!this.state.selectedMessages.length; @@ -220,7 +226,7 @@ export class SessionConversation extends React.Component { const showMessageDetails = this.state.infoViewState === 'messageDetails'; return ( - +
{this.renderHeader()}
{/* { )} -
+ ); } @@ -383,6 +389,7 @@ export class SessionConversation extends React.Component { }; messageProps.quote = quoteProps || undefined; + messageProps.onReply = this.replyToMessage; return ; } @@ -1029,6 +1036,15 @@ export class SessionConversation extends React.Component { }); } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~ + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + private replyToMessage(quotedMessageProps?: ReplyingToMessageProps) { + if (!_.isEqual(this.state.quotedMessageProps, quotedMessageProps)) { + this.setState({ quotedMessageProps }); + } + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/ts/components/session/conversation/SessionQuotedMessageComposition.tsx b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx new file mode 100644 index 000000000..fc58665be --- /dev/null +++ b/ts/components/session/conversation/SessionQuotedMessageComposition.tsx @@ -0,0 +1,67 @@ +import React, { useContext } from 'react'; +import { Flex } from '../Flex'; +import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; +import { ReplyingToMessageProps } from './SessionCompositionBox'; +import styled, { DefaultTheme, ThemeContext } from 'styled-components'; + +// tslint:disable: react-unused-props-and-state +interface Props { + quotedMessageProps: ReplyingToMessageProps; + removeQuotedMessage: any; +} + +const QuotedMessageComposition = styled.div` + width: 50%; +`; + +const QuotedMessageCompositionReply = styled.div` + background: ${props => props.theme.colors.quoteBottomBarBackground}; + border-radius: ${props => props.theme.common.margins.sm}; + padding: ${props => props.theme.common.margins.xs}; + box-shadow: ${props => props.theme.colors.sessionShadow}; + margin: ${props => props.theme.common.margins.xs}; +`; + +const Subtle = styled.div` + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + display: -webkit-box; + color: ${props => props.theme.colors.textColor}; +`; + +const ReplyingTo = styled.div` + color: ${props => props.theme.colors.textColor}; +`; + +export const SessionQuotedMessageComposition = (props: Props) => { + const { quotedMessageProps, removeQuotedMessage } = props; + const theme = useContext(ThemeContext); + + const { text: body, attachments } = quotedMessageProps; + const hasAttachments = attachments && attachments.length > 0; + return ( + + + {window.i18n('replyingToMessage')} + + + + + {(hasAttachments && window.i18n('mediaMessage')) || body} + + + + ); +}; diff --git a/ts/state/ducks/SessionTheme.tsx b/ts/state/ducks/SessionTheme.tsx index 34b11f217..36a9a4ef1 100644 --- a/ts/state/ducks/SessionTheme.tsx +++ b/ts/state/ducks/SessionTheme.tsx @@ -13,16 +13,20 @@ const borderLightTheme = '#f1f1f1'; const borderDarkTheme = '#ffffff0F'; const borderAvatarColor = '#00000059'; -const commonThemes = { +const common = { fonts: { sessionFontDefault: 'Public Sans', sessionFontAccent: 'Loor', sessionFontMono: 'SpaceMono', }, + margins: { + xs: '5px', + sm: '10px', + }, }; export const lightTheme: DefaultTheme = { - commonThemes, + common, colors: { accent: accentLightTheme, accentButton: black, @@ -76,7 +80,7 @@ export const lightTheme: DefaultTheme = { }; export const darkTheme = { - commonThemes, + common, colors: { accent: accentDarkTheme, accentButton: accentDarkTheme, @@ -130,7 +134,7 @@ export const darkTheme = { }, }; -export const Theme = ({ +export const SessionTheme = ({ children, theme, }: { diff --git a/ts/styled.d.ts b/ts/styled.d.ts index 7f2ac8231..e29059160 100644 --- a/ts/styled.d.ts +++ b/ts/styled.d.ts @@ -2,12 +2,16 @@ import 'styled-components'; declare module 'styled-components' { export interface DefaultTheme { - commonThemes: { + common: { fonts: { sessionFontDefault: string; sessionFontAccent: string; sessionFontMono: string; }; + margins: { + xs: string; + sm: string; + }; }; colors: { accent: string;