2023-10-31 17:22:57 +03:00
|
|
|
import { Fragment } from 'preact';
|
2023-10-23 11:24:30 +03:00
|
|
|
import { memo } from 'preact/compat';
|
|
|
|
|
2023-09-12 13:50:46 +03:00
|
|
|
import shortenNumber from '../utils/shorten-number';
|
2023-04-30 16:03:09 +03:00
|
|
|
import states from '../utils/states';
|
|
|
|
import store from '../utils/store';
|
2023-09-19 16:53:59 +03:00
|
|
|
import useTruncated from '../utils/useTruncated';
|
2023-04-30 16:03:09 +03:00
|
|
|
|
|
|
|
import Avatar from './avatar';
|
2023-05-06 12:13:39 +03:00
|
|
|
import FollowRequestButtons from './follow-request-buttons';
|
2023-04-30 16:03:09 +03:00
|
|
|
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',
|
2023-09-05 04:19:11 +03:00
|
|
|
'admin.signup': 'account-edit',
|
|
|
|
'admin.report': 'account-warning',
|
2023-04-30 16:03:09 +03:00
|
|
|
};
|
|
|
|
|
|
|
|
/*
|
|
|
|
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.',
|
2023-08-27 08:06:26 +03:00
|
|
|
'reblog+account': (count) => `boosted ${count} of your posts.`,
|
2023-08-20 09:22:47 +03:00
|
|
|
reblog_reply: 'boosted your reply.',
|
2023-04-30 16:03:09 +03:00
|
|
|
follow: 'followed you.',
|
|
|
|
follow_request: 'requested to follow you.',
|
2023-10-25 08:55:12 +03:00
|
|
|
favourite: 'liked your post.',
|
|
|
|
'favourite+account': (count) => `liked ${count} of your posts.`,
|
|
|
|
favourite_reply: 'liked your reply.',
|
2023-04-30 16:03:09 +03:00
|
|
|
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.',
|
2023-10-25 08:55:12 +03:00
|
|
|
'favourite+reblog': 'boosted & liked your post.',
|
2023-08-27 08:06:26 +03:00
|
|
|
'favourite+reblog+account': (count) =>
|
2023-10-25 08:55:12 +03:00
|
|
|
`boosted & liked ${count} of your posts.`,
|
|
|
|
'favourite+reblog_reply': 'boosted & liked your reply.',
|
2023-10-14 12:58:46 +03:00
|
|
|
'admin.sign_up': 'signed up.',
|
2023-10-14 12:59:18 +03:00
|
|
|
'admin.report': (targetAccount) => <>reported {targetAccount}</>,
|
2023-04-30 16:03:09 +03:00
|
|
|
};
|
|
|
|
|
2023-09-16 18:42:49 +03:00
|
|
|
const AVATARS_LIMIT = 50;
|
|
|
|
|
2023-12-16 09:10:33 +03:00
|
|
|
function Notification({
|
|
|
|
notification,
|
|
|
|
instance,
|
|
|
|
isStatic,
|
|
|
|
disableContextMenu,
|
|
|
|
}) {
|
2023-10-14 12:59:18 +03:00
|
|
|
const { id, status, account, report, _accounts, _statuses } = notification;
|
2023-04-30 16:03:09 +03:00
|
|
|
let { type } = notification;
|
|
|
|
|
|
|
|
// status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update
|
2023-09-01 10:40:00 +03:00
|
|
|
const actualStatus = status?.reblog || status;
|
|
|
|
const actualStatusID = actualStatus?.id;
|
2023-04-30 16:03:09 +03:00
|
|
|
|
|
|
|
const currentAccount = store.session.get('currentAccount');
|
|
|
|
const isSelf = currentAccount === account?.id;
|
|
|
|
const isVoted = status?.poll?.voted;
|
2023-08-20 09:22:47 +03:00
|
|
|
const isReplyToOthers =
|
|
|
|
!!status?.inReplyToAccountId &&
|
|
|
|
status?.inReplyToAccountId !== currentAccount &&
|
|
|
|
status?.account?.id === currentAccount;
|
2023-04-30 16:03:09 +03:00
|
|
|
|
|
|
|
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';
|
|
|
|
}
|
|
|
|
|
2023-08-20 09:22:47 +03:00
|
|
|
let text;
|
|
|
|
if (type === 'poll') {
|
|
|
|
text = contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'];
|
|
|
|
} else if (
|
|
|
|
type === 'reblog' ||
|
|
|
|
type === 'favourite' ||
|
|
|
|
type === 'favourite+reblog'
|
|
|
|
) {
|
2023-08-27 08:06:26 +03:00
|
|
|
if (_statuses?.length > 1) {
|
|
|
|
text = contentText[`${type}+account`];
|
|
|
|
} else if (isReplyToOthers) {
|
|
|
|
text = contentText[`${type}_reply`];
|
|
|
|
} else {
|
|
|
|
text = contentText[type];
|
|
|
|
}
|
2023-09-05 04:19:11 +03:00
|
|
|
} else if (contentText[type]) {
|
2023-08-20 09:22:47 +03:00
|
|
|
text = contentText[type];
|
2023-09-05 04:19:11 +03:00
|
|
|
} else {
|
|
|
|
// Anticipate unhandled notification types, possibly from Mastodon forks or non-Mastodon instances
|
|
|
|
// This surfaces the error to the user, hoping that users will report it
|
|
|
|
text = `[Unknown notification type: ${type}]`;
|
2023-08-20 09:22:47 +03:00
|
|
|
}
|
2023-09-05 04:19:11 +03:00
|
|
|
|
2023-08-27 08:06:26 +03:00
|
|
|
if (typeof text === 'function') {
|
2023-10-14 12:59:18 +03:00
|
|
|
const count = _statuses?.length || _accounts?.length;
|
|
|
|
if (count) {
|
|
|
|
text = text(count);
|
|
|
|
} else if (type === 'admin.report') {
|
|
|
|
const targetAccount = report?.targetAccount;
|
|
|
|
if (targetAccount) {
|
|
|
|
text = text(<NameText account={targetAccount} showAvatar />);
|
|
|
|
}
|
|
|
|
}
|
2023-08-27 08:06:26 +03:00
|
|
|
}
|
2023-04-30 16:03:09 +03:00
|
|
|
|
2023-05-02 03:01:52 +03:00
|
|
|
if (type === 'mention' && !status) {
|
|
|
|
// Could be deleted
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-09-05 04:19:11 +03:00
|
|
|
const formattedCreatedAt =
|
|
|
|
notification.createdAt && new Date(notification.createdAt).toLocaleString();
|
|
|
|
|
2023-09-12 06:27:54 +03:00
|
|
|
const genericAccountsHeading =
|
|
|
|
{
|
2023-10-25 08:55:12 +03:00
|
|
|
'favourite+reblog': 'Boosted/Liked by…',
|
|
|
|
favourite: 'Liked by…',
|
2023-09-12 06:27:54 +03:00
|
|
|
reblog: 'Boosted by…',
|
|
|
|
follow: 'Followed by…',
|
|
|
|
}[type] || 'Accounts';
|
|
|
|
const handleOpenGenericAccounts = () => {
|
|
|
|
states.showGenericAccounts = {
|
|
|
|
heading: genericAccountsHeading,
|
|
|
|
accounts: _accounts,
|
|
|
|
showReactions: type === 'favourite+reblog',
|
2023-12-20 08:55:56 +03:00
|
|
|
excludeRelationshipAttrs: type === 'follow' ? ['followedBy'] : [],
|
2023-09-12 06:27:54 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-10-23 11:24:30 +03:00
|
|
|
console.debug('RENDER Notification', notification.id);
|
|
|
|
|
2023-04-30 16:03:09 +03:00
|
|
|
return (
|
2023-10-23 11:24:30 +03:00
|
|
|
<div
|
|
|
|
class={`notification notification-${type}`}
|
|
|
|
data-notification-id={id}
|
|
|
|
tabIndex="0"
|
|
|
|
>
|
2023-04-30 16:03:09 +03:00
|
|
|
<div
|
|
|
|
class={`notification-type notification-${type}`}
|
2023-09-05 04:19:11 +03:00
|
|
|
title={formattedCreatedAt}
|
2023-04-30 16:03:09 +03:00
|
|
|
>
|
|
|
|
{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 ? (
|
|
|
|
<>
|
2023-09-12 06:27:54 +03:00
|
|
|
<b tabIndex="0" onClick={handleOpenGenericAccounts}>
|
2023-09-12 13:50:46 +03:00
|
|
|
<span title={_accounts.length}>
|
|
|
|
{shortenNumber(_accounts.length)}
|
|
|
|
</span>{' '}
|
|
|
|
people
|
2023-09-12 06:27:54 +03:00
|
|
|
</b>{' '}
|
2023-04-30 16:03:09 +03:00
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<NameText account={account} showAvatar />{' '}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{text}
|
|
|
|
{type === 'mention' && (
|
|
|
|
<span class="insignificant">
|
|
|
|
{' '}
|
|
|
|
•{' '}
|
|
|
|
<RelativeTime
|
|
|
|
datetime={notification.createdAt}
|
|
|
|
format="micro"
|
|
|
|
/>
|
|
|
|
</span>
|
|
|
|
)}
|
|
|
|
</p>
|
|
|
|
{type === 'follow_request' && (
|
2023-10-23 11:24:30 +03:00
|
|
|
<FollowRequestButtons accountID={account.id} />
|
2023-04-30 16:03:09 +03:00
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
{_accounts?.length > 1 && (
|
|
|
|
<p class="avatars-stack">
|
2023-10-31 17:22:57 +03:00
|
|
|
{_accounts.slice(0, AVATARS_LIMIT).map((account) => (
|
|
|
|
<Fragment key={account.id}>
|
2023-04-30 16:03:09 +03:00
|
|
|
<a
|
2023-10-31 17:22:57 +03:00
|
|
|
key={account.id}
|
2023-04-30 16:03:09 +03:00
|
|
|
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'
|
2023-09-12 13:50:46 +03:00
|
|
|
: _accounts.length < 20
|
2023-04-30 16:03:09 +03:00
|
|
|
? 'xl'
|
2023-09-12 13:50:46 +03:00
|
|
|
: _accounts.length < 30
|
2023-04-30 16:03:09 +03:00
|
|
|
? 'l'
|
2023-09-12 13:50:46 +03:00
|
|
|
: _accounts.length < 40
|
2023-04-30 16:03:09 +03:00
|
|
|
? '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>{' '}
|
2023-10-31 17:22:57 +03:00
|
|
|
</Fragment>
|
2023-04-30 16:03:09 +03:00
|
|
|
))}
|
2023-09-12 06:27:54 +03:00
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="small plain"
|
|
|
|
onClick={handleOpenGenericAccounts}
|
|
|
|
>
|
2023-09-16 18:42:49 +03:00
|
|
|
{_accounts.length > AVATARS_LIMIT &&
|
|
|
|
`+${_accounts.length - AVATARS_LIMIT}`}
|
2023-09-12 06:27:54 +03:00
|
|
|
<Icon icon="chevron-down" />
|
|
|
|
</button>
|
2023-04-30 16:03:09 +03:00
|
|
|
</p>
|
|
|
|
)}
|
2023-08-27 08:06:26 +03:00
|
|
|
{_statuses?.length > 1 && (
|
|
|
|
<ul class="notification-group-statuses">
|
|
|
|
{_statuses.map((status) => (
|
|
|
|
<li key={status.id}>
|
2023-09-19 16:53:59 +03:00
|
|
|
<TruncatedLink
|
2023-08-27 08:06:26 +03:00
|
|
|
class={`status-link status-type-${type}`}
|
|
|
|
to={
|
|
|
|
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
|
|
|
|
}
|
|
|
|
>
|
2024-01-11 05:44:24 +03:00
|
|
|
<Status
|
|
|
|
status={status}
|
|
|
|
size="s"
|
|
|
|
previewMode
|
|
|
|
allowContextMenu
|
|
|
|
/>
|
2023-09-19 16:53:59 +03:00
|
|
|
</TruncatedLink>
|
2023-08-27 08:06:26 +03:00
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
)}
|
|
|
|
{status && (!_statuses?.length || _statuses?.length <= 1) && (
|
2023-09-19 16:53:59 +03:00
|
|
|
<TruncatedLink
|
2023-04-30 16:03:09 +03:00
|
|
|
class={`status-link status-type-${type}`}
|
|
|
|
to={
|
|
|
|
instance
|
|
|
|
? `/${instance}/s/${actualStatusID}`
|
|
|
|
: `/s/${actualStatusID}`
|
|
|
|
}
|
2023-12-16 09:10:33 +03:00
|
|
|
onContextMenu={
|
|
|
|
!disableContextMenu
|
|
|
|
? (e) => {
|
|
|
|
const post = e.target.querySelector('.status');
|
|
|
|
if (post) {
|
|
|
|
// Fire a custom event to open the context menu
|
|
|
|
if (e.metaKey) return;
|
|
|
|
e.preventDefault();
|
|
|
|
post.dispatchEvent(
|
|
|
|
new MouseEvent('contextmenu', {
|
|
|
|
clientX: e.clientX,
|
|
|
|
clientY: e.clientY,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
: undefined
|
|
|
|
}
|
2023-04-30 16:03:09 +03:00
|
|
|
>
|
2023-09-01 10:40:00 +03:00
|
|
|
{isStatic ? (
|
2024-01-11 05:44:24 +03:00
|
|
|
<Status
|
|
|
|
status={actualStatus}
|
|
|
|
size="s"
|
2024-02-23 13:07:42 +03:00
|
|
|
readOnly
|
2024-01-11 05:44:24 +03:00
|
|
|
allowContextMenu
|
|
|
|
/>
|
2023-09-01 10:40:00 +03:00
|
|
|
) : (
|
2024-01-11 05:44:24 +03:00
|
|
|
<Status
|
|
|
|
statusID={actualStatusID}
|
|
|
|
size="s"
|
2024-02-23 13:07:42 +03:00
|
|
|
readOnly
|
2024-01-11 05:44:24 +03:00
|
|
|
allowContextMenu
|
|
|
|
/>
|
2023-09-01 10:40:00 +03:00
|
|
|
)}
|
2023-09-19 16:53:59 +03:00
|
|
|
</TruncatedLink>
|
2023-04-30 16:03:09 +03:00
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-09-19 16:53:59 +03:00
|
|
|
function TruncatedLink(props) {
|
|
|
|
const ref = useTruncated();
|
|
|
|
return <Link {...props} data-read-more="Read more →" ref={ref} />;
|
|
|
|
}
|
|
|
|
|
2024-02-15 13:07:17 +03:00
|
|
|
export default memo(Notification, (oldProps, newProps) => {
|
|
|
|
return oldProps.notification?.id === newProps.notification?.id;
|
|
|
|
});
|