add reply to message UI logic

pull/1387/head
Audric Ackermann 5 years ago
parent 1a379d2466
commit b7f5a32570
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -755,6 +755,9 @@
"description": "Shown in triple-dot menu next to message to allow user to start crafting a message with a quotation", "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" "androidKey": "conversation_context__menu_reply_to_message"
}, },
"replyingToMessage": {
"message": "Replying to:"
},
"originalMessageNotFound": { "originalMessageNotFound": {
"message": "Original message not found", "message": "Original message not found",
"description": "Shown in quote if reference message was not found as message was initially downloaded and processed", "description": "Shown in quote if reference message was not found as message was initially downloaded and processed",

@ -1861,7 +1861,6 @@
message => message.get('received_at') <= newestUnreadDate message => message.get('received_at') <= newestUnreadDate
); );
let read = await Promise.all( let read = await Promise.all(
_.map(oldUnread, async providedM => { _.map(oldUnread, async providedM => {
const m = MessageController.register(providedM.id, providedM); const m = MessageController.register(providedM.id, providedM);

@ -616,7 +616,6 @@
onSelectMessageUnchecked: () => this.selectMessageUnchecked(), onSelectMessageUnchecked: () => this.selectMessageUnchecked(),
onCopyPubKey: () => this.copyPubKey(), onCopyPubKey: () => this.copyPubKey(),
onBanUser: () => this.banUser(), onBanUser: () => this.banUser(),
onReply: () => this.trigger('reply', this),
onRetrySend: () => this.retrySend(), onRetrySend: () => this.retrySend(),
onShowDetail: () => this.trigger('show-message-detail', this), onShowDetail: () => this.trigger('show-message-detail', this),
onDelete: () => this.trigger('delete', this), onDelete: () => this.trigger('delete', this),

@ -77,11 +77,6 @@
'scroll-to-message', 'scroll-to-message',
this.scrollToMessage this.scrollToMessage
); );
this.listenTo(
this.model.messageCollection,
'reply',
this.setQuoteMessage
);
this.listenTo( this.listenTo(
this.model.messageCollection, this.model.messageCollection,
'show-contact-detail', 'show-contact-detail',

@ -9,7 +9,7 @@ div.spacer-lg {
} }
.subtle { .subtle {
opacity: 0.6; opacity: $session-subtle-factor;
} }
.soft { .soft {

@ -105,9 +105,6 @@ $session-color-light-grey: #a0a0a0;
$session-color-dark-grey: #353535; $session-color-dark-grey: #353535;
$session-color-black: #000; $session-color-black: #000;
$session-background-overlay: #212121;
$session-background: #121212;
// Semantic Colors // Semantic Colors
$session-color-info: $session-shade-11; $session-color-info: $session-shade-11;
$session-color-success: #35d388; $session-color-success: #35d388;
@ -207,15 +204,4 @@ $session-fadein-duration: 0.1s;
// ///////////////// Various //////////////////// // ///////////////// 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; $composition-container-height: 60px;

@ -1,12 +1,3 @@
@keyframes fadein {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes toShadow { @keyframes toShadow {
from { from {
opacity: 1; opacity: 1;
@ -144,7 +135,6 @@
@include themify($themes) { @include themify($themes) {
border-left: themed('sessionBorder'); border-left: themed('sessionBorder');
border-top: themed('sessionBorder'); border-top: themed('sessionBorder');
border-bottom: themed('sessionBorder');
} }
&__blocking-overlay { &__blocking-overlay {
@ -221,6 +211,7 @@
min-height: min-content; min-height: min-content;
@include themify($themes) { @include themify($themes) {
background: themed('composeViewBackground'); background: themed('composeViewBackground');
border-top: themed('sessionBorder');
} }
& > .session-icon-button { & > .session-icon-button {

@ -36,6 +36,8 @@ import { isFileDangerous } from '../../util/isFileDangerous';
import { ColorType, LocalizerType } from '../../types/Util'; import { ColorType, LocalizerType } from '../../types/Util';
import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu'; import { ContextMenu, ContextMenuTrigger, MenuItem } from 'react-contextmenu';
import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon'; import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import { ReplyingToMessageProps } from '../session/conversation/SessionCompositionBox';
import _ from 'lodash';
declare global { declare global {
interface Window { interface Window {
@ -65,7 +67,7 @@ export interface Props {
isModerator?: boolean; isModerator?: boolean;
text?: string; text?: string;
textPending?: boolean; textPending?: boolean;
id?: string; id: string;
collapseMetadata?: boolean; collapseMetadata?: boolean;
direction: 'incoming' | 'outgoing'; direction: 'incoming' | 'outgoing';
timestamp: number; timestamp: number;
@ -111,7 +113,7 @@ export interface Props {
onClickLinkPreview?: (url: string) => void; onClickLinkPreview?: (url: string) => void;
onCopyText?: () => void; onCopyText?: () => void;
onSelectMessage: (messageId: string) => void; onSelectMessage: (messageId: string) => void;
onReply?: () => void; onReply?: (messageProps: ReplyingToMessageProps) => void;
onRetrySend?: () => void; onRetrySend?: () => void;
onDownload?: (isDangerous: boolean) => void; onDownload?: (isDangerous: boolean) => void;
onDelete?: () => void; onDelete?: () => void;
@ -145,6 +147,7 @@ export class Message extends React.PureComponent<Props, State> {
this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this); this.captureMenuTriggerBound = this.captureMenuTrigger.bind(this);
this.showMenuBound = this.showMenu.bind(this); this.showMenuBound = this.showMenu.bind(this);
this.handleImageErrorBound = this.handleImageError.bind(this); this.handleImageErrorBound = this.handleImageError.bind(this);
this.onReplyPrivate = this.onReplyPrivate.bind(this);
this.state = { this.state = {
expiring: false, expiring: false,
@ -812,7 +815,6 @@ export class Message extends React.PureComponent<Props, State> {
onReply, onReply,
onRetrySend, onRetrySend,
onShowDetail, onShowDetail,
onCopyPubKey,
isPublic, isPublic,
i18n, i18n,
isModerator, isModerator,
@ -827,10 +829,10 @@ export class Message extends React.PureComponent<Props, State> {
// Wraps a function to prevent event propagation, thus preventing // Wraps a function to prevent event propagation, thus preventing
// message selection whenever any of the menu buttons are pressed. // message selection whenever any of the menu buttons are pressed.
const wrap = (f: any) => (event: Event) => { const wrap = (f: any, ...args: Array<any>) => (event: Event) => {
event.stopPropagation(); event.stopPropagation();
if (f) { if (f) {
f(); f(...args);
} }
}; };
@ -879,7 +881,7 @@ export class Message extends React.PureComponent<Props, State> {
attributes={{ attributes={{
className: 'module-message__context__reply', className: 'module-message__context__reply',
}} }}
onClick={wrap(onReply)} onClick={this.onReplyPrivate}
> >
{i18n('replyToMessage')} {i18n('replyToMessage')}
</MenuItem> </MenuItem>
@ -1149,4 +1151,19 @@ export class Message extends React.PureComponent<Props, State> {
</div> </div>
); );
} }
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);
}
}
} }

@ -38,77 +38,18 @@ export interface FlexProps {
} }
export const Flex = styled.div<FlexProps>` export const Flex = styled.div<FlexProps>`
${props => display: ${props => (props.container ? 'flex' : 'block')};
(props.container && justify-content: ${props => props.justifyContent || 'flex-start'};
css` flex-direction: ${props => props.flexDirection || 'row'};
display: flex; flex-grow: ${props => props.flexGrow || '0'};
`) || flex-basis: ${props => props.flexBasis || 'auto'};
css` flex-shrink: ${props => props.flexShrink || '1'};
display: block; flex-wrap: ${props => props.flexWrap || 'nowrap'};
`}; flex: ${props => props.flex || '0 1 auto'};
${props => align-items: ${props => props.alignItems || 'stretch'};
props.justifyContent && margin: ${props => props.margin || '0'};
css` padding: ${props => props.padding || '0'};
justifycontent: ${props.justifyContent || 'flex-start'}; width: ${props => props.justifyContent || 'auto'};
`}; height: ${props => props.height || 'auto'};
${props => max-width: ${props => props.maxWidth || 'none'};
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'};
`};
`; `;

@ -9,12 +9,23 @@ import TextareaAutosize from 'react-autosize-textarea';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon'; import { SessionIconButton, SessionIconSize, SessionIconType } from '../icon';
import { SessionEmojiPanel } from './SessionEmojiPanel'; import { SessionEmojiPanel } from './SessionEmojiPanel';
import { SessionRecording } from './SessionRecording'; 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 { Constants } from '../../../session';
import { toArray } from 'react-emoji-render'; 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<any>;
}
interface Props { interface Props {
placeholder?: string; placeholder?: string;
@ -28,6 +39,8 @@ interface Props {
onExitVoiceNoteView: any; onExitVoiceNoteView: any;
dropZoneFiles: FileList; dropZoneFiles: FileList;
quotedMessageProps?: ReplyingToMessageProps;
removeQuotedMessage: () => void;
} }
interface State { interface State {
@ -48,7 +61,6 @@ export class SessionCompositionBox extends React.Component<Props, State> {
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
message: '', message: '',
attachments: [], attachments: [],
@ -70,6 +82,7 @@ export class SessionCompositionBox extends React.Component<Props, State> {
this.renderRecordingView = this.renderRecordingView.bind(this); this.renderRecordingView = this.renderRecordingView.bind(this);
this.renderCompositionView = this.renderCompositionView.bind(this); this.renderCompositionView = this.renderCompositionView.bind(this);
this.renderQuotedMessage = this.renderQuotedMessage.bind(this);
// Recording view functions // Recording view functions
this.sendVoiceMessage = this.sendVoiceMessage.bind(this); this.sendVoiceMessage = this.sendVoiceMessage.bind(this);
@ -102,13 +115,14 @@ export class SessionCompositionBox extends React.Component<Props, State> {
const { showRecordingView } = this.state; const { showRecordingView } = this.state;
return ( return (
<Flex flexDirection="column">
{this.renderQuotedMessage()}
<div className="composition-container"> <div className="composition-container">
{showRecordingView ? ( {showRecordingView
<>{this.renderRecordingView()}</> ? this.renderRecordingView()
) : ( : this.renderCompositionView()}
<>{this.renderCompositionView()}</>
)}
</div> </div>
</Flex>
); );
} }
@ -227,6 +241,19 @@ export class SessionCompositionBox extends React.Component<Props, State> {
); );
} }
private renderQuotedMessage() {
const { quotedMessageProps, removeQuotedMessage } = this.props;
if (quotedMessageProps && quotedMessageProps.id) {
return (
<SessionQuotedMessageComposition
quotedMessageProps={quotedMessageProps}
removeQuotedMessage={removeQuotedMessage}
/>
);
}
return <></>;
}
private onChooseAttachment() { private onChooseAttachment() {
this.fileInput.current?.click(); this.fileInput.current?.click();
} }

@ -4,10 +4,13 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { SessionCompositionBox } from './SessionCompositionBox'; import {
ReplyingToMessageProps,
SessionCompositionBox,
} from './SessionCompositionBox';
import { SessionProgress } from '../SessionProgress'; import { SessionProgress } from '../SessionProgress';
import { Message } from '../../conversation/Message'; import { Message, Props as MessageProps } from '../../conversation/Message';
import { TimerNotification } from '../../conversation/TimerNotification'; import { TimerNotification } from '../../conversation/TimerNotification';
import { getTimestamp } from './SessionConversationManager'; import { getTimestamp } from './SessionConversationManager';
@ -21,7 +24,7 @@ import { UserUtil } from '../../../util';
import { MultiDeviceProtocol } from '../../../session/protocols'; import { MultiDeviceProtocol } from '../../../session/protocols';
import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader'; import { ConversationHeaderWithDetails } from '../../conversation/ConversationHeader';
import { SessionRightPanelWithDetails } from './SessionRightPanel'; import { SessionRightPanelWithDetails } from './SessionRightPanel';
import { Theme } from '../../../state/ducks/SessionTheme'; import { SessionTheme } from '../../../state/ducks/SessionTheme';
import { DefaultTheme } from 'styled-components'; import { DefaultTheme } from 'styled-components';
interface State { interface State {
@ -130,6 +133,8 @@ export class SessionConversation extends React.Component<Props, State> {
this.onMessageFailure = this.onMessageFailure.bind(this); this.onMessageFailure = this.onMessageFailure.bind(this);
this.deleteSelectedMessages = this.deleteSelectedMessages.bind(this); this.deleteSelectedMessages = this.deleteSelectedMessages.bind(this);
this.replyToMessage = this.replyToMessage.bind(this);
this.messagesEndRef = React.createRef(); this.messagesEndRef = React.createRef();
this.messageContainerRef = React.createRef(); this.messageContainerRef = React.createRef();
@ -197,6 +202,7 @@ export class SessionConversation extends React.Component<Props, State> {
showRecordingView, showRecordingView,
showOptionsPane, showOptionsPane,
showScrollButton, showScrollButton,
quotedMessageProps,
} = this.state; } = this.state;
const loading = !doneInitialScroll; const loading = !doneInitialScroll;
const selectionMode = !!this.state.selectedMessages.length; const selectionMode = !!this.state.selectedMessages.length;
@ -220,7 +226,7 @@ export class SessionConversation extends React.Component<Props, State> {
const showMessageDetails = this.state.infoViewState === 'messageDetails'; const showMessageDetails = this.state.infoViewState === 'messageDetails';
return ( return (
<Theme theme={this.props.theme}> <SessionTheme theme={this.props.theme}>
<div className="conversation-header">{this.renderHeader()}</div> <div className="conversation-header">{this.renderHeader()}</div>
{/* <SessionProgress {/* <SessionProgress
@ -300,7 +306,7 @@ export class SessionConversation extends React.Component<Props, State> {
<SessionRightPanelWithDetails {...groupSettingsProps} /> <SessionRightPanelWithDetails {...groupSettingsProps} />
</div> </div>
)} )}
</Theme> </SessionTheme>
); );
} }
@ -383,6 +389,7 @@ export class SessionConversation extends React.Component<Props, State> {
}; };
messageProps.quote = quoteProps || undefined; messageProps.quote = quoteProps || undefined;
messageProps.onReply = this.replyToMessage;
return <Message {...messageProps} />; return <Message {...messageProps} />;
} }
@ -1029,6 +1036,15 @@ export class SessionConversation extends React.Component<Props, State> {
}); });
} }
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
private replyToMessage(quotedMessageProps?: ReplyingToMessageProps) {
if (!_.isEqual(this.state.quotedMessageProps, quotedMessageProps)) {
this.setState({ quotedMessageProps });
}
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~ // ~~~~~~~~~~~ KEYBOARD NAVIGATION ~~~~~~~~~~~~
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

@ -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 (
<QuotedMessageComposition>
<Flex
container={true}
justifyContent="space-between"
flexGrow={1}
margin={theme.common.margins.xs}
>
<ReplyingTo>{window.i18n('replyingToMessage')}</ReplyingTo>
<SessionIconButton
iconType={SessionIconType.Exit}
iconSize={SessionIconSize.Small}
onClick={removeQuotedMessage}
/>
</Flex>
<QuotedMessageCompositionReply>
<Subtle>
{(hasAttachments && window.i18n('mediaMessage')) || body}
</Subtle>
</QuotedMessageCompositionReply>
</QuotedMessageComposition>
);
};

@ -13,16 +13,20 @@ const borderLightTheme = '#f1f1f1';
const borderDarkTheme = '#ffffff0F'; const borderDarkTheme = '#ffffff0F';
const borderAvatarColor = '#00000059'; const borderAvatarColor = '#00000059';
const commonThemes = { const common = {
fonts: { fonts: {
sessionFontDefault: 'Public Sans', sessionFontDefault: 'Public Sans',
sessionFontAccent: 'Loor', sessionFontAccent: 'Loor',
sessionFontMono: 'SpaceMono', sessionFontMono: 'SpaceMono',
}, },
margins: {
xs: '5px',
sm: '10px',
},
}; };
export const lightTheme: DefaultTheme = { export const lightTheme: DefaultTheme = {
commonThemes, common,
colors: { colors: {
accent: accentLightTheme, accent: accentLightTheme,
accentButton: black, accentButton: black,
@ -76,7 +80,7 @@ export const lightTheme: DefaultTheme = {
}; };
export const darkTheme = { export const darkTheme = {
commonThemes, common,
colors: { colors: {
accent: accentDarkTheme, accent: accentDarkTheme,
accentButton: accentDarkTheme, accentButton: accentDarkTheme,
@ -130,7 +134,7 @@ export const darkTheme = {
}, },
}; };
export const Theme = ({ export const SessionTheme = ({
children, children,
theme, theme,
}: { }: {

6
ts/styled.d.ts vendored

@ -2,12 +2,16 @@ import 'styled-components';
declare module 'styled-components' { declare module 'styled-components' {
export interface DefaultTheme { export interface DefaultTheme {
commonThemes: { common: {
fonts: { fonts: {
sessionFontDefault: string; sessionFontDefault: string;
sessionFontAccent: string; sessionFontAccent: string;
sessionFontMono: string; sessionFontMono: string;
}; };
margins: {
xs: string;
sm: string;
};
}; };
colors: { colors: {
accent: string; accent: string;

Loading…
Cancel
Save