mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-14 04:08:32 +03:00
Notifications popover, for larger screens
This commit is contained in:
parent
c9dbe23347
commit
ef06faf259
6 changed files with 493 additions and 298 deletions
|
@ -97,6 +97,8 @@ function NavMenu(props) {
|
|||
{...props}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
align="center"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
unmountOnClose
|
||||
>
|
||||
|
|
250
src/components/notification.jsx
Normal file
250
src/components/notification.jsx
Normal file
|
@ -0,0 +1,250 @@
|
|||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
|
||||
import Avatar from './avatar';
|
||||
import Icon from './icon';
|
||||
import Link from './link';
|
||||
import NameText from './name-text';
|
||||
import RelativeTime from './relative-time';
|
||||
import Status from './status';
|
||||
|
||||
const NOTIFICATION_ICONS = {
|
||||
mention: 'comment',
|
||||
status: 'notification',
|
||||
reblog: 'rocket',
|
||||
follow: 'follow',
|
||||
follow_request: 'follow-add',
|
||||
favourite: 'heart',
|
||||
poll: 'poll',
|
||||
update: 'pencil',
|
||||
};
|
||||
|
||||
/*
|
||||
Notification types
|
||||
==================
|
||||
mention = Someone mentioned you in their status
|
||||
status = Someone you enabled notifications for has posted a status
|
||||
reblog = Someone boosted one of your statuses
|
||||
follow = Someone followed you
|
||||
follow_request = Someone requested to follow you
|
||||
favourite = Someone favourited one of your statuses
|
||||
poll = A poll you have voted in or created has ended
|
||||
update = A status you interacted with has been edited
|
||||
admin.sign_up = Someone signed up (optionally sent to admins)
|
||||
admin.report = A new report has been filed
|
||||
*/
|
||||
|
||||
const contentText = {
|
||||
mention: 'mentioned you in their post.',
|
||||
status: 'published a post.',
|
||||
reblog: 'boosted your post.',
|
||||
follow: 'followed you.',
|
||||
follow_request: 'requested to follow you.',
|
||||
favourite: 'favourited your post.',
|
||||
poll: 'A poll you have voted in or created has ended.',
|
||||
'poll-self': 'A poll you have created has ended.',
|
||||
'poll-voted': 'A poll you have voted in has ended.',
|
||||
update: 'A post you interacted with has been edited.',
|
||||
'favourite+reblog': 'boosted & favourited your post.',
|
||||
};
|
||||
|
||||
function Notification({ notification, instance }) {
|
||||
const { id, status, account, _accounts } = notification;
|
||||
let { type } = notification;
|
||||
|
||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||
const actualStatusID = status?.reblog?.id || status?.id;
|
||||
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const isSelf = currentAccount === account?.id;
|
||||
const isVoted = status?.poll?.voted;
|
||||
|
||||
let favsCount = 0;
|
||||
let reblogsCount = 0;
|
||||
if (type === 'favourite+reblog') {
|
||||
for (const account of _accounts) {
|
||||
if (account._types?.includes('favourite')) {
|
||||
favsCount++;
|
||||
}
|
||||
if (account._types?.includes('reblog')) {
|
||||
reblogsCount++;
|
||||
}
|
||||
}
|
||||
if (!reblogsCount && favsCount) type = 'favourite';
|
||||
if (!favsCount && reblogsCount) type = 'reblog';
|
||||
}
|
||||
|
||||
const text =
|
||||
type === 'poll'
|
||||
? contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll']
|
||||
: contentText[type];
|
||||
|
||||
return (
|
||||
<div class={`notification notification-${type}`} tabIndex="0">
|
||||
<div
|
||||
class={`notification-type notification-${type}`}
|
||||
title={new Date(notification.createdAt).toLocaleString()}
|
||||
>
|
||||
{type === 'favourite+reblog' ? (
|
||||
<>
|
||||
<Icon icon="rocket" size="xl" alt={type} class="reblog-icon" />
|
||||
<Icon icon="heart" size="xl" alt={type} class="favourite-icon" />
|
||||
</>
|
||||
) : (
|
||||
<Icon
|
||||
icon={NOTIFICATION_ICONS[type] || 'notification'}
|
||||
size="xl"
|
||||
alt={type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
{type !== 'mention' && (
|
||||
<>
|
||||
<p>
|
||||
{!/poll|update/i.test(type) && (
|
||||
<>
|
||||
{_accounts?.length > 1 ? (
|
||||
<>
|
||||
<b>{_accounts.length} people</b>{' '}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NameText account={account} showAvatar />{' '}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{text}
|
||||
{type === 'mention' && (
|
||||
<span class="insignificant">
|
||||
{' '}
|
||||
•{' '}
|
||||
<RelativeTime
|
||||
datetime={notification.createdAt}
|
||||
format="micro"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{type === 'follow_request' && (
|
||||
<FollowRequestButtons
|
||||
accountID={account.id}
|
||||
onChange={() => {
|
||||
loadNotifications(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{_accounts?.length > 1 && (
|
||||
<p class="avatars-stack">
|
||||
{_accounts.map((account, i) => (
|
||||
<>
|
||||
<a
|
||||
href={account.url}
|
||||
rel="noopener noreferrer"
|
||||
class="account-avatar-stack"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = account;
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
url={account.avatarStatic}
|
||||
size={
|
||||
_accounts.length <= 10
|
||||
? 'xxl'
|
||||
: _accounts.length < 100
|
||||
? 'xl'
|
||||
: _accounts.length < 1000
|
||||
? 'l'
|
||||
: _accounts.length < 2000
|
||||
? 'm'
|
||||
: 's' // My god, this person is popular!
|
||||
}
|
||||
key={account.id}
|
||||
alt={`${account.displayName} @${account.acct}`}
|
||||
squircle={account?.bot}
|
||||
/>
|
||||
{type === 'favourite+reblog' && (
|
||||
<div class="account-sub-icons">
|
||||
{account._types.map((type) => (
|
||||
<Icon
|
||||
icon={NOTIFICATION_ICONS[type]}
|
||||
size="s"
|
||||
class={`${type}-icon`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</a>{' '}
|
||||
</>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{status && (
|
||||
<Link
|
||||
class={`status-link status-type-${type}`}
|
||||
to={
|
||||
instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`
|
||||
}
|
||||
>
|
||||
<Status statusID={actualStatusID} size="s" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FollowRequestButtons({ accountID, onChange }) {
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
return (
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.followRequests.authorize(accountID);
|
||||
onChange();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('default');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button>{' '}
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
class="light danger"
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.followRequests.reject(accountID);
|
||||
onChange();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('default');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export default Notification;
|
|
@ -1,13 +1,20 @@
|
|||
import './notifications-menu.css';
|
||||
|
||||
import { ControlledMenu } from '@szhsin/react-menu';
|
||||
import { memo } from 'preact/compat';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Columns from '../components/columns';
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import Notification from '../components/notification';
|
||||
import { api } from '../utils/api';
|
||||
import db from '../utils/db';
|
||||
import groupNotifications from '../utils/group-notifications';
|
||||
import openCompose from '../utils/open-compose';
|
||||
import states from '../utils/states';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
|
||||
import Following from './following';
|
||||
|
@ -27,6 +34,9 @@ function Home() {
|
|||
})();
|
||||
}, []);
|
||||
|
||||
const notificationLinkRef = useRef();
|
||||
const [menuState, setMenuState] = useState('closed');
|
||||
|
||||
return (
|
||||
<>
|
||||
{(snapStates.settings.shortcutsColumnsMode ||
|
||||
|
@ -40,17 +50,31 @@ function Home() {
|
|||
id="home"
|
||||
headerStart={false}
|
||||
headerEnd={
|
||||
<Link
|
||||
to="/notifications"
|
||||
class={`button plain notifications-button ${
|
||||
snapStates.notificationsShowNew ? 'has-badge' : ''
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Icon icon="notification" size="l" alt="Notifications" />
|
||||
</Link>
|
||||
<>
|
||||
<Link
|
||||
ref={notificationLinkRef}
|
||||
to="/notifications"
|
||||
class={`button plain notifications-button ${
|
||||
snapStates.notificationsShowNew ? 'has-badge' : ''
|
||||
} ${menuState}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (window.matchMedia('(min-width: calc(40em))').matches) {
|
||||
e.preventDefault();
|
||||
setMenuState((state) =>
|
||||
state === 'closed' ? 'open' : 'closed',
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon="notification" size="l" alt="Notifications" />
|
||||
</Link>
|
||||
<NotificationsMenu
|
||||
state={menuState}
|
||||
anchorRef={notificationLinkRef}
|
||||
onClose={() => setMenuState('closed')}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
@ -76,4 +100,112 @@ function Home() {
|
|||
);
|
||||
}
|
||||
|
||||
const NOTIFICATIONS_LIMIT = 30;
|
||||
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
|
||||
function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||
const { masto, instance } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: NOTIFICATIONS_LIMIT,
|
||||
});
|
||||
|
||||
async function fetchNotifications() {
|
||||
const allNotifications = await notificationsIterator.next();
|
||||
const notifications = allNotifications.value;
|
||||
|
||||
if (notifications?.length) {
|
||||
notifications.forEach((notification) => {
|
||||
saveStatus(notification.status, instance, {
|
||||
skipThreading: true,
|
||||
});
|
||||
});
|
||||
|
||||
const groupedNotifications = groupNotifications(notifications);
|
||||
|
||||
states.notificationsLast = notifications[0];
|
||||
states.notifications = groupedNotifications;
|
||||
}
|
||||
|
||||
states.notificationsShowNew = false;
|
||||
states.notificationsLastFetchTime = Date.now();
|
||||
return allNotifications;
|
||||
}
|
||||
|
||||
function loadNotifications() {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await fetchNotifications();
|
||||
setUIState('default');
|
||||
} catch (e) {
|
||||
setUIState('error');
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadNotifications();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ControlledMenu
|
||||
menuClassName="notifications-menu"
|
||||
state={state}
|
||||
anchorRef={anchorRef}
|
||||
onClose={onClose}
|
||||
portal={{
|
||||
target: document.body,
|
||||
}}
|
||||
overflow="auto"
|
||||
viewScroll="close"
|
||||
position="anchor"
|
||||
align="center"
|
||||
boundingBoxPadding="8 8 8 8"
|
||||
unmountOnClose
|
||||
>
|
||||
<header>
|
||||
<h2>Notifications</h2>
|
||||
</header>
|
||||
{snapStates.notifications.length ? (
|
||||
<>
|
||||
{snapStates.notifications
|
||||
.slice(0, NOTIFICATIONS_DISPLAY_LIMIT)
|
||||
.map((notification) => (
|
||||
<Notification
|
||||
key={notification.id}
|
||||
instance={instance}
|
||||
notification={notification}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : uiState === 'loading' ? (
|
||||
<div class="ui-state">
|
||||
<Loader abrupt />
|
||||
</div>
|
||||
) : (
|
||||
uiState === 'error' && (
|
||||
<div class="ui-state">
|
||||
<p>Unable to fetch notifications.</p>
|
||||
<p>
|
||||
<button type="button" onClick={loadNotifications}>
|
||||
Try again
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<footer>
|
||||
<Link to="/mentions" class="button plain">
|
||||
<Icon icon="at" /> <span>Mentions</span>
|
||||
</Link>
|
||||
<Link to="/notifications" class="button plain2">
|
||||
<b>See all</b> <Icon icon="arrow-right" />
|
||||
</Link>
|
||||
</footer>
|
||||
</ControlledMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Home);
|
||||
|
|
51
src/pages/notifications-menu.css
Normal file
51
src/pages/notifications-menu.css
Normal file
|
@ -0,0 +1,51 @@
|
|||
@keyframes bell {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: rotate(5deg);
|
||||
}
|
||||
66% {
|
||||
transform: rotate(-10deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
.notifications-button.open {
|
||||
animation: bell 0.3s ease-out both;
|
||||
transform-origin: 50% 0;
|
||||
}
|
||||
|
||||
.notifications-menu {
|
||||
width: 23em;
|
||||
font-size: 90%;
|
||||
padding: 0;
|
||||
height: 40em;
|
||||
overflow: auto;
|
||||
}
|
||||
.notifications-menu header {
|
||||
padding: 16px;
|
||||
margin: 0;
|
||||
border-bottom: var(--hairline-width) solid var(--outline-color);
|
||||
}
|
||||
.notifications-menu header h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.notifications-menu .notification {
|
||||
animation: appear-smooth 0.3s ease-out 0.1s both;
|
||||
}
|
||||
.notifications-menu footer {
|
||||
animation: slide-up 0.3s ease-out 0.2s both;
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
border-top: var(--hairline-width) solid var(--outline-color);
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(16px);
|
||||
padding: 16px;
|
||||
gap: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
|
@ -4,61 +4,18 @@ import { memo } from 'preact/compat';
|
|||
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||
import { useSnapshot } from 'valtio';
|
||||
|
||||
import Avatar from '../components/avatar';
|
||||
import Icon from '../components/icon';
|
||||
import Link from '../components/link';
|
||||
import Loader from '../components/loader';
|
||||
import NameText from '../components/name-text';
|
||||
import NavMenu from '../components/nav-menu';
|
||||
import RelativeTime from '../components/relative-time';
|
||||
import Status from '../components/status';
|
||||
import Notification from '../components/notification';
|
||||
import { api } from '../utils/api';
|
||||
import groupNotifications from '../utils/group-notifications';
|
||||
import niceDateTime from '../utils/nice-date-time';
|
||||
import states, { saveStatus } from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import useScroll from '../utils/useScroll';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
/*
|
||||
Notification types
|
||||
==================
|
||||
mention = Someone mentioned you in their status
|
||||
status = Someone you enabled notifications for has posted a status
|
||||
reblog = Someone boosted one of your statuses
|
||||
follow = Someone followed you
|
||||
follow_request = Someone requested to follow you
|
||||
favourite = Someone favourited one of your statuses
|
||||
poll = A poll you have voted in or created has ended
|
||||
update = A status you interacted with has been edited
|
||||
admin.sign_up = Someone signed up (optionally sent to admins)
|
||||
admin.report = A new report has been filed
|
||||
*/
|
||||
|
||||
const contentText = {
|
||||
mention: 'mentioned you in their post.',
|
||||
status: 'published a post.',
|
||||
reblog: 'boosted your post.',
|
||||
follow: 'followed you.',
|
||||
follow_request: 'requested to follow you.',
|
||||
favourite: 'favourited your post.',
|
||||
poll: 'A poll you have voted in or created has ended.',
|
||||
'poll-self': 'A poll you have created has ended.',
|
||||
'poll-voted': 'A poll you have voted in has ended.',
|
||||
update: 'A post you interacted with has been edited.',
|
||||
'favourite+reblog': 'boosted & favourited your post.',
|
||||
};
|
||||
|
||||
const NOTIFICATION_ICONS = {
|
||||
mention: 'comment',
|
||||
status: 'notification',
|
||||
reblog: 'rocket',
|
||||
follow: 'follow',
|
||||
follow_request: 'follow-add',
|
||||
favourite: 'heart',
|
||||
poll: 'poll',
|
||||
update: 'pencil',
|
||||
};
|
||||
|
||||
const LIMIT = 30; // 30 is the maximum limit :(
|
||||
|
||||
function Notifications() {
|
||||
|
@ -287,245 +244,5 @@ function Notifications() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
function Notification({ notification, instance }) {
|
||||
const { id, status, account, _accounts } = notification;
|
||||
let { type } = notification;
|
||||
|
||||
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
||||
const actualStatusID = status?.reblog?.id || status?.id;
|
||||
|
||||
const currentAccount = store.session.get('currentAccount');
|
||||
const isSelf = currentAccount === account?.id;
|
||||
const isVoted = status?.poll?.voted;
|
||||
|
||||
let favsCount = 0;
|
||||
let reblogsCount = 0;
|
||||
if (type === 'favourite+reblog') {
|
||||
for (const account of _accounts) {
|
||||
if (account._types?.includes('favourite')) {
|
||||
favsCount++;
|
||||
}
|
||||
if (account._types?.includes('reblog')) {
|
||||
reblogsCount++;
|
||||
}
|
||||
}
|
||||
if (!reblogsCount && favsCount) type = 'favourite';
|
||||
if (!favsCount && reblogsCount) type = 'reblog';
|
||||
}
|
||||
|
||||
const text =
|
||||
type === 'poll'
|
||||
? contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll']
|
||||
: contentText[type];
|
||||
|
||||
return (
|
||||
<div class={`notification notification-${type}`} tabIndex="0">
|
||||
<div
|
||||
class={`notification-type notification-${type}`}
|
||||
title={new Date(notification.createdAt).toLocaleString()}
|
||||
>
|
||||
{type === 'favourite+reblog' ? (
|
||||
<>
|
||||
<Icon icon="rocket" size="xl" alt={type} class="reblog-icon" />
|
||||
<Icon icon="heart" size="xl" alt={type} class="favourite-icon" />
|
||||
</>
|
||||
) : (
|
||||
<Icon
|
||||
icon={NOTIFICATION_ICONS[type] || 'notification'}
|
||||
size="xl"
|
||||
alt={type}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div class="notification-content">
|
||||
{type !== 'mention' && (
|
||||
<>
|
||||
<p>
|
||||
{!/poll|update/i.test(type) && (
|
||||
<>
|
||||
{_accounts?.length > 1 ? (
|
||||
<>
|
||||
<b>{_accounts.length} people</b>{' '}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<NameText account={account} showAvatar />{' '}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{text}
|
||||
{type === 'mention' && (
|
||||
<span class="insignificant">
|
||||
{' '}
|
||||
•{' '}
|
||||
<RelativeTime
|
||||
datetime={notification.createdAt}
|
||||
format="micro"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
{type === 'follow_request' && (
|
||||
<FollowRequestButtons
|
||||
accountID={account.id}
|
||||
onChange={() => {
|
||||
loadNotifications(true);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{_accounts?.length > 1 && (
|
||||
<p class="avatars-stack">
|
||||
{_accounts.map((account, i) => (
|
||||
<>
|
||||
<a
|
||||
href={account.url}
|
||||
rel="noopener noreferrer"
|
||||
class="account-avatar-stack"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = account;
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
url={account.avatarStatic}
|
||||
size={
|
||||
_accounts.length <= 10
|
||||
? 'xxl'
|
||||
: _accounts.length < 100
|
||||
? 'xl'
|
||||
: _accounts.length < 1000
|
||||
? 'l'
|
||||
: _accounts.length < 2000
|
||||
? 'm'
|
||||
: 's' // My god, this person is popular!
|
||||
}
|
||||
key={account.id}
|
||||
alt={`${account.displayName} @${account.acct}`}
|
||||
squircle={account?.bot}
|
||||
/>
|
||||
{type === 'favourite+reblog' && (
|
||||
<div class="account-sub-icons">
|
||||
{account._types.map((type) => (
|
||||
<Icon
|
||||
icon={NOTIFICATION_ICONS[type]}
|
||||
size="s"
|
||||
class={`${type}-icon`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</a>{' '}
|
||||
</>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
{status && (
|
||||
<Link
|
||||
class={`status-link status-type-${type}`}
|
||||
to={
|
||||
instance
|
||||
? `/${instance}/s/${actualStatusID}`
|
||||
: `/s/${actualStatusID}`
|
||||
}
|
||||
>
|
||||
<Status statusID={actualStatusID} size="s" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FollowRequestButtons({ accountID, onChange }) {
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
return (
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.followRequests.authorize(accountID);
|
||||
onChange();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('default');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button>{' '}
|
||||
<button
|
||||
type="button"
|
||||
disabled={uiState === 'loading'}
|
||||
class="light danger"
|
||||
onClick={() => {
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.followRequests.reject(accountID);
|
||||
onChange();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setUIState('default');
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function groupNotifications(notifications) {
|
||||
// Create new flat list of notifications
|
||||
// Combine sibling notifications based on type and status id
|
||||
// Concat all notification.account into an array of _accounts
|
||||
const notificationsMap = {};
|
||||
const cleanNotifications = [];
|
||||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
const { status, account, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
let virtualType = type;
|
||||
if (type === 'favourite' || type === 'reblog') {
|
||||
virtualType = 'favourite+reblog';
|
||||
}
|
||||
const key = `${status?.id}-${virtualType}-${date}`;
|
||||
const mappedNotification = notificationsMap[key];
|
||||
if (virtualType === 'follow_request') {
|
||||
cleanNotifications[j++] = notification;
|
||||
} else if (mappedNotification?.account) {
|
||||
const mappedAccount = mappedNotification._accounts.find(
|
||||
(a) => a.id === account.id,
|
||||
);
|
||||
if (mappedAccount) {
|
||||
mappedAccount._types.push(type);
|
||||
mappedAccount._types.sort().reverse();
|
||||
} else {
|
||||
account._types = [type];
|
||||
mappedNotification._accounts.push(account);
|
||||
}
|
||||
} else {
|
||||
account._types = [type];
|
||||
let n = (notificationsMap[key] = {
|
||||
...notification,
|
||||
type: virtualType,
|
||||
_accounts: [account],
|
||||
});
|
||||
cleanNotifications[j++] = n;
|
||||
}
|
||||
}
|
||||
return cleanNotifications;
|
||||
}
|
||||
|
||||
export default memo(Notifications);
|
||||
|
|
43
src/utils/group-notifications.jsx
Normal file
43
src/utils/group-notifications.jsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
function groupNotifications(notifications) {
|
||||
// Create new flat list of notifications
|
||||
// Combine sibling notifications based on type and status id
|
||||
// Concat all notification.account into an array of _accounts
|
||||
const notificationsMap = {};
|
||||
const cleanNotifications = [];
|
||||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||
const notification = notifications[i];
|
||||
const { status, account, type, createdAt } = notification;
|
||||
const date = new Date(createdAt).toLocaleDateString();
|
||||
let virtualType = type;
|
||||
if (type === 'favourite' || type === 'reblog') {
|
||||
virtualType = 'favourite+reblog';
|
||||
}
|
||||
const key = `${status?.id}-${virtualType}-${date}`;
|
||||
const mappedNotification = notificationsMap[key];
|
||||
if (virtualType === 'follow_request') {
|
||||
cleanNotifications[j++] = notification;
|
||||
} else if (mappedNotification?.account) {
|
||||
const mappedAccount = mappedNotification._accounts.find(
|
||||
(a) => a.id === account.id,
|
||||
);
|
||||
if (mappedAccount) {
|
||||
mappedAccount._types.push(type);
|
||||
mappedAccount._types.sort().reverse();
|
||||
} else {
|
||||
account._types = [type];
|
||||
mappedNotification._accounts.push(account);
|
||||
}
|
||||
} else {
|
||||
account._types = [type];
|
||||
let n = (notificationsMap[key] = {
|
||||
...notification,
|
||||
type: virtualType,
|
||||
_accounts: [account],
|
||||
});
|
||||
cleanNotifications[j++] = n;
|
||||
}
|
||||
}
|
||||
return cleanNotifications;
|
||||
}
|
||||
|
||||
export default groupNotifications;
|
Loading…
Add table
Reference in a new issue