mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-25 04:46:40 +07:00
notification menu
This commit is contained in:
parent
6c1f8967e8
commit
9b0c3dd1fb
20 changed files with 371 additions and 39 deletions
|
@ -13,6 +13,8 @@ import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
|||
import { DIALOGS } from '~/redux/modal/constants';
|
||||
import { pick } from 'ramda';
|
||||
import { UserButton } from '../UserButton';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { Notifications } from '../Notifications';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: pick(['username', 'is_user', 'photo'])(selectUser(state)),
|
||||
|
@ -39,8 +41,7 @@ const HeaderUnconnected: FC<IProps> = memo(
|
|||
<Filler />
|
||||
|
||||
<div className={style.plugs}>
|
||||
<Link to="/boris">((( boris )))</Link>
|
||||
<Link to="/">flow</Link>
|
||||
<Notifications />
|
||||
</div>
|
||||
|
||||
{is_user && (
|
||||
|
|
|
@ -14,11 +14,24 @@
|
|||
.plugs {
|
||||
display: flex;
|
||||
user-select: none;
|
||||
font: $font_16_medium;
|
||||
text-transform: uppercase;
|
||||
align-items: center;
|
||||
|
||||
> a,
|
||||
> div {
|
||||
&::after {
|
||||
content: ' ';
|
||||
margin-left: $spc;
|
||||
background: white;
|
||||
width: 4px;
|
||||
height: $gap;
|
||||
display: block;
|
||||
opacity: 0.2;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
& > a.item,
|
||||
& > div.item {
|
||||
font: $font_16_medium;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
@ -33,17 +46,6 @@
|
|||
color: $red;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
margin-left: $spc;
|
||||
background: white;
|
||||
width: 4px;
|
||||
height: $gap;
|
||||
display: block;
|
||||
opacity: 0.2;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-left: $spc + $gap;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from 'react';
|
||||
import * as styles from './style.scss';
|
||||
import React from 'react';
|
||||
import styles from './style.scss';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
export const Logo = () => (
|
||||
<div className={styles.logo}>
|
||||
<Link className={styles.logo} to="/">
|
||||
VAULT
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
.logo {
|
||||
// font-family: Raleway;
|
||||
//font-size: $text_sign;
|
||||
//font-weight: 800;
|
||||
font: $font_24_bold;
|
||||
//font-family: Raleway;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
62
src/components/main/Notifications/index.tsx
Normal file
62
src/components/main/Notifications/index.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { FC, useMemo, useState, useCallback, useEffect } from 'react';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import styles from './styles.scss';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectAuthUpdates, selectAuthUser } from '~/redux/auth/selectors';
|
||||
import pick from 'ramda/es/pick';
|
||||
import classNames from 'classnames';
|
||||
import * as AUTH_ACTIONS from '~/redux/auth/actions';
|
||||
import { NotificationBubble } from '../../notifications/NotificationBubble';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: pick(['last_seen_messages'], selectAuthUser(state)),
|
||||
updates: selectAuthUpdates(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
authSetLastSeenMessages: AUTH_ACTIONS.authSetLastSeenMessages,
|
||||
};
|
||||
|
||||
type IProps = ReturnType<typeof mapStateToProps> & typeof mapDispatchToProps & {};
|
||||
|
||||
const NotificationsUnconnected: FC<IProps> = ({
|
||||
updates: { last, notifications },
|
||||
user: { last_seen_messages },
|
||||
authSetLastSeenMessages,
|
||||
}) => {
|
||||
const [visible, setVisible] = useState(true);
|
||||
const has_new = useMemo(
|
||||
() =>
|
||||
notifications.length &&
|
||||
last &&
|
||||
Date.parse(last) &&
|
||||
(!last_seen_messages ||
|
||||
(Date.parse(last_seen_messages) && Date.parse(last) > Date.parse(last_seen_messages))),
|
||||
[last, last_seen_messages, notifications]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !has_new) return;
|
||||
authSetLastSeenMessages(new Date().toISOString());
|
||||
}, [visible]);
|
||||
|
||||
const showList = useCallback(() => setVisible(true), [setVisible]);
|
||||
const hideList = useCallback(() => setVisible(false), [setVisible]);
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.wrap, { [styles.is_new]: has_new })}>
|
||||
<div className={styles.icon} onFocus={showList} onBlur={hideList} tabIndex={-1}>
|
||||
{has_new ? <Icon icon="bell_ring" size={24} /> : <Icon icon="bell" size={24} />}
|
||||
</div>
|
||||
|
||||
{visible && <NotificationBubble notifications={notifications} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Notifications = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(NotificationsUnconnected);
|
||||
|
||||
export { Notifications };
|
38
src/components/main/Notifications/styles.scss
Normal file
38
src/components/main/Notifications/styles.scss
Normal file
|
@ -0,0 +1,38 @@
|
|||
@keyframes ring {
|
||||
0% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: rotate(10deg);
|
||||
}
|
||||
|
||||
40% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
fill: white;
|
||||
position: relative;
|
||||
outline: none;
|
||||
|
||||
&.is_new {
|
||||
.icon {
|
||||
animation: ring 1s infinite alternate;
|
||||
|
||||
svg {
|
||||
fill: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
31
src/components/notifications/NotificationBubble/index.tsx
Normal file
31
src/components/notifications/NotificationBubble/index.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React, { FC, createElement } from 'react';
|
||||
import { INotification, NOTIFICATION_TYPES } from '~/redux/types';
|
||||
import styles from './styles.scss';
|
||||
import { NotificationMessage } from '../NotificationMessage';
|
||||
|
||||
interface IProps {
|
||||
notifications: INotification[];
|
||||
}
|
||||
|
||||
const NOTIFICATION_RENDERERS = {
|
||||
[NOTIFICATION_TYPES.message]: NotificationMessage,
|
||||
};
|
||||
|
||||
const NotificationBubble: FC<IProps> = ({ notifications }) => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<div className={styles.list}>
|
||||
{notifications
|
||||
.filter(notification => notification.type && NOTIFICATION_RENDERERS[notification.type])
|
||||
.map(notification =>
|
||||
createElement(NOTIFICATION_RENDERERS[notification.type], {
|
||||
notification,
|
||||
key: notification.content.id,
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotificationBubble };
|
76
src/components/notifications/NotificationBubble/styles.scss
Normal file
76
src/components/notifications/NotificationBubble/styles.scss
Normal file
|
@ -0,0 +1,76 @@
|
|||
@keyframes appear {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
.wrap {
|
||||
position: absolute;
|
||||
position: absolute;
|
||||
background: $content_bg;
|
||||
top: 50px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
border-radius: $radius;
|
||||
animation: appear 0.5s forwards;
|
||||
|
||||
&::before {
|
||||
content: ' ';
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 0 0 16px 16px;
|
||||
border-color: transparent transparent $content_bg transparent;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: -16px;
|
||||
transform: translate(-20px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
width: 300px;
|
||||
max-width: 100vw;
|
||||
min-width: 0;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
padding: $gap;
|
||||
min-width: 0;
|
||||
|
||||
svg {
|
||||
fill: white;
|
||||
margin-right: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.item_head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.item_title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
font: $font_14_semibold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
// text-transform: none;
|
||||
}
|
||||
|
||||
.item_text {
|
||||
font: $font_14_regular;
|
||||
max-height: 2.4em;
|
||||
padding-left: 30px;
|
||||
overflow: hidden;
|
||||
}
|
26
src/components/notifications/NotificationMessage/index.tsx
Normal file
26
src/components/notifications/NotificationMessage/index.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React, { FC } from 'react';
|
||||
import styles from '~/components/notifications/NotificationBubble/styles.scss';
|
||||
import { Icon } from '~/components/input/Icon';
|
||||
import { IMessageNotification } from '~/redux/types';
|
||||
|
||||
interface IProps {
|
||||
notification: IMessageNotification;
|
||||
}
|
||||
|
||||
const NotificationMessage: FC<IProps> = ({
|
||||
notification: {
|
||||
content: { text, from },
|
||||
},
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.item}>
|
||||
<div className={styles.item_head}>
|
||||
<Icon icon="message" />
|
||||
<div className={styles.item_title}>Сообщение от ~{from.username}:</div>
|
||||
</div>
|
||||
<div className={styles.item_text}>{text}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { NotificationMessage };
|
|
@ -54,3 +54,15 @@ export const authSendMessage = (message: Partial<IMessage>, onSuccess) => ({
|
|||
message,
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
export const authSetUpdates = (updates: Partial<IAuthState['updates']>) => ({
|
||||
type: AUTH_USER_ACTIONS.SET_UPDATES,
|
||||
updates,
|
||||
});
|
||||
|
||||
export const authSetLastSeenMessages = (
|
||||
last_seen_messages: IAuthState['user']['last_seen_messages']
|
||||
) => ({
|
||||
type: AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES,
|
||||
last_seen_messages,
|
||||
});
|
||||
|
|
|
@ -54,8 +54,9 @@ export const apiAuthSendMessage = ({
|
|||
export const apiAuthGetUpdates = ({
|
||||
access,
|
||||
exclude_dialogs,
|
||||
last,
|
||||
}): Promise<IResultWithStatus<{ message: IMessage }>> =>
|
||||
api
|
||||
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs } }))
|
||||
.get(API.USER.GET_UPDATES, configWithToken(access, { params: { exclude_dialogs, last } }))
|
||||
.then(resultMiddleware)
|
||||
.catch(errorMiddleware);
|
||||
|
|
|
@ -13,6 +13,9 @@ export const AUTH_USER_ACTIONS = {
|
|||
SET_PROFILE: 'SET_PROFILE',
|
||||
GET_MESSAGES: 'GET_MESSAGES',
|
||||
SEND_MESSAGE: 'SEND_MESSAGE',
|
||||
|
||||
SET_UPDATES: 'SET_UPDATES',
|
||||
SET_LAST_SEEN_MESSAGES: 'SET_LAST_SEEN_MESSAGES',
|
||||
};
|
||||
|
||||
export const USER_ERRORS = {
|
||||
|
@ -46,9 +49,11 @@ export const EMPTY_USER: IUser = {
|
|||
cover: null,
|
||||
is_activated: false,
|
||||
is_user: false,
|
||||
last_seen: null,
|
||||
fullname: null,
|
||||
description: null,
|
||||
|
||||
last_seen: null,
|
||||
last_seen_messages: null,
|
||||
};
|
||||
|
||||
export interface IApiUser {
|
||||
|
|
|
@ -37,9 +37,31 @@ const setProfile: ActionHandler<typeof ActionCreators.authSetProfile> = (state,
|
|||
...profile,
|
||||
},
|
||||
});
|
||||
|
||||
const setUpdates: ActionHandler<typeof ActionCreators.authSetUpdates> = (state, { updates }) => ({
|
||||
...state,
|
||||
updates: {
|
||||
...state.updates,
|
||||
...updates,
|
||||
},
|
||||
});
|
||||
|
||||
const setLastSeenMessages: ActionHandler<typeof ActionCreators.authSetLastSeenMessages> = (
|
||||
state,
|
||||
{ last_seen_messages }
|
||||
) => ({
|
||||
...state,
|
||||
user: {
|
||||
...state.user,
|
||||
last_seen_messages,
|
||||
},
|
||||
});
|
||||
|
||||
export const AUTH_USER_HANDLERS = {
|
||||
[AUTH_USER_ACTIONS.SET_LOGIN_ERROR]: setLoginError,
|
||||
[AUTH_USER_ACTIONS.SET_USER]: setUser,
|
||||
[AUTH_USER_ACTIONS.SET_TOKEN]: setToken,
|
||||
[AUTH_USER_ACTIONS.SET_PROFILE]: setProfile,
|
||||
[AUTH_USER_ACTIONS.SET_UPDATES]: setUpdates,
|
||||
[AUTH_USER_ACTIONS.SET_LAST_SEEN_MESSAGES]: setLastSeenMessages,
|
||||
};
|
||||
|
|
|
@ -12,7 +12,8 @@ const INITIAL_STATE: IAuthState = {
|
|||
user: { ...EMPTY_USER },
|
||||
|
||||
updates: {
|
||||
messages: [],
|
||||
last: null,
|
||||
notifications: [],
|
||||
},
|
||||
|
||||
login: {
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
authSetProfile,
|
||||
authGetMessages,
|
||||
authSendMessage,
|
||||
authSetUpdates,
|
||||
} from '~/redux/auth/actions';
|
||||
import {
|
||||
apiUserLogin,
|
||||
|
@ -20,8 +21,8 @@ import {
|
|||
apiAuthGetUpdates,
|
||||
} from '~/redux/auth/api';
|
||||
import { modalSetShown, modalShowDialog } from '~/redux/modal/actions';
|
||||
import { selectToken, selectAuthProfile, selectAuthUser } from './selectors';
|
||||
import { IResultWithStatus } from '../types';
|
||||
import { selectToken, selectAuthProfile, selectAuthUser, selectAuthUpdates } from './selectors';
|
||||
import { IResultWithStatus, INotification } from '../types';
|
||||
import { IUser, IAuthState } from './types';
|
||||
import { REHYDRATE, RehydrateAction } from 'redux-persist';
|
||||
import { selectModal } from '../modal/selectors';
|
||||
|
@ -197,19 +198,32 @@ function* getUpdates() {
|
|||
|
||||
const modal: IModalState = yield select(selectModal);
|
||||
const profile: IAuthState['profile'] = yield select(selectAuthProfile);
|
||||
|
||||
const { last }: IAuthState['updates'] = yield select(selectAuthUpdates);
|
||||
const exclude_dialogs =
|
||||
modal.is_shown && modal.dialog === DIALOGS.PROFILE && profile.user.id ? profile.user.id : null;
|
||||
|
||||
const { error, data } = yield call(reqWrapper, apiAuthGetUpdates, { exclude_dialogs });
|
||||
const { error, data }: IResultWithStatus<{ notifications: INotification[] }> = yield call(
|
||||
reqWrapper,
|
||||
apiAuthGetUpdates,
|
||||
{ exclude_dialogs, last }
|
||||
);
|
||||
|
||||
if (error || !data) return;
|
||||
if (error || !data || !data.notifications || !data.notifications.length) return;
|
||||
|
||||
const { notifications } = data;
|
||||
|
||||
yield put(
|
||||
authSetUpdates({
|
||||
last: notifications[0].created_at,
|
||||
notifications,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function* startPollingSaga() {
|
||||
while (true) {
|
||||
yield call(getUpdates);
|
||||
yield delay(30000);
|
||||
yield delay(60000);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@ export const selectToken = (state: IState): IState['auth']['token'] => state.aut
|
|||
export const selectAuthLogin = (state: IState): IState['auth']['login'] => state.auth.login;
|
||||
export const selectAuthProfile = (state: IState): IState['auth']['profile'] => state.auth.profile;
|
||||
export const selectAuthUser = (state: IState): IState['auth']['user'] => state.auth.user;
|
||||
export const selectAuthUpdates = (state: IState): IState['auth']['updates'] => state.auth.updates;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { IFile, IMessage } from '../types';
|
||||
import { IFile, IMessage, INotification } from '../types';
|
||||
|
||||
export interface IToken {
|
||||
access: string;
|
||||
|
@ -13,10 +13,12 @@ export interface IUser {
|
|||
photo: IFile;
|
||||
cover: IFile;
|
||||
name: string;
|
||||
last_seen: string;
|
||||
fullname: string;
|
||||
description: string;
|
||||
|
||||
last_seen: string;
|
||||
last_seen_messages: string;
|
||||
|
||||
is_activated: boolean;
|
||||
is_user: boolean;
|
||||
}
|
||||
|
@ -26,7 +28,8 @@ export type IAuthState = Readonly<{
|
|||
token: string;
|
||||
|
||||
updates: {
|
||||
messages: IMessage[];
|
||||
last: string;
|
||||
notifications: INotification[];
|
||||
};
|
||||
|
||||
login: {
|
||||
|
|
|
@ -29,7 +29,7 @@ import { gotAuthPostMessage, authOpenProfile } from './auth/actions';
|
|||
|
||||
const authPersistConfig: PersistConfig = {
|
||||
key: 'auth',
|
||||
whitelist: ['token', 'user'],
|
||||
whitelist: ['token', 'user', 'updates'],
|
||||
storage,
|
||||
};
|
||||
|
||||
|
@ -50,8 +50,6 @@ history.listen(({ pathname }) => {
|
|||
console.log({ pathname });
|
||||
|
||||
if (pathname.match(/~([\wа-яА-Я]+)/)) {
|
||||
console.log('hi!', pathname.match(/~([\wа-яА-Я]+)/));
|
||||
|
||||
const [, username] = pathname.match(/~([\wа-яА-Я]+)/);
|
||||
window.postMessage({ type: 'username', username }, '*');
|
||||
}
|
||||
|
|
|
@ -161,3 +161,28 @@ export type IUploadProgressHandler = (progress: ProgressEvent) => void;
|
|||
export type IError = ValueOf<typeof ERRORS>;
|
||||
export type IValidationErrors = Record<string, IError>;
|
||||
export type InputHandler<T = string> = (val: T) => void;
|
||||
|
||||
export const NOTIFICATION_TYPES = {
|
||||
message: 'message',
|
||||
comment: 'comment',
|
||||
node: 'node',
|
||||
};
|
||||
|
||||
export type IMessageNotification = {
|
||||
type: typeof NOTIFICATION_TYPES['message'];
|
||||
content: Partial<IMessage>;
|
||||
};
|
||||
|
||||
export type ICommentNotification = {
|
||||
type: typeof NOTIFICATION_TYPES['comment'];
|
||||
content: Partial<IComment>;
|
||||
};
|
||||
|
||||
export type INodeNotification = {
|
||||
type: typeof NOTIFICATION_TYPES['node'];
|
||||
content: Partial<INode>;
|
||||
};
|
||||
|
||||
export type INotification = (IMessageNotification | ICommentNotification) & {
|
||||
created_at: string;
|
||||
};
|
||||
|
|
|
@ -171,6 +171,21 @@ const Sprites: FC<{}> = () => (
|
|||
<path d="M545.451,400.298c-0.664-1.431-1.283-2.618-1.858-3.569c-9.514-17.135-27.695-38.167-54.532-63.102l-0.567-0.571 l-0.284-0.28l-0.287-0.287h-0.288c-12.18-11.611-19.893-19.418-23.123-23.415c-5.91-7.614-7.234-15.321-4.004-23.13 c2.282-5.9,10.854-18.36,25.696-37.397c7.807-10.089,13.99-18.175,18.556-24.267c32.931-43.78,47.208-71.756,42.828-83.939 l-1.701-2.847c-1.143-1.714-4.093-3.282-8.846-4.712c-4.764-1.427-10.853-1.663-18.278-0.712l-82.224,0.568 c-1.332-0.472-3.234-0.428-5.712,0.144c-2.475,0.572-3.713,0.859-3.713,0.859l-1.431,0.715l-1.136,0.859 c-0.952,0.568-1.999,1.567-3.142,2.995c-1.137,1.423-2.088,3.093-2.848,4.996c-8.952,23.031-19.13,44.444-30.553,64.238 c-7.043,11.803-13.511,22.032-19.418,30.693c-5.899,8.658-10.848,15.037-14.842,19.126c-4,4.093-7.61,7.372-10.852,9.849 c-3.237,2.478-5.708,3.525-7.419,3.142c-1.715-0.383-3.33-0.763-4.859-1.143c-2.663-1.714-4.805-4.045-6.42-6.995 c-1.622-2.95-2.714-6.663-3.285-11.136c-0.568-4.476-0.904-8.326-1-11.563c-0.089-3.233-0.048-7.806,0.145-13.706 c0.198-5.903,0.287-9.897,0.287-11.991c0-7.234,0.141-15.085,0.424-23.555c0.288-8.47,0.521-15.181,0.716-20.125 c0.194-4.949,0.284-10.185,0.284-15.705s-0.336-9.849-1-12.991c-0.656-3.138-1.663-6.184-2.99-9.137 c-1.335-2.95-3.289-5.232-5.853-6.852c-2.569-1.618-5.763-2.902-9.564-3.856c-10.089-2.283-22.936-3.518-38.547-3.71 c-35.401-0.38-58.148,1.906-68.236,6.855c-3.997,2.091-7.614,4.948-10.848,8.562c-3.427,4.189-3.905,6.475-1.431,6.851 c11.422,1.711,19.508,5.804,24.267,12.275l1.715,3.429c1.334,2.474,2.666,6.854,3.999,13.134c1.331,6.28,2.19,13.227,2.568,20.837 c0.95,13.897,0.95,25.793,0,35.689c-0.953,9.9-1.853,17.607-2.712,23.127c-0.859,5.52-2.143,9.993-3.855,13.418 c-1.715,3.426-2.856,5.52-3.428,6.28c-0.571,0.76-1.047,1.239-1.425,1.427c-2.474,0.948-5.047,1.431-7.71,1.431 c-2.667,0-5.901-1.334-9.707-4c-3.805-2.666-7.754-6.328-11.847-10.992c-4.093-4.665-8.709-11.184-13.85-19.558 c-5.137-8.374-10.467-18.271-15.987-29.691l-4.567-8.282c-2.855-5.328-6.755-13.086-11.704-23.267 c-4.952-10.185-9.329-20.037-13.134-29.554c-1.521-3.997-3.806-7.04-6.851-9.134l-1.429-0.859c-0.95-0.76-2.475-1.567-4.567-2.427 c-2.095-0.859-4.281-1.475-6.567-1.854l-78.229,0.568c-7.994,0-13.418,1.811-16.274,5.428l-1.143,1.711 C0.288,140.146,0,141.668,0,143.763c0,2.094,0.571,4.664,1.714,7.707c11.42,26.84,23.839,52.725,37.257,77.659 c13.418,24.934,25.078,45.019,34.973,60.237c9.897,15.229,19.985,29.602,30.264,43.112c10.279,13.515,17.083,22.176,20.412,25.981 c3.333,3.812,5.951,6.662,7.854,8.565l7.139,6.851c4.568,4.569,11.276,10.041,20.127,16.416 c8.853,6.379,18.654,12.659,29.408,18.85c10.756,6.181,23.269,11.225,37.546,15.126c14.275,3.905,28.169,5.472,41.684,4.716h32.834 c6.659-0.575,11.704-2.669,15.133-6.283l1.136-1.431c0.764-1.136,1.479-2.901,2.139-5.276c0.668-2.379,1-5,1-7.851 c-0.195-8.183,0.428-15.558,1.852-22.124c1.423-6.564,3.045-11.513,4.859-14.846c1.813-3.33,3.859-6.14,6.136-8.418 c2.282-2.283,3.908-3.666,4.862-4.142c0.948-0.479,1.705-0.804,2.276-0.999c4.568-1.522,9.944-0.048,16.136,4.429 c6.187,4.473,11.99,9.996,17.418,16.56c5.425,6.57,11.943,13.941,19.555,22.124c7.617,8.186,14.277,14.271,19.985,18.274 l5.708,3.426c3.812,2.286,8.761,4.38,14.853,6.283c6.081,1.902,11.409,2.378,15.984,1.427l73.087-1.14 c7.229,0,12.854-1.197,16.844-3.572c3.998-2.379,6.373-5,7.139-7.851c0.764-2.854,0.805-6.092,0.145-9.712 C546.782,404.25,546.115,401.725,545.451,400.298z" />
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<g id="bell" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z" />
|
||||
</g>
|
||||
|
||||
<g id="bell_ring" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6zM7.58 4.08L6.15 2.65C3.75 4.48 2.17 7.3 2.03 10.5h2c.15-2.65 1.51-4.97 3.55-6.42zm12.39 6.42h2c-.15-3.2-1.73-6.02-4.12-7.85l-1.42 1.43c2.02 1.45 3.39 3.77 3.54 6.42z" />
|
||||
</g>
|
||||
|
||||
<g id="message" stroke="none">
|
||||
<path fill="none" d="M0 0h24v24H0V0z" />
|
||||
<path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 14H4V8l8 5 8-5v10zm-8-7L4 6h16l-8 5z" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue