import { List as ImmutableList } from 'immutable'; import React, { useCallback } from 'react'; import { defineMessages, useIntl, FormattedList, FormattedMessage, IntlShape, MessageDescriptor } from 'react-intl'; import { Link, useHistory } from 'react-router-dom'; import { mentionCompose } from 'soapbox/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'soapbox/actions/interactions'; import { openModal } from 'soapbox/actions/modals'; import { getSettings } from 'soapbox/actions/settings'; import { hideStatus, revealStatus } from 'soapbox/actions/statuses'; import Icon from 'soapbox/components/icon'; import { HStack, Text, Emoji } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account-container'; import StatusContainer from 'soapbox/containers/status-container'; import { HotKeys } from 'soapbox/features/ui/components/hotkeys'; import { useAppDispatch, useAppSelector, useInstance } from 'soapbox/hooks'; import { makeGetNotification } from 'soapbox/selectors'; import { NotificationType, validType } from 'soapbox/utils/notification'; import type { ScrollPosition } from 'soapbox/components/status'; import type { Account as AccountEntity, Status as StatusEntity, Notification as NotificationEntity, } from 'soapbox/types/entities'; const notificationForScreenReader = (intl: IntlShape, message: string, timestamp: Date) => { const output = [message]; output.push(intl.formatDate(timestamp, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' })); return output.join(', '); }; const buildLink = (account: AccountEntity): JSX.Element => ( ); const icons: Record = { follow: require('@tabler/icons/outline/user-plus.svg'), follow_request: require('@tabler/icons/outline/user-plus.svg'), mention: require('@tabler/icons/outline/at.svg'), favourite: require('@tabler/icons/outline/heart.svg'), group_favourite: require('@tabler/icons/outline/heart.svg'), reblog: require('@tabler/icons/outline/repeat.svg'), group_reblog: require('@tabler/icons/outline/repeat.svg'), status: require('@tabler/icons/outline/bell-ringing.svg'), poll: require('@tabler/icons/outline/chart-bar.svg'), move: require('@tabler/icons/outline/briefcase.svg'), 'pleroma:chat_mention': require('@tabler/icons/outline/messages.svg'), 'pleroma:emoji_reaction': require('@tabler/icons/outline/mood-happy.svg'), user_approved: require('@tabler/icons/outline/user-plus.svg'), update: require('@tabler/icons/outline/pencil.svg'), 'pleroma:event_reminder': require('@tabler/icons/outline/calendar-time.svg'), 'pleroma:participation_request': require('@tabler/icons/outline/calendar-event.svg'), 'pleroma:participation_accepted': require('@tabler/icons/outline/calendar-event.svg'), }; const messages: Record = defineMessages({ follow: { id: 'notification.follow', defaultMessage: '{name} followed you', }, follow_request: { id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you', }, mention: { id: 'notification.mentioned', defaultMessage: '{name} mentioned you', }, favourite: { id: 'notification.favourite', defaultMessage: '{name} liked your post', }, group_favourite: { id: 'notification.group_favourite', defaultMessage: '{name} liked your group post', }, reblog: { id: 'notification.reblog', defaultMessage: '{name} reposted your post', }, group_reblog: { id: 'notification.group_reblog', defaultMessage: '{name} reposted your group post', }, status: { id: 'notification.status', defaultMessage: '{name} just posted', }, poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended', }, move: { id: 'notification.move', defaultMessage: '{name} moved to {targetName}', }, 'pleroma:chat_mention': { id: 'notification.pleroma:chat_mention', defaultMessage: '{name} sent you a message', }, 'pleroma:emoji_reaction': { id: 'notification.pleroma:emoji_reaction', defaultMessage: '{name} reacted to your post', }, user_approved: { id: 'notification.user_approved', defaultMessage: 'Welcome to {instance}!', }, update: { id: 'notification.update', defaultMessage: '{name} edited a post you interacted with', }, 'pleroma:event_reminder': { id: 'notification.pleroma:event_reminder', defaultMessage: 'An event you are participating in starts soon', }, 'pleroma:participation_request': { id: 'notification.pleroma:participation_request', defaultMessage: '{name} wants to join your event', }, 'pleroma:participation_accepted': { id: 'notification.pleroma:participation_accepted', defaultMessage: 'You were accepted to join the event', }, }); const buildMessage = ( intl: IntlShape, type: NotificationType, account: AccountEntity, accounts: ImmutableList | null, targetName: string, instanceTitle: string, ): React.ReactNode => { if (!accounts) accounts = accounts || ImmutableList([account]); const renderedAccounts = accounts.slice(0, 2).map(account => buildLink(account)).toArray().filter(Boolean); if (accounts.size > 2) { renderedAccounts.push( , ); } return intl.formatMessage(messages[type], { name: , targetName, instance: instanceTitle, count: accounts.size, }); }; const avatarSize = 48; interface INotification { hidden?: boolean; notification: NotificationEntity; onMoveUp?: (notificationId: string) => void; onMoveDown?: (notificationId: string) => void; onReblog?: (status: StatusEntity, e?: KeyboardEvent) => void; getScrollPosition?: () => ScrollPosition | undefined; updateScrollBottom?: (bottom: number) => void; } const Notification: React.FC = (props) => { const { hidden = false, onMoveUp, onMoveDown } = props; const dispatch = useAppDispatch(); const getNotification = useCallback(makeGetNotification(), []); const notification = useAppSelector((state) => getNotification(state, props.notification)); const history = useHistory(); const intl = useIntl(); const instance = useInstance(); const type = notification.type; const { account, accounts, status } = notification; const getHandlers = () => ({ reply: handleMention, favourite: handleHotkeyFavourite, boost: handleHotkeyBoost, mention: handleMention, open: handleOpen, openProfile: handleOpenProfile, moveUp: handleMoveUp, moveDown: handleMoveDown, toggleHidden: handleHotkeyToggleHidden, }); const handleOpen = () => { if (status && typeof status === 'object' && account && typeof account === 'object') { history.push(`/@${account.acct}/posts/${status.id}`); } else { handleOpenProfile(); } }; const handleOpenProfile = () => { if (account && typeof account === 'object') { history.push(`/@${account.acct}`); } }; const handleMention = useCallback((e?: KeyboardEvent) => { e?.preventDefault(); if (account && typeof account === 'object') { dispatch(mentionCompose(account)); } }, [account]); const handleHotkeyFavourite = useCallback((e?: KeyboardEvent) => { if (status && typeof status === 'object') { if (status.favourited) { dispatch(unfavourite(status)); } else { dispatch(favourite(status)); } } }, [status]); const handleHotkeyBoost = useCallback((e?: KeyboardEvent) => { if (status && typeof status === 'object') { dispatch((_, getState) => { const boostModal = getSettings(getState()).get('boostModal'); if (status.reblogged) { dispatch(unreblog(status)); } else { if (e?.shiftKey || !boostModal) { dispatch(reblog(status)); } else { dispatch(openModal('BOOST', { status, onReblog: (status: StatusEntity) => { dispatch(reblog(status)); } })); } } }); } }, [status]); const handleHotkeyToggleHidden = useCallback((e?: KeyboardEvent) => { if (status && typeof status === 'object') { if (status.hidden) { dispatch(revealStatus(status.id)); } else { dispatch(hideStatus(status.id)); } } }, [status]); const handleMoveUp = () => { if (onMoveUp) { onMoveUp(notification.id); } }; const handleMoveDown = () => { if (onMoveDown) { onMoveDown(notification.id); } }; const renderIcon = (): React.ReactNode => { if (type === 'pleroma:emoji_reaction' && notification.emoji) { return ( ); } else if (validType(type)) { return ( ); } else { return null; } }; const renderContent = () => { switch (type as NotificationType) { case 'follow': case 'user_approved': return account && typeof account === 'object' ? (