import React, { useState } from 'react'; import styled, { css } from 'styled-components'; import TimeAgo from 'react-timeago'; import { Link } from 'react-router-dom'; import { mixin } from 'shared/utils/styles'; import { useNotificationsQuery, NotificationFilter, ActionType, useNotificationAddedSubscription, useNotificationToggleReadMutation, } from 'shared/generated/graphql'; import dayjs from 'dayjs'; import { Popup, usePopup } from 'shared/components/PopupMenu'; import { CheckCircleOutline, Circle, CircleSolid, UserCircle } from 'shared/icons'; import produce from 'immer'; const ItemWrapper = styled.div` cursor: pointer; border-bottom: 1px solid #414561; padding-left: 1rem; padding-right: 1rem; padding-top: 1rem; padding-bottom: 1rem; justify-content: space-between; display: flex; &:hover { background: #10163a; } `; const ItemWrapperContent = styled.div` display: flex; align-items: flex-start; `; const ItemIconContainer = styled.span` position: relative; display: inline-flex; align-items: center; `; const ItemTextContainer = styled.div` margin-left: 0.5rem; margin-right: 0.5rem; `; const ItemTextTitle = styled.span` font-weight: 500; display: block; color: ${(props) => props.theme.colors.primary}; font-size: 14px; `; const ItemTextDesc = styled.span` font-size: 12px; `; const ItemTimeAgo = styled.span` margin-top: 0.25rem; white-space: nowrap; font-size: 11px; `; type NotificationItemProps = { title: string; description: string; createdAt: string; }; export const NotificationItem: React.FC = ({ title, description, createdAt }) => { return ( {title} {description} ); }; const NotificationHeader = styled.div` padding: 20px 28px; text-align: center; border-top-left-radius: 6px; border-top-right-radius: 6px; background: ${(props) => props.theme.colors.primary}; `; const NotificationHeaderTitle = styled.span` font-size: 14px; color: ${(props) => props.theme.colors.text.secondary}; `; const Notifications = styled.div` border-right: 1px solid rgba(0, 0, 0, 0.1); border-left: 1px solid rgba(0, 0, 0, 0.1); border-top: 1px solid rgba(0, 0, 0, 0.1); border-color: #414561; height: 448px; overflow-y: scroll; user-select: none; `; const NotificationFooter = styled.div` cursor: pointer; padding: 0.5rem; text-align: center; color: ${(props) => props.theme.colors.primary}; &:hover { background: ${(props) => props.theme.colors.bg.primary}; } border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; border-right: 1px solid rgba(0, 0, 0, 0.1); border-left: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1); border-color: #414561; `; const NotificationTabs = styled.div` align-items: flex-end; align-self: stretch; display: flex; flex: 1 0 auto; justify-content: flex-start; max-width: 100%; padding-top: 4px; border-right: 1px solid rgba(0, 0, 0, 0.1); border-left: 1px solid rgba(0, 0, 0, 0.1); border-color: #414561; `; const NotificationTab = styled.div<{ active: boolean }>` font-size: 80%; color: ${(props) => props.theme.colors.text.primary}; font-size: 15px; cursor: pointer; display: flex; user-select: none; justify-content: center; line-height: normal; min-width: 1px; transition-duration: 0.2s; transition-property: box-shadow, color; white-space: nowrap; flex: 0 1 auto; padding: 12px 16px; &:first-child { margin-left: 12px; } &:hover { box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary}; color: ${(props) => props.theme.colors.text.secondary}; } &:not(:last-child) { margin-right: 12px; } ${(props) => props.active && css` box-shadow: inset 0 -2px ${props.theme.colors.secondary}; color: ${props.theme.colors.secondary}; &:hover { box-shadow: inset 0 -2px ${props.theme.colors.secondary}; color: ${props.theme.colors.secondary}; } `} `; const NotificationLink = styled(Link)` display: flex; align-items: center; text-decoration: none; padding: 16px 8px; width: 100%; `; const NotificationControls = styled.div` width: 0; display: flex; flex-direction: column; align-items: center; justify-content: space-around; visibility: hidden; padding: 4px; `; const NotificationButtons = styled.div` display: flex; align-self: flex-end; align-items: center; margin-top: auto; margin-bottom: 6px; `; const NotificationButton = styled.div` padding: 4px 15px; cursor: pointer; &:hover svg { fill: rgb(216, 93, 216); stroke: rgb(216, 93, 216); } `; const NotificationWrapper = styled.li` min-height: 112px; display: flex; font-size: 14px; transition: background-color 0.1s ease-in-out; margin: 2px 8px; border-radius: 8px; justify-content: space-between; position: relative; &:hover { background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)}; } &:hover ${NotificationLink} { color: #fff; } &:hover ${NotificationControls} { visibility: visible; } `; const NotificationContentFooter = styled.div` margin-top: 8px; display: flex; align-items: center; color: ${(props) => props.theme.colors.text.primary}; `; const NotificationCausedBy = styled.div` height: 60px; width: 60px; min-height: 60px; min-width: 60px; `; const NotificationCausedByInitials = styled.div` position: relative; display: flex; align-items: center; text: #fff; font-size: 18px; justify-content: center; border-radius: 50%; flex-shrink: 0; height: 100%; width: 100%; border: none; background: #7367f0; `; const NotificationCausedByImage = styled.img` position: relative; display: flex; border-radius: 50%; flex-shrink: 0; height: 100%; width: 100%; border: none; background: #7367f0; `; const NotificationContent = styled.div` display: flex; overflow: hidden; flex-direction: column; margin-left: 16px; `; const NotificationContentHeader = styled.div` font-weight: bold; font-size: 14px; color: #fff; svg { margin-left: 8px; fill: rgb(216, 93, 216); stroke: rgb(216, 93, 216); } `; const NotificationBody = styled.div` margin-top: 8px; display: flex; align-items: center; color: #fff; svg { fill: rgb(216, 93, 216); stroke: rgb(216, 93, 216); } `; const NotificationPrefix = styled.span` color: rgb(216, 93, 216); margin: 0 4px; `; const NotificationSeparator = styled.span` margin: 0 6px; `; type NotificationProps = { causedBy?: { fullname: string; username: string; id: string } | null; createdAt: string; read: boolean; data: Array<{ key: string; value: string }>; actionType: ActionType; onToggleRead: () => void; }; const Notification: React.FC = ({ causedBy, createdAt, data, actionType, read, onToggleRead }) => { const prefix: any = []; const { hidePopup } = usePopup(); const dataMap = new Map(); data.forEach((d) => dataMap.set(d.key, d.value)); let link = '#'; switch (actionType) { case ActionType.TaskAssigned: prefix.push(); prefix.push(Assigned ); prefix.push(you to the task "{dataMap.get('TaskName')}"); link = `/projects/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`; break; default: throw new Error('unknown action type'); } return ( {causedBy ? causedBy.fullname .split(' ') .map((n) => n[0]) .join('.') : 'RU'} {causedBy ? causedBy.fullname : 'Removed user'} {!read && } {prefix} {dayjs.duration(dayjs(createdAt).diff(dayjs())).humanize(true)} {dataMap.get('ProjectName')} onToggleRead()}> {read ? : } ); }; const PopupContent = styled.div` display: flex; flex-direction: column; border-right: 1px solid rgba(0, 0, 0, 0.1); border-left: 1px solid rgba(0, 0, 0, 0.1); border-bottom: 1px solid rgba(0, 0, 0, 0.1); padding-bottom: 10px; border-color: #414561; `; const tabs = [ { label: 'All', key: NotificationFilter.All }, { label: 'Unread', key: NotificationFilter.Unread }, { label: 'I was mentioned', key: NotificationFilter.Mentioned }, { label: 'Assigned to me', key: NotificationFilter.Assigned }, ]; type NotificationEntry = { id: string; read: boolean; readAt?: string | undefined | null; notification: { id: string; data: Array<{ key: string; value: string }>; actionType: ActionType; causedBy?: { id: string; username: string; fullname: string } | undefined | null; createdAt: string; }; }; const NotificationPopup: React.FC = ({ children }) => { const [filter, setFilter] = useState(NotificationFilter.Unread); const [data, setData] = useState<{ nodes: Array; hasNextPage: boolean; cursor: string }>({ nodes: [], hasNextPage: false, cursor: '', }); const [toggleRead] = useNotificationToggleReadMutation({ onCompleted: (data) => { setData((prev) => { return produce(prev, (draft) => { const idx = draft.nodes.findIndex((n) => n.id === data.notificationToggleRead.id); if (idx !== -1) { draft.nodes[idx].read = data.notificationToggleRead.read; draft.nodes[idx].readAt = data.notificationToggleRead.readAt; } }); }); }, }); const { data: nData, fetchMore } = useNotificationsQuery({ variables: { limit: 5, filter }, onCompleted: (d) => { setData((prev) => ({ hasNextPage: d.notified.pageInfo.hasNextPage, cursor: d.notified.pageInfo.endCursor, nodes: [...prev.nodes, ...d.notified.notified], })); }, }); const { data: sData, loading } = useNotificationAddedSubscription({ onSubscriptionData: (d) => { setData((n) => { if (d.subscriptionData.data) { return { ...n, nodes: [d.subscriptionData.data.notificationAdded, ...n.nodes], }; } return n; }); }, }); return ( Notifications {tabs.map((tab) => ( { if (filter !== tab.key) { setData({ cursor: '', hasNextPage: false, nodes: [] }); setFilter(tab.key); } }} active={tab.key === filter} > {tab.label} ))} { if (currentTarget.scrollTop + currentTarget.clientHeight >= currentTarget.scrollHeight) { if (data.hasNextPage) { console.log(`fetching more = ${data.cursor} - ${data.hasNextPage}`); fetchMore({ variables: { limit: 5, filter, cursor: data.cursor, }, updateQuery: (prev, { fetchMoreResult }) => { if (!fetchMoreResult) return prev; setData((d) => ({ cursor: fetchMoreResult.notified.pageInfo.endCursor, hasNextPage: fetchMoreResult.notified.pageInfo.hasNextPage, nodes: [...d.nodes, ...fetchMoreResult.notified.notified], })); return { ...prev, notified: { ...prev.notified, pageInfo: { ...fetchMoreResult.notified.pageInfo, }, notified: [...prev.notified.notified, ...fetchMoreResult.notified.notified], }, }; }, }); } } }} > {data.nodes.map((n) => ( toggleRead({ variables: { notifiedID: n.id }, optimisticResponse: { __typename: 'Mutation', notificationToggleRead: { __typename: 'Notified', id: n.id, read: !n.read, readAt: new Date().toUTCString(), }, }, }) } /> ))} ); }; export default NotificationPopup;