mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
Merge branch 'develop' into master
This commit is contained in:
commit
a8dc543c3c
29 changed files with 511 additions and 241 deletions
|
@ -57,6 +57,7 @@
|
|||
"@hot-loader/react-dom": "^16.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "^1.13.0",
|
||||
"@typescript-eslint/parser": "^1.13.0",
|
||||
"autosize": "^4.0.2",
|
||||
"axios": "^0.18.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"body-scroll-lock": "^2.6.4",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 6;
|
||||
white-space: nowrap;
|
||||
|
||||
animation: appear 0.25s forwards;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)} />
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
background-size: cover;
|
||||
border-radius: 2px;
|
||||
position: relative;
|
||||
background: $content_bg;
|
||||
}
|
||||
|
||||
&__provider {
|
||||
|
|
|
@ -11,6 +11,7 @@ export const API = {
|
|||
PROFILE: (username: string) => `/user/user/${username}/profile`,
|
||||
MESSAGES: (username: string) => `/user/user/${username}/messages`,
|
||||
MESSAGE_SEND: (username: string) => `/user/user/${username}/messages`,
|
||||
MESSAGE_DELETE: (username: string, id: number) => `/user/user/${username}/messages/${id}`,
|
||||
GET_UPDATES: '/user/updates',
|
||||
REQUEST_CODE: (code?: string) => `/user/restore/${code || ''}`,
|
||||
UPLOAD: (target, type) => `/upload/${target}/${type}`,
|
||||
|
@ -19,8 +20,6 @@ export const API = {
|
|||
DROP_SOCIAL: (provider, id) => `/oauth/${provider}/${id}`,
|
||||
ATTACH_SOCIAL: `/oauth/attach`,
|
||||
LOGIN_WITH_SOCIAL: `/oauth/login`,
|
||||
// TODO: REMOVE
|
||||
VKONTAKTE_LOGIN: `${process.env.API_HOST}/oauth/vkontakte/redirect/login`,
|
||||
},
|
||||
NODE: {
|
||||
SAVE: '/node/',
|
||||
|
|
|
@ -40,6 +40,7 @@ export const ERRORS = {
|
|||
CANT_SAVE_USER: 'CantSaveUser',
|
||||
CANT_DELETE_COMMENT: 'CantDeleteComment',
|
||||
CANT_RESTORE_COMMENT: 'CantRestoreComment',
|
||||
MESSAGE_NOT_FOUND: 'MessageNotFound',
|
||||
};
|
||||
|
||||
export const ERROR_LITERAL = {
|
||||
|
@ -85,4 +86,5 @@ export const ERROR_LITERAL = {
|
|||
[ERRORS.CANT_SAVE_USER]: 'Не удалось сохранить пользователя',
|
||||
[ERRORS.CANT_DELETE_COMMENT]: 'Не удалось удалить комментарий',
|
||||
[ERRORS.CANT_RESTORE_COMMENT]: 'Не удалось восстановить комментарий',
|
||||
[ERRORS.MESSAGE_NOT_FOUND]: 'Сообщение не найдено',
|
||||
};
|
||||
|
|
|
@ -1,63 +1,89 @@
|
|||
import React, { FC, useEffect } from 'react';
|
||||
import React, { FC, useCallback, useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectAuthProfile, selectAuth, selectAuthUser } from '~/redux/auth/selectors';
|
||||
import { selectAuthProfile, selectAuthUser } from '~/redux/auth/selectors';
|
||||
import styles from './styles.scss';
|
||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||
import * as AUTH_ACTIONS from '~/redux/messages/actions';
|
||||
import { Message } from '~/components/profile/Message';
|
||||
import { Group } from '~/components/containers/Group';
|
||||
import pick from 'ramda/es/pick';
|
||||
import { NodeNoComments } from '~/components/node/NodeNoComments';
|
||||
import { selectMessages } from '~/redux/messages/selectors';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
profile: selectAuthProfile(state),
|
||||
messages: selectMessages(state),
|
||||
user: pick(['id'], selectAuthUser(state)),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
authGetMessages: AUTH_ACTIONS.authGetMessages,
|
||||
messagesGetMessages: AUTH_ACTIONS.messagesGetMessages,
|
||||
messagesDeleteMessage: AUTH_ACTIONS.messagesDeleteMessage,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const ProfileMessagesUnconnected: FC<IProps> = ({ profile, user: { id }, authGetMessages }) => {
|
||||
const ProfileMessagesUnconnected: FC<IProps> = ({
|
||||
profile,
|
||||
messages,
|
||||
user: { id },
|
||||
messagesGetMessages,
|
||||
messagesDeleteMessage,
|
||||
}) => {
|
||||
const [editingMessageId, setEditingMessageId] = useState(0);
|
||||
|
||||
const onEditMessage = useCallback((id: number) => setEditingMessageId(id), [setEditingMessageId]);
|
||||
const onCancelEdit = useCallback(() => setEditingMessageId(0), [setEditingMessageId]);
|
||||
const onDeleteMessage = useCallback((id: number) => messagesDeleteMessage(id, true), [
|
||||
messagesDeleteMessage,
|
||||
]);
|
||||
const onRestoreMessage = useCallback((id: number) => messagesDeleteMessage(id, false), [
|
||||
messagesDeleteMessage,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile.is_loading || !profile.user || !profile.user.username) return;
|
||||
|
||||
authGetMessages(profile.user.username);
|
||||
messagesGetMessages(profile.user.username);
|
||||
}, [profile.user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (profile.is_loading || !profile.user || !profile.user.username || profile.messages_error)
|
||||
if (profile.is_loading || !profile.user || !profile.user.username || messages.messages_error)
|
||||
return;
|
||||
|
||||
const timer = setTimeout(() => authGetMessages(profile.user.username), 20000);
|
||||
const timer = setTimeout(() => messagesGetMessages(profile.user.username), 20000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [profile.user, profile.messages]);
|
||||
}, [profile.user, messages.messages]);
|
||||
|
||||
if (!profile.messages.length || profile.is_loading)
|
||||
return <NodeNoComments is_loading={profile.is_loading_messages || profile.is_loading} />;
|
||||
if (!messages.messages.length || profile.is_loading)
|
||||
return <NodeNoComments is_loading={messages.is_loading_messages || profile.is_loading} />;
|
||||
|
||||
return (
|
||||
<Group className={styles.messages}>
|
||||
{profile.messages
|
||||
{messages.messages
|
||||
.filter(message => !!message.text)
|
||||
.map((
|
||||
message // TODO: show files / memo
|
||||
) => (
|
||||
<Message message={message} incoming={id !== message.from.id} key={message.id} />
|
||||
<Message
|
||||
message={message}
|
||||
incoming={id !== message.from.id}
|
||||
key={message.id}
|
||||
onEdit={onEditMessage}
|
||||
onDelete={onDeleteMessage}
|
||||
isEditing={editingMessageId === message.id}
|
||||
onCancelEdit={onCancelEdit}
|
||||
onRestore={onRestoreMessage}
|
||||
/>
|
||||
))}
|
||||
|
||||
{!profile.is_loading_messages && profile.messages.length > 0 && (
|
||||
{!messages.is_loading_messages && messages.messages.length > 0 && (
|
||||
<div className={styles.placeholder}>Когда-нибудь здесь будут еще сообщения</div>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
const ProfileMessages = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ProfileMessagesUnconnected);
|
||||
const ProfileMessages = connect(mapStateToProps, mapDispatchToProps)(ProfileMessagesUnconnected);
|
||||
|
||||
export { ProfileMessages };
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AUTH_USER_ACTIONS } from '~/redux/auth/constants';
|
||||
import { IAuthState, ISocialProvider, IUser } from '~/redux/auth/types';
|
||||
import { IMessage, IOAuthEvent } from '../types';
|
||||
import { IOAuthEvent } from '../types';
|
||||
|
||||
export const userSendLoginRequest = ({
|
||||
username,
|
||||
|
@ -54,17 +54,6 @@ export const authSetProfile = (profile: Partial<IAuthState['profile']>) => ({
|
|||
profile,
|
||||
});
|
||||
|
||||
export const authGetMessages = (username: string) => ({
|
||||
type: AUTH_USER_ACTIONS.GET_MESSAGES,
|
||||
username,
|
||||
});
|
||||
|
||||
export const authSendMessage = (message: Partial<IMessage>, onSuccess) => ({
|
||||
type: AUTH_USER_ACTIONS.SEND_MESSAGE,
|
||||
message,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
export const authSetUpdates = (updates: Partial<IAuthState['updates']>) => ({
|
||||
type: AUTH_USER_ACTIONS.SET_UPDATES,
|
||||
updates,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { api, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api';
|
||||
import { API } from '~/constants/api';
|
||||
import { IMessage, INotification, IResultWithStatus } from '~/redux/types';
|
||||
import { INotification, IResultWithStatus } from '~/redux/types';
|
||||
import { userLoginTransform } from '~/redux/auth/transforms';
|
||||
import { ISocialAccount, IToken, IUser } from './types';
|
||||
import { ISocialAccount, IUser } from './types';
|
||||
|
||||
export const apiUserLogin = ({
|
||||
username,
|
||||
|
@ -32,25 +32,6 @@ export const apiAuthGetUserProfile = ({
|
|||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const apiAuthGetUserMessages = ({
|
||||
access,
|
||||
username,
|
||||
}): Promise<IResultWithStatus<{ messages: IMessage[] }>> =>
|
||||
api
|
||||
.get(API.USER.MESSAGES(username), configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const apiAuthSendMessage = ({
|
||||
access,
|
||||
username,
|
||||
message,
|
||||
}): Promise<IResultWithStatus<{ message: IMessage }>> =>
|
||||
api
|
||||
.post(API.USER.MESSAGE_SEND(username), { message }, configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const apiAuthGetUpdates = ({
|
||||
access,
|
||||
exclude_dialogs,
|
||||
|
|
|
@ -13,8 +13,6 @@ export const AUTH_USER_ACTIONS = {
|
|||
OPEN_PROFILE: 'OPEN_PROFILE',
|
||||
LOAD_PROFILE: 'LOAD_PROFILE',
|
||||
SET_PROFILE: 'SET_PROFILE',
|
||||
GET_MESSAGES: 'GET_MESSAGES',
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
|
||||
SET_UPDATES: 'SET_UPDATES',
|
||||
SET_LAST_SEEN_MESSAGES: 'SET_LAST_SEEN_MESSAGES',
|
||||
|
|
|
@ -26,11 +26,8 @@ const INITIAL_STATE: IAuthState = {
|
|||
profile: {
|
||||
tab: 'profile',
|
||||
is_loading: true,
|
||||
is_loading_messages: true,
|
||||
is_sending_messages: false,
|
||||
|
||||
user: null,
|
||||
messages: [],
|
||||
messages_error: null,
|
||||
patch_errors: {},
|
||||
|
||||
socials: {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { AUTH_USER_ACTIONS, EMPTY_USER, USER_ERRORS, USER_ROLES } from '~/redux/
|
|||
import {
|
||||
authAttachSocial,
|
||||
authDropSocial,
|
||||
authGetMessages,
|
||||
authGotOauthLoginEvent,
|
||||
authLoadProfile,
|
||||
authLoggedIn,
|
||||
|
@ -12,7 +11,6 @@ import {
|
|||
authPatchUser,
|
||||
authRequestRestoreCode,
|
||||
authRestorePassword,
|
||||
authSendMessage,
|
||||
authSendRegisterSocial,
|
||||
authSetLastSeenMessages,
|
||||
authSetProfile,
|
||||
|
@ -32,9 +30,7 @@ import {
|
|||
apiAttachSocial,
|
||||
apiAuthGetUpdates,
|
||||
apiAuthGetUser,
|
||||
apiAuthGetUserMessages,
|
||||
apiAuthGetUserProfile,
|
||||
apiAuthSendMessage,
|
||||
apiCheckRestoreCode,
|
||||
apiDropSocial,
|
||||
apiGetSocials,
|
||||
|
@ -54,13 +50,14 @@ import {
|
|||
selectAuthUser,
|
||||
selectToken,
|
||||
} from './selectors';
|
||||
import { IMessageNotification, IResultWithStatus, OAUTH_EVENT_TYPES, Unwrap } from '../types';
|
||||
import { IResultWithStatus, OAUTH_EVENT_TYPES, Unwrap } from '../types';
|
||||
import { IAuthState, IUser } from './types';
|
||||
import { REHYDRATE, RehydrateAction } from 'redux-persist';
|
||||
import { selectModal } from '~/redux/modal/selectors';
|
||||
import { IModalState } from '~/redux/modal';
|
||||
import { DIALOGS } from '~/redux/modal/constants';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { messagesSet } from '~/redux/messages/actions';
|
||||
|
||||
export function* reqWrapper(requestAction, props = {}): ReturnType<typeof requestAction> {
|
||||
const access = yield select(selectToken);
|
||||
|
@ -157,7 +154,8 @@ function* loadProfile({ username }: ReturnType<typeof authLoadProfile>) {
|
|||
return false;
|
||||
}
|
||||
|
||||
yield put(authSetProfile({ is_loading: false, user, messages: [] }));
|
||||
yield put(authSetProfile({ is_loading: false, user }));
|
||||
yield put(messagesSet({ messages: [] }));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -172,94 +170,6 @@ function* openProfile({ username, tab = 'profile' }: ReturnType<typeof authOpenP
|
|||
}
|
||||
}
|
||||
|
||||
function* getMessages({ username }: ReturnType<typeof authGetMessages>) {
|
||||
// yield put(modalShowDialog(DIALOGS.PROFILE));
|
||||
const { messages } = yield select(selectAuthProfile);
|
||||
|
||||
yield put(
|
||||
authSetProfile({
|
||||
is_loading_messages: true,
|
||||
messages:
|
||||
messages &&
|
||||
messages.length > 0 &&
|
||||
(messages[0].to.username === username || messages[0].from.username === username)
|
||||
? messages
|
||||
: [],
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
error,
|
||||
data,
|
||||
// data: { messages },
|
||||
} = yield call(reqWrapper, apiAuthGetUserMessages, { username });
|
||||
|
||||
if (error || !data.messages) {
|
||||
return yield put(
|
||||
authSetProfile({
|
||||
is_loading_messages: false,
|
||||
messages_error: ERRORS.EMPTY_RESPONSE,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
yield put(authSetProfile({ is_loading_messages: false, messages: data.messages }));
|
||||
|
||||
const { notifications } = yield select(selectAuthUpdates);
|
||||
|
||||
// clear viewed message from notifcation list
|
||||
const filtered = notifications.filter(
|
||||
notification =>
|
||||
notification.type !== 'message' ||
|
||||
(notification as IMessageNotification).content.from.username !== username
|
||||
);
|
||||
|
||||
if (filtered.length !== notifications.length) {
|
||||
yield put(authSetUpdates({ notifications: filtered }));
|
||||
}
|
||||
}
|
||||
|
||||
function* sendMessage({ message, onSuccess }: ReturnType<typeof authSendMessage>) {
|
||||
const {
|
||||
user: { username },
|
||||
} = yield select(selectAuthProfile);
|
||||
|
||||
if (!username) return;
|
||||
|
||||
yield put(authSetProfile({ is_sending_messages: true, messages_error: null }));
|
||||
|
||||
const { error, data } = yield call(reqWrapper, apiAuthSendMessage, {
|
||||
username,
|
||||
message,
|
||||
});
|
||||
|
||||
console.log({ error, data });
|
||||
|
||||
if (error || !data.message) {
|
||||
return yield put(
|
||||
authSetProfile({
|
||||
is_sending_messages: false,
|
||||
messages_error: error || ERRORS.EMPTY_RESPONSE,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { user, messages } = yield select(selectAuthProfile);
|
||||
|
||||
if (user.username !== username) {
|
||||
return yield put(authSetProfile({ is_sending_messages: false }));
|
||||
}
|
||||
|
||||
yield put(
|
||||
authSetProfile({
|
||||
is_sending_messages: false,
|
||||
messages: [data.message, ...messages],
|
||||
})
|
||||
);
|
||||
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
function* getUpdates() {
|
||||
const user: ReturnType<typeof selectAuthUser> = yield select(selectAuthUser);
|
||||
|
||||
|
@ -543,8 +453,6 @@ function* authSaga() {
|
|||
yield takeLatest(AUTH_USER_ACTIONS.GOT_AUTH_POST_MESSAGE, gotPostMessageSaga);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.OPEN_PROFILE, openProfile);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.LOAD_PROFILE, loadProfile);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.GET_MESSAGES, getMessages);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.SEND_MESSAGE, sendMessage);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES, setLastSeenMessages);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.PATCH_USER, patchUser);
|
||||
yield takeLatest(AUTH_USER_ACTIONS.REQUEST_RESTORE_CODE, requestRestoreCode);
|
||||
|
|
|
@ -5,6 +5,7 @@ export const selectUser = (state: IState) => state.auth.user;
|
|||
export const selectToken = (state: IState) => state.auth.token;
|
||||
export const selectAuthLogin = (state: IState) => state.auth.login;
|
||||
export const selectAuthProfile = (state: IState) => state.auth.profile;
|
||||
export const selectAuthProfileUsername = (state: IState) => state.auth.profile.user.username;
|
||||
export const selectAuthUser = (state: IState) => state.auth.user;
|
||||
export const selectAuthUpdates = (state: IState) => state.auth.updates;
|
||||
export const selectAuthRestore = (state: IState) => state.auth.restore;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IFile, IMessage, INotification } from '../types';
|
||||
import { IFile, INotification } from '../types';
|
||||
|
||||
export interface IToken {
|
||||
access: string;
|
||||
|
@ -52,12 +52,8 @@ export type IAuthState = Readonly<{
|
|||
profile: {
|
||||
tab: 'profile' | 'messages' | 'settings';
|
||||
is_loading: boolean;
|
||||
is_loading_messages: boolean;
|
||||
is_sending_messages: boolean;
|
||||
|
||||
user: IUser;
|
||||
messages: IMessage[];
|
||||
messages_error: string;
|
||||
patch_errors: Record<string, string>;
|
||||
|
||||
socials: {
|
||||
|
|
25
src/redux/messages/actions.ts
Normal file
25
src/redux/messages/actions.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { IMessage } from '~/redux/types';
|
||||
import { MESSAGES_ACTIONS } from '~/redux/messages/constants';
|
||||
import { IMessagesState } from '~/redux/messages';
|
||||
|
||||
export const messagesGetMessages = (username: string) => ({
|
||||
type: MESSAGES_ACTIONS.GET_MESSAGES,
|
||||
username,
|
||||
});
|
||||
|
||||
export const messagesSendMessage = (message: Partial<IMessage>, onSuccess) => ({
|
||||
type: MESSAGES_ACTIONS.SEND_MESSAGE,
|
||||
message,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
export const messagesDeleteMessage = (id: IMessage['id'], is_locked: boolean) => ({
|
||||
type: MESSAGES_ACTIONS.DELETE_MESSAGE,
|
||||
id,
|
||||
is_locked,
|
||||
});
|
||||
|
||||
export const messagesSet = (messages: Partial<IMessagesState>) => ({
|
||||
type: MESSAGES_ACTIONS.SET_MESSAGES,
|
||||
messages,
|
||||
});
|
41
src/redux/messages/api.ts
Normal file
41
src/redux/messages/api.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { IMessage, IResultWithStatus } from '~/redux/types';
|
||||
import { api, configWithToken, errorMiddleware, resultMiddleware } from '~/utils/api';
|
||||
import { API } from '~/constants/api';
|
||||
|
||||
export const apiMessagesGetUserMessages = ({
|
||||
access,
|
||||
username,
|
||||
}): Promise<IResultWithStatus<{ messages: IMessage[] }>> =>
|
||||
api
|
||||
.get(API.USER.MESSAGES(username), configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const apiMessagesSendMessage = ({
|
||||
access,
|
||||
username,
|
||||
message,
|
||||
}): Promise<IResultWithStatus<{ message: IMessage }>> =>
|
||||
api
|
||||
.post(API.USER.MESSAGE_SEND(username), { message }, configWithToken(access))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
||||
export const apiMessagesDeleteMessage = ({
|
||||
access,
|
||||
username,
|
||||
id,
|
||||
is_locked,
|
||||
}: {
|
||||
access: string;
|
||||
username: string;
|
||||
id: number;
|
||||
is_locked: boolean;
|
||||
}): Promise<IResultWithStatus<{ message: IMessage }>> =>
|
||||
api
|
||||
.delete(
|
||||
API.USER.MESSAGE_DELETE(username, id),
|
||||
configWithToken(access, { params: { is_locked } })
|
||||
)
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
8
src/redux/messages/constants.ts
Normal file
8
src/redux/messages/constants.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
const p = 'MESSAGES.';
|
||||
|
||||
export const MESSAGES_ACTIONS = {
|
||||
SET_MESSAGES: `${p}SET_MESSAGES`,
|
||||
GET_MESSAGES: `${p}GET_MESSAGES`,
|
||||
SEND_MESSAGE: `${p}SEND_MESSAGE`,
|
||||
DELETE_MESSAGE: `${p}DELETE_MESSAGE`,
|
||||
};
|
15
src/redux/messages/handlers.ts
Normal file
15
src/redux/messages/handlers.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { MESSAGES_ACTIONS } from '~/redux/messages/constants';
|
||||
import { IMessagesState } from '~/redux/messages';
|
||||
import { messagesSet } from '~/redux/messages/actions';
|
||||
|
||||
const setMessages = (
|
||||
state: IMessagesState,
|
||||
{ messages }: ReturnType<typeof messagesSet>
|
||||
): IMessagesState => ({
|
||||
...state,
|
||||
...messages,
|
||||
});
|
||||
|
||||
export const MESSAGE_HANDLERS = {
|
||||
[MESSAGES_ACTIONS.SET_MESSAGES]: setMessages,
|
||||
};
|
19
src/redux/messages/index.ts
Normal file
19
src/redux/messages/index.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { createReducer } from '~/utils/reducer';
|
||||
import { MESSAGE_HANDLERS } from '~/redux/messages/handlers';
|
||||
import { IMessage } from '~/redux/types';
|
||||
|
||||
export interface IMessagesState {
|
||||
is_loading_messages: boolean;
|
||||
is_sending_messages: boolean;
|
||||
messages: IMessage[];
|
||||
messages_error: string;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: IMessagesState = {
|
||||
is_loading_messages: true,
|
||||
is_sending_messages: false,
|
||||
messages_error: null,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
export default createReducer(INITIAL_STATE, MESSAGE_HANDLERS);
|
172
src/redux/messages/sagas.ts
Normal file
172
src/redux/messages/sagas.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
import { authSetUpdates } from '~/redux/auth/actions';
|
||||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import {
|
||||
selectAuthProfile,
|
||||
selectAuthProfileUsername,
|
||||
selectAuthUpdates,
|
||||
} from '~/redux/auth/selectors';
|
||||
import {
|
||||
apiMessagesDeleteMessage,
|
||||
apiMessagesGetUserMessages,
|
||||
apiMessagesSendMessage,
|
||||
} from '~/redux/messages/api';
|
||||
import { ERRORS } from '~/constants/errors';
|
||||
import { IMessageNotification, Unwrap } from '~/redux/types';
|
||||
import { reqWrapper } from '~/redux/auth/sagas';
|
||||
import {
|
||||
messagesDeleteMessage,
|
||||
messagesGetMessages,
|
||||
messagesSendMessage,
|
||||
messagesSet,
|
||||
} from '~/redux/messages/actions';
|
||||
import { MESSAGES_ACTIONS } from '~/redux/messages/constants';
|
||||
import { selectMessages } from '~/redux/messages/selectors';
|
||||
|
||||
function* getMessages({ username }: ReturnType<typeof messagesGetMessages>) {
|
||||
const { messages }: ReturnType<typeof selectMessages> = yield select(selectMessages);
|
||||
|
||||
yield put(
|
||||
messagesSet({
|
||||
is_loading_messages: true,
|
||||
messages:
|
||||
messages &&
|
||||
messages.length > 0 &&
|
||||
(messages[0].to.username === username || messages[0].from.username === username)
|
||||
? messages
|
||||
: [],
|
||||
})
|
||||
);
|
||||
|
||||
const { error, data } = yield call(reqWrapper, apiMessagesGetUserMessages, { username });
|
||||
|
||||
if (error || !data.messages) {
|
||||
return yield put(
|
||||
messagesSet({
|
||||
is_loading_messages: false,
|
||||
messages_error: ERRORS.EMPTY_RESPONSE,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
yield put(messagesSet({ is_loading_messages: false, messages: data.messages }));
|
||||
|
||||
const { notifications } = yield select(selectAuthUpdates);
|
||||
|
||||
// clear viewed message from notifcation list
|
||||
const filtered = notifications.filter(
|
||||
notification =>
|
||||
notification.type !== 'message' ||
|
||||
(notification as IMessageNotification).content.from.username !== username
|
||||
);
|
||||
|
||||
if (filtered.length !== notifications.length) {
|
||||
yield put(authSetUpdates({ notifications: filtered }));
|
||||
}
|
||||
}
|
||||
|
||||
function* sendMessage({ message, onSuccess }: ReturnType<typeof messagesSendMessage>) {
|
||||
const username: ReturnType<typeof selectAuthProfileUsername> = yield select(
|
||||
selectAuthProfileUsername
|
||||
);
|
||||
|
||||
if (!username) return;
|
||||
|
||||
yield put(messagesSet({ is_sending_messages: true, messages_error: null }));
|
||||
|
||||
const { error, data }: Unwrap<ReturnType<typeof apiMessagesSendMessage>> = yield call(
|
||||
reqWrapper,
|
||||
apiMessagesSendMessage,
|
||||
{
|
||||
username,
|
||||
message,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !data.message) {
|
||||
return yield put(
|
||||
messagesSet({
|
||||
is_sending_messages: false,
|
||||
messages_error: error || ERRORS.EMPTY_RESPONSE,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const { user }: ReturnType<typeof selectAuthProfile> = yield select(selectAuthProfile);
|
||||
|
||||
if (user.username !== username) {
|
||||
return yield put(messagesSet({ is_sending_messages: false }));
|
||||
}
|
||||
|
||||
const { messages }: ReturnType<typeof selectMessages> = yield select(selectMessages);
|
||||
|
||||
if (message.id > 0) {
|
||||
// modified
|
||||
yield put(
|
||||
messagesSet({
|
||||
is_sending_messages: false,
|
||||
messages: messages.map(item => (item.id === message.id ? data.message : item)),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// created
|
||||
yield put(
|
||||
messagesSet({
|
||||
is_sending_messages: false,
|
||||
messages: [data.message, ...messages],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onSuccess();
|
||||
}
|
||||
|
||||
function* deleteMessage({ id, is_locked }: ReturnType<typeof messagesDeleteMessage>) {
|
||||
const username: ReturnType<typeof selectAuthProfileUsername> = yield select(
|
||||
selectAuthProfileUsername
|
||||
);
|
||||
|
||||
if (!username) return;
|
||||
|
||||
yield put(messagesSet({ is_sending_messages: true, messages_error: null }));
|
||||
|
||||
const { error, data }: Unwrap<ReturnType<typeof apiMessagesDeleteMessage>> = yield call(
|
||||
reqWrapper,
|
||||
apiMessagesDeleteMessage,
|
||||
{
|
||||
username,
|
||||
id,
|
||||
is_locked,
|
||||
}
|
||||
);
|
||||
|
||||
if (error || !data.message) {
|
||||
return yield put(
|
||||
messagesSet({
|
||||
is_sending_messages: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const currentUsername: ReturnType<typeof selectAuthProfileUsername> = yield select(
|
||||
selectAuthProfileUsername
|
||||
);
|
||||
|
||||
if (currentUsername !== username) {
|
||||
return yield put(messagesSet({ is_sending_messages: false }));
|
||||
}
|
||||
|
||||
const { messages }: ReturnType<typeof selectMessages> = yield select(selectMessages);
|
||||
|
||||
yield put(
|
||||
messagesSet({
|
||||
is_sending_messages: false,
|
||||
messages: messages.map(item => (item.id === id ? data.message : item)),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default function*() {
|
||||
yield takeLatest(MESSAGES_ACTIONS.GET_MESSAGES, getMessages);
|
||||
yield takeLatest(MESSAGES_ACTIONS.SEND_MESSAGE, sendMessage);
|
||||
yield takeLatest(MESSAGES_ACTIONS.DELETE_MESSAGE, deleteMessage);
|
||||
}
|
3
src/redux/messages/selectors.ts
Normal file
3
src/redux/messages/selectors.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import { IState } from '~/redux/store';
|
||||
|
||||
export const selectMessages = (state: IState) => state.messages;
|
|
@ -1,9 +1,9 @@
|
|||
import { createStore, applyMiddleware, combineReducers, compose, Store } from 'redux';
|
||||
import { applyMiddleware, combineReducers, compose, createStore, Store } from 'redux';
|
||||
|
||||
import { persistStore, persistReducer } from 'redux-persist';
|
||||
import { persistReducer, persistStore } from 'redux-persist';
|
||||
import storage from 'redux-persist/lib/storage';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import { connectRouter, RouterState, routerMiddleware } from 'connected-react-router';
|
||||
import { connectRouter, routerMiddleware, RouterState } from 'connected-react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { PersistConfig, Persistor } from 'redux-persist/es/types';
|
||||
|
||||
|
@ -26,11 +26,14 @@ import playerSaga from '~/redux/player/sagas';
|
|||
import modal, { IModalState } from '~/redux/modal';
|
||||
import { modalSaga } from './modal/sagas';
|
||||
|
||||
import { gotAuthPostMessage, authOpenProfile } from './auth/actions';
|
||||
import { authOpenProfile, gotAuthPostMessage } from './auth/actions';
|
||||
|
||||
import boris, { IBorisState } from './boris/reducer';
|
||||
import borisSaga from './boris/sagas';
|
||||
|
||||
import messages, { IMessagesState } from './messages';
|
||||
import messagesSaga from './messages/sagas';
|
||||
|
||||
const authPersistConfig: PersistConfig = {
|
||||
key: 'auth',
|
||||
whitelist: ['token', 'user', 'updates'],
|
||||
|
@ -58,6 +61,7 @@ export interface IState {
|
|||
flow: IFlowState;
|
||||
player: IPlayerState;
|
||||
boris: IBorisState;
|
||||
messages: IMessagesState;
|
||||
}
|
||||
|
||||
export const sagaMiddleware = createSagaMiddleware();
|
||||
|
@ -78,6 +82,7 @@ export const store = createStore(
|
|||
uploads,
|
||||
flow: persistReducer(flowPersistConfig, flow),
|
||||
player: persistReducer(playerPersistConfig, player),
|
||||
messages,
|
||||
}),
|
||||
composeEnhancers(applyMiddleware(routerMiddleware(history), sagaMiddleware))
|
||||
);
|
||||
|
@ -93,6 +98,7 @@ export function configureStore(): {
|
|||
sagaMiddleware.run(playerSaga);
|
||||
sagaMiddleware.run(modalSaga);
|
||||
sagaMiddleware.run(borisSaga);
|
||||
sagaMiddleware.run(messagesSaga);
|
||||
|
||||
window.addEventListener('message', message => {
|
||||
if (message && message.data && message.data.type === 'oauth_login' && message.data.token)
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
padding: 0;
|
||||
|
||||
textarea {
|
||||
padding: $gap;
|
||||
padding: $gap / 2 $gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,20 +77,20 @@
|
|||
color: white;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
background: linear-gradient(270deg, $input_bg_color $gap, transparentize($input_bg_color, 1));
|
||||
position: absolute;
|
||||
width: $gap * 2;
|
||||
height: $input_height;
|
||||
top: 1px;
|
||||
right: 1px;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.25s;
|
||||
border-radius: 0 $input_radius $input_radius 0;
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
||||
//&::before {
|
||||
// content: ' ';
|
||||
// background: linear-gradient(270deg, $input_bg_color $gap, transparentize($input_bg_color, 1));
|
||||
// position: absolute;
|
||||
// width: $gap * 2;
|
||||
// height: $input_height;
|
||||
// top: 1px;
|
||||
// right: 1px;
|
||||
// transform: translateX(0);
|
||||
// transition: transform 0.25s;
|
||||
// border-radius: 0 $input_radius $input_radius 0;
|
||||
// pointer-events: none;
|
||||
// touch-action: none;
|
||||
//}
|
||||
}
|
||||
|
||||
&.required {
|
||||
|
|
|
@ -1368,6 +1368,11 @@ autoresponsive-react@^1.1.31:
|
|||
autoresponsive-core "^1.0.1"
|
||||
exenv "^1.2.0"
|
||||
|
||||
autosize@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/autosize/-/autosize-4.0.2.tgz#073cfd07c8bf45da4b9fd153437f5bafbba1e4c9"
|
||||
integrity sha512-jnSyH2d+qdfPGpWlcuhGiHmqBJ6g3X+8T+iRwFrHPLVcdoGJE/x6Qicm6aDHfTsbgZKxyV8UU/YB2p4cjKDRRA==
|
||||
|
||||
awesome-typescript-loader@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-5.2.1.tgz#a41daf7847515f4925cdbaa3075d61f289e913fc"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue