1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 12:56:41 +07:00

Merge branch 'develop' into master

This commit is contained in:
Fedor Katurov 2020-09-08 13:27:13 +07:00
commit a8dc543c3c
29 changed files with 511 additions and 241 deletions

View file

@ -2,14 +2,14 @@ import React, {
ChangeEvent,
LegacyRef,
memo,
TextareaHTMLAttributes,
useCallback,
useLayoutEffect,
useEffect,
useRef,
useState,
TextareaHTMLAttributes,
} from 'react';
import { getStyle } from '~/utils/dom';
import classNames from 'classnames';
import autosize from 'autosize';
import * as styles from '~/styles/inputs.scss';
import { Icon } from '../Icon';
@ -55,34 +55,13 @@ const Textarea = memo<IProps>(
const onFocus = useCallback(() => setFocused(true), [setFocused]);
const onBlur = useCallback(() => setFocused(false), [setFocused]);
useLayoutEffect(() => {
const lineHeight = parseInt(getStyle(textarea.current, 'line-height'), 10) || 15;
useEffect(() => {
if (!textarea.current) return;
textarea.current.rows = 1; // reset number of rows in textarea
autosize(textarea.current);
const paddingTop = parseInt(getStyle(textarea.current, 'padding-top'), 10) || 0;
const paddingBottom = parseInt(getStyle(textarea.current, 'padding-bottom'), 10) || 0;
const actualScrollHeight =
(textarea.current.scrollHeight || 0) - (paddingTop + paddingBottom);
const rowsCount = Math.round(actualScrollHeight / lineHeight);
let currentRows = minRows;
if (rowsCount > maxRows) {
currentRows = maxRows;
textarea.current.scrollTop = textarea.current.scrollHeight;
} else if (rowsCount <= minRows) {
currentRows = minRows;
} else {
currentRows = rowsCount;
}
textarea.current.rows = currentRows;
setRows(currentRows);
}, [value, minRows, maxRows]);
return () => autosize.destroy(textarea.current);
}, [textarea.current]);
return (
<div
@ -104,6 +83,10 @@ const Textarea = memo<IProps>(
ref={textarea}
onFocus={onFocus}
onBlur={onBlur}
style={{
maxHeight: maxRows * 20,
minHeight: minRows * 20,
}}
{...props}
/>
</div>

View file

@ -42,6 +42,7 @@
display: flex;
flex-direction: column;
z-index: 6;
white-space: nowrap;
animation: appear 0.25s forwards;
}

View file

@ -1,27 +1,82 @@
import React, { FC } from 'react';
import React, { FC, useCallback } from 'react';
import { IMessage } from '~/redux/types';
import styles from './styles.scss';
import { formatText, getURL, getPrettyDate } from '~/utils/dom';
import { formatText, getPrettyDate, getURL } from '~/utils/dom';
import { PRESETS } from '~/constants/urls';
import classNames from 'classnames';
import { Group } from '~/components/containers/Group';
import { CommentMenu } from '~/components/node/CommentMenu';
import { MessageForm } from '~/components/profile/MessageForm';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
interface IProps {
message: IMessage;
incoming: boolean;
onEdit: (id: number) => void;
onDelete: (id: number) => void;
onRestore: (id: number) => void;
onCancelEdit: () => void;
isEditing: boolean;
}
const Message: FC<IProps> = ({ message, incoming }) => (
<div className={classNames(styles.message, { [styles.incoming]: incoming })}>
<Group className={styles.text} dangerouslySetInnerHTML={{ __html: formatText(message.text) }} />
const Message: FC<IProps> = ({
message,
incoming,
onEdit,
onDelete,
isEditing,
onCancelEdit,
onRestore,
}) => {
const onEditClicked = useCallback(() => onEdit(message.id), [onEdit, message.id]);
const onDeleteClicked = useCallback(() => onDelete(message.id), [onDelete, message.id]);
const onRestoreClicked = useCallback(() => onRestore(message.id), [onRestore, message.id]);
<div
className={styles.avatar}
style={{ backgroundImage: `url("${getURL(message.from.photo, PRESETS.avatar)}")` }}
/>
if (message.deleted_at) {
return (
<div className={classNames(styles.message)}>
<Group className={styles.deleted} horizontal>
<Filler>Сообщение удалено</Filler>
<Button
size="mini"
onClick={onRestoreClicked}
color="link"
iconLeft="restore"
className={styles.restore}
>
Восстановить
</Button>
</Group>
<div className={styles.stamp}>{getPrettyDate(message.created_at)}</div>
</div>
);
<div
className={styles.avatar}
style={{ backgroundImage: `url("${getURL(message.from.photo, PRESETS.avatar)}")` }}
/>
</div>
);
}
return (
<div className={classNames(styles.message, { [styles.incoming]: incoming })}>
{isEditing ? (
<div className={styles.form}>
<MessageForm id={message.id} text={message.text} onCancel={onCancelEdit} />
</div>
) : (
<div className={styles.text}>
{!incoming && <CommentMenu onEdit={onEditClicked} onDelete={onDeleteClicked} />}
<Group dangerouslySetInnerHTML={{ __html: formatText(message.text) }} />
</div>
)}
<div
className={styles.avatar}
style={{ backgroundImage: `url("${getURL(message.from.photo, PRESETS.avatar)}")` }}
/>
<div className={styles.stamp}>{getPrettyDate(message.created_at)}</div>
</div>
);
};
export { Message };

View file

@ -57,7 +57,6 @@ $outgoing_color: $comment_bg;
background: 50% 50% no-repeat;
background-size: cover;
// display: none;
}
.text {
@ -65,8 +64,17 @@ $outgoing_color: $comment_bg;
background: $outgoing_color;
word-wrap: break-word;
word-break: break-word;
width: 90%;
width: 100%;
border-radius: $radius $radius 0 $radius;
position: relative;
box-sizing: border-box;
}
.form {
width: 100%;
border-radius: $radius $radius 0 $radius;
background: $outgoing_color;
box-sizing: border-box;
}
.stamp {
@ -79,3 +87,15 @@ $outgoing_color: $comment_bg;
padding: 2px $gap;
border-radius: $radius;
}
.restore {
color: $red;
fill: $red;
}
.deleted {
background: mix($red, $content_bg, 50%);
border-radius: $radius $radius $radius 0;
padding: $gap / 2;
z-index: 2;
}

View file

@ -1,38 +1,52 @@
import React, { FC, useState, useCallback, KeyboardEventHandler } from 'react';
import React, { FC, KeyboardEventHandler, useCallback, useMemo, useState } from 'react';
import styles from './styles.scss';
import { Textarea } from '~/components/input/Textarea';
import { Filler } from '~/components/containers/Filler';
import { Button } from '~/components/input/Button';
import { Group } from '~/components/containers/Group';
import { selectAuthProfile } from '~/redux/auth/selectors';
import { connect } from 'react-redux';
import { LoaderCircle } from '~/components/input/LoaderCircle';
import * as AUTH_ACTIONS from '~/redux/auth/actions';
import * as MESSAGES_ACTIONS from '~/redux/messages/actions';
import { ERROR_LITERAL } from '~/constants/errors';
import { selectMessages } from '~/redux/messages/selectors';
const mapStateToProps = state => ({
profile: selectAuthProfile(state),
messages: selectMessages(state),
});
const mapDispatchToProps = {
authSendMessage: AUTH_ACTIONS.authSendMessage,
messagesSendMessage: MESSAGES_ACTIONS.messagesSendMessage,
};
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
type IProps = ReturnType<typeof mapStateToProps> &
typeof mapDispatchToProps & {
id?: number;
text?: string;
onCancel?: () => void;
};
const MessageFormUnconnected: FC<IProps> = ({
profile: { is_sending_messages, is_loading_messages, messages_error },
authSendMessage,
messages: { is_sending_messages, is_loading_messages, messages_error },
messagesSendMessage,
id = 0,
text: initialText = '',
onCancel,
}) => {
const [text, setText] = useState('');
const isEditing = useMemo(() => id > 0, [id]);
const [text, setText] = useState(initialText);
const onSuccess = useCallback(() => {
setText('');
}, [setText]);
if (isEditing) {
onCancel();
}
}, [setText, isEditing, onCancel]);
const onSubmit = useCallback(() => {
authSendMessage({ text }, onSuccess);
}, [authSendMessage, text, onSuccess]);
messagesSendMessage({ text, id }, onSuccess);
}, [messagesSendMessage, text, id, onSuccess]);
const onKeyDown = useCallback<KeyboardEventHandler<HTMLTextAreaElement>>(
({ ctrlKey, key }) => {
@ -55,7 +69,7 @@ const MessageFormUnconnected: FC<IProps> = ({
value={text}
handler={setText}
minRows={1}
maxRows={4}
maxRows={isEditing ? 15 : 5}
seamless
onKeyDown={onKeyDown}
disabled={is_sending_messages}
@ -67,6 +81,12 @@ const MessageFormUnconnected: FC<IProps> = ({
{is_sending_messages && <LoaderCircle size={20} />}
{isEditing && (
<Button size="small" color="link" onClick={onCancel}>
Отмена
</Button>
)}
<Button
size="small"
color="gray"
@ -74,7 +94,7 @@ const MessageFormUnconnected: FC<IProps> = ({
disabled={is_sending_messages}
onClick={onSubmit}
>
Сказать
{isEditing ? 'Схоронить' : 'Сказать'}
</Button>
</Group>
</Group>
@ -82,9 +102,6 @@ const MessageFormUnconnected: FC<IProps> = ({
);
};
const MessageForm = connect(
mapStateToProps,
mapDispatchToProps
)(MessageFormUnconnected);
const MessageForm = connect(mapStateToProps, mapDispatchToProps)(MessageFormUnconnected);
export { MessageForm };

View file

@ -20,6 +20,7 @@
justify-content: center;
flex-direction: row;
padding: 0 $gap / 2 $gap / 2 $gap / 2;
border-radius: 0 0 $radius $radius;
:global(.loader-circle) {
svg {

View file

@ -111,7 +111,7 @@ const ProfileAccountsUnconnected: FC<IProps> = ({
</div>
</div>
<div className={styles.account__name}>{it.name}</div>
<div className={styles.account__name}>{it.name || it.id}</div>
<div className={styles.account__drop}>
<Icon icon="close" size={22} onClick={() => authDropSocial(it.provider, it.id)} />

View file

@ -50,6 +50,7 @@
background-size: cover;
border-radius: 2px;
position: relative;
background: $content_bg;
}
&__provider {