this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Notifications popover, for larger screens

+493 -298
+2
src/components/nav-menu.jsx
··· 97 97 {...props} 98 98 overflow="auto" 99 99 viewScroll="close" 100 + position="anchor" 101 + align="center" 100 102 boundingBoxPadding="8 8 8 8" 101 103 unmountOnClose 102 104 >
+250
src/components/notification.jsx
··· 1 + import states from '../utils/states'; 2 + import store from '../utils/store'; 3 + 4 + import Avatar from './avatar'; 5 + import Icon from './icon'; 6 + import Link from './link'; 7 + import NameText from './name-text'; 8 + import RelativeTime from './relative-time'; 9 + import Status from './status'; 10 + 11 + const NOTIFICATION_ICONS = { 12 + mention: 'comment', 13 + status: 'notification', 14 + reblog: 'rocket', 15 + follow: 'follow', 16 + follow_request: 'follow-add', 17 + favourite: 'heart', 18 + poll: 'poll', 19 + update: 'pencil', 20 + }; 21 + 22 + /* 23 + Notification types 24 + ================== 25 + mention = Someone mentioned you in their status 26 + status = Someone you enabled notifications for has posted a status 27 + reblog = Someone boosted one of your statuses 28 + follow = Someone followed you 29 + follow_request = Someone requested to follow you 30 + favourite = Someone favourited one of your statuses 31 + poll = A poll you have voted in or created has ended 32 + update = A status you interacted with has been edited 33 + admin.sign_up = Someone signed up (optionally sent to admins) 34 + admin.report = A new report has been filed 35 + */ 36 + 37 + const contentText = { 38 + mention: 'mentioned you in their post.', 39 + status: 'published a post.', 40 + reblog: 'boosted your post.', 41 + follow: 'followed you.', 42 + follow_request: 'requested to follow you.', 43 + favourite: 'favourited your post.', 44 + poll: 'A poll you have voted in or created has ended.', 45 + 'poll-self': 'A poll you have created has ended.', 46 + 'poll-voted': 'A poll you have voted in has ended.', 47 + update: 'A post you interacted with has been edited.', 48 + 'favourite+reblog': 'boosted & favourited your post.', 49 + }; 50 + 51 + function Notification({ notification, instance }) { 52 + const { id, status, account, _accounts } = notification; 53 + let { type } = notification; 54 + 55 + // status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update 56 + const actualStatusID = status?.reblog?.id || status?.id; 57 + 58 + const currentAccount = store.session.get('currentAccount'); 59 + const isSelf = currentAccount === account?.id; 60 + const isVoted = status?.poll?.voted; 61 + 62 + let favsCount = 0; 63 + let reblogsCount = 0; 64 + if (type === 'favourite+reblog') { 65 + for (const account of _accounts) { 66 + if (account._types?.includes('favourite')) { 67 + favsCount++; 68 + } 69 + if (account._types?.includes('reblog')) { 70 + reblogsCount++; 71 + } 72 + } 73 + if (!reblogsCount && favsCount) type = 'favourite'; 74 + if (!favsCount && reblogsCount) type = 'reblog'; 75 + } 76 + 77 + const text = 78 + type === 'poll' 79 + ? contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'] 80 + : contentText[type]; 81 + 82 + return ( 83 + <div class={`notification notification-${type}`} tabIndex="0"> 84 + <div 85 + class={`notification-type notification-${type}`} 86 + title={new Date(notification.createdAt).toLocaleString()} 87 + > 88 + {type === 'favourite+reblog' ? ( 89 + <> 90 + <Icon icon="rocket" size="xl" alt={type} class="reblog-icon" /> 91 + <Icon icon="heart" size="xl" alt={type} class="favourite-icon" /> 92 + </> 93 + ) : ( 94 + <Icon 95 + icon={NOTIFICATION_ICONS[type] || 'notification'} 96 + size="xl" 97 + alt={type} 98 + /> 99 + )} 100 + </div> 101 + <div class="notification-content"> 102 + {type !== 'mention' && ( 103 + <> 104 + <p> 105 + {!/poll|update/i.test(type) && ( 106 + <> 107 + {_accounts?.length > 1 ? ( 108 + <> 109 + <b>{_accounts.length} people</b>{' '} 110 + </> 111 + ) : ( 112 + <> 113 + <NameText account={account} showAvatar />{' '} 114 + </> 115 + )} 116 + </> 117 + )} 118 + {text} 119 + {type === 'mention' && ( 120 + <span class="insignificant"> 121 + {' '} 122 + •{' '} 123 + <RelativeTime 124 + datetime={notification.createdAt} 125 + format="micro" 126 + /> 127 + </span> 128 + )} 129 + </p> 130 + {type === 'follow_request' && ( 131 + <FollowRequestButtons 132 + accountID={account.id} 133 + onChange={() => { 134 + loadNotifications(true); 135 + }} 136 + /> 137 + )} 138 + </> 139 + )} 140 + {_accounts?.length > 1 && ( 141 + <p class="avatars-stack"> 142 + {_accounts.map((account, i) => ( 143 + <> 144 + <a 145 + href={account.url} 146 + rel="noopener noreferrer" 147 + class="account-avatar-stack" 148 + onClick={(e) => { 149 + e.preventDefault(); 150 + states.showAccount = account; 151 + }} 152 + > 153 + <Avatar 154 + url={account.avatarStatic} 155 + size={ 156 + _accounts.length <= 10 157 + ? 'xxl' 158 + : _accounts.length < 100 159 + ? 'xl' 160 + : _accounts.length < 1000 161 + ? 'l' 162 + : _accounts.length < 2000 163 + ? 'm' 164 + : 's' // My god, this person is popular! 165 + } 166 + key={account.id} 167 + alt={`${account.displayName} @${account.acct}`} 168 + squircle={account?.bot} 169 + /> 170 + {type === 'favourite+reblog' && ( 171 + <div class="account-sub-icons"> 172 + {account._types.map((type) => ( 173 + <Icon 174 + icon={NOTIFICATION_ICONS[type]} 175 + size="s" 176 + class={`${type}-icon`} 177 + /> 178 + ))} 179 + </div> 180 + )} 181 + </a>{' '} 182 + </> 183 + ))} 184 + </p> 185 + )} 186 + {status && ( 187 + <Link 188 + class={`status-link status-type-${type}`} 189 + to={ 190 + instance 191 + ? `/${instance}/s/${actualStatusID}` 192 + : `/s/${actualStatusID}` 193 + } 194 + > 195 + <Status statusID={actualStatusID} size="s" /> 196 + </Link> 197 + )} 198 + </div> 199 + </div> 200 + ); 201 + } 202 + 203 + function FollowRequestButtons({ accountID, onChange }) { 204 + const { masto } = api(); 205 + const [uiState, setUIState] = useState('default'); 206 + return ( 207 + <p> 208 + <button 209 + type="button" 210 + disabled={uiState === 'loading'} 211 + onClick={() => { 212 + setUIState('loading'); 213 + (async () => { 214 + try { 215 + await masto.v1.followRequests.authorize(accountID); 216 + onChange(); 217 + } catch (e) { 218 + console.error(e); 219 + setUIState('default'); 220 + } 221 + })(); 222 + }} 223 + > 224 + Accept 225 + </button>{' '} 226 + <button 227 + type="button" 228 + disabled={uiState === 'loading'} 229 + class="light danger" 230 + onClick={() => { 231 + setUIState('loading'); 232 + (async () => { 233 + try { 234 + await masto.v1.followRequests.reject(accountID); 235 + onChange(); 236 + } catch (e) { 237 + console.error(e); 238 + setUIState('default'); 239 + } 240 + })(); 241 + }} 242 + > 243 + Reject 244 + </button> 245 + <Loader hidden={uiState !== 'loading'} /> 246 + </p> 247 + ); 248 + } 249 + 250 + export default Notification;
+145 -13
src/pages/home.jsx
··· 1 + import './notifications-menu.css'; 2 + 3 + import { ControlledMenu } from '@szhsin/react-menu'; 1 4 import { memo } from 'preact/compat'; 2 - import { useEffect } from 'preact/hooks'; 5 + import { useEffect, useRef, useState } from 'preact/hooks'; 3 6 import { useSnapshot } from 'valtio'; 4 7 5 8 import Columns from '../components/columns'; 6 9 import Icon from '../components/icon'; 7 10 import Link from '../components/link'; 11 + import Loader from '../components/loader'; 12 + import Notification from '../components/notification'; 13 + import { api } from '../utils/api'; 8 14 import db from '../utils/db'; 15 + import groupNotifications from '../utils/group-notifications'; 9 16 import openCompose from '../utils/open-compose'; 10 - import states from '../utils/states'; 17 + import states, { saveStatus } from '../utils/states'; 11 18 import { getCurrentAccountNS } from '../utils/store-utils'; 12 19 13 20 import Following from './following'; ··· 27 34 })(); 28 35 }, []); 29 36 37 + const notificationLinkRef = useRef(); 38 + const [menuState, setMenuState] = useState('closed'); 39 + 30 40 return ( 31 41 <> 32 42 {(snapStates.settings.shortcutsColumnsMode || ··· 40 50 id="home" 41 51 headerStart={false} 42 52 headerEnd={ 43 - <Link 44 - to="/notifications" 45 - class={`button plain notifications-button ${ 46 - snapStates.notificationsShowNew ? 'has-badge' : '' 47 - }`} 48 - onClick={(e) => { 49 - e.stopPropagation(); 50 - }} 51 - > 52 - <Icon icon="notification" size="l" alt="Notifications" /> 53 - </Link> 53 + <> 54 + <Link 55 + ref={notificationLinkRef} 56 + to="/notifications" 57 + class={`button plain notifications-button ${ 58 + snapStates.notificationsShowNew ? 'has-badge' : '' 59 + } ${menuState}`} 60 + onClick={(e) => { 61 + e.stopPropagation(); 62 + if (window.matchMedia('(min-width: calc(40em))').matches) { 63 + e.preventDefault(); 64 + setMenuState((state) => 65 + state === 'closed' ? 'open' : 'closed', 66 + ); 67 + } 68 + }} 69 + > 70 + <Icon icon="notification" size="l" alt="Notifications" /> 71 + </Link> 72 + <NotificationsMenu 73 + state={menuState} 74 + anchorRef={notificationLinkRef} 75 + onClose={() => setMenuState('closed')} 76 + /> 77 + </> 54 78 } 55 79 /> 56 80 )} ··· 73 97 <Icon icon="quill" size="xl" alt="Compose" /> 74 98 </button> 75 99 </> 100 + ); 101 + } 102 + 103 + const NOTIFICATIONS_LIMIT = 30; 104 + const NOTIFICATIONS_DISPLAY_LIMIT = 5; 105 + function NotificationsMenu({ anchorRef, state, onClose }) { 106 + const { masto, instance } = api(); 107 + const snapStates = useSnapshot(states); 108 + const [uiState, setUIState] = useState('default'); 109 + 110 + const notificationsIterator = masto.v1.notifications.list({ 111 + limit: NOTIFICATIONS_LIMIT, 112 + }); 113 + 114 + async function fetchNotifications() { 115 + const allNotifications = await notificationsIterator.next(); 116 + const notifications = allNotifications.value; 117 + 118 + if (notifications?.length) { 119 + notifications.forEach((notification) => { 120 + saveStatus(notification.status, instance, { 121 + skipThreading: true, 122 + }); 123 + }); 124 + 125 + const groupedNotifications = groupNotifications(notifications); 126 + 127 + states.notificationsLast = notifications[0]; 128 + states.notifications = groupedNotifications; 129 + } 130 + 131 + states.notificationsShowNew = false; 132 + states.notificationsLastFetchTime = Date.now(); 133 + return allNotifications; 134 + } 135 + 136 + function loadNotifications() { 137 + setUIState('loading'); 138 + (async () => { 139 + try { 140 + await fetchNotifications(); 141 + setUIState('default'); 142 + } catch (e) { 143 + setUIState('error'); 144 + } 145 + })(); 146 + } 147 + 148 + useEffect(() => { 149 + loadNotifications(); 150 + }, []); 151 + 152 + return ( 153 + <ControlledMenu 154 + menuClassName="notifications-menu" 155 + state={state} 156 + anchorRef={anchorRef} 157 + onClose={onClose} 158 + portal={{ 159 + target: document.body, 160 + }} 161 + overflow="auto" 162 + viewScroll="close" 163 + position="anchor" 164 + align="center" 165 + boundingBoxPadding="8 8 8 8" 166 + unmountOnClose 167 + > 168 + <header> 169 + <h2>Notifications</h2> 170 + </header> 171 + {snapStates.notifications.length ? ( 172 + <> 173 + {snapStates.notifications 174 + .slice(0, NOTIFICATIONS_DISPLAY_LIMIT) 175 + .map((notification) => ( 176 + <Notification 177 + key={notification.id} 178 + instance={instance} 179 + notification={notification} 180 + /> 181 + ))} 182 + </> 183 + ) : uiState === 'loading' ? ( 184 + <div class="ui-state"> 185 + <Loader abrupt /> 186 + </div> 187 + ) : ( 188 + uiState === 'error' && ( 189 + <div class="ui-state"> 190 + <p>Unable to fetch notifications.</p> 191 + <p> 192 + <button type="button" onClick={loadNotifications}> 193 + Try again 194 + </button> 195 + </p> 196 + </div> 197 + ) 198 + )} 199 + <footer> 200 + <Link to="/mentions" class="button plain"> 201 + <Icon icon="at" /> <span>Mentions</span> 202 + </Link> 203 + <Link to="/notifications" class="button plain2"> 204 + <b>See all</b> <Icon icon="arrow-right" /> 205 + </Link> 206 + </footer> 207 + </ControlledMenu> 76 208 ); 77 209 } 78 210
+51
src/pages/notifications-menu.css
··· 1 + @keyframes bell { 2 + 0% { 3 + transform: rotate(0deg); 4 + } 5 + 33% { 6 + transform: rotate(5deg); 7 + } 8 + 66% { 9 + transform: rotate(-10deg); 10 + } 11 + 100% { 12 + transform: rotate(0deg); 13 + } 14 + } 15 + .notifications-button.open { 16 + animation: bell 0.3s ease-out both; 17 + transform-origin: 50% 0; 18 + } 19 + 20 + .notifications-menu { 21 + width: 23em; 22 + font-size: 90%; 23 + padding: 0; 24 + height: 40em; 25 + overflow: auto; 26 + } 27 + .notifications-menu header { 28 + padding: 16px; 29 + margin: 0; 30 + border-bottom: var(--hairline-width) solid var(--outline-color); 31 + } 32 + .notifications-menu header h2 { 33 + margin: 0; 34 + padding: 0; 35 + font-size: 1.2em; 36 + } 37 + .notifications-menu .notification { 38 + animation: appear-smooth 0.3s ease-out 0.1s both; 39 + } 40 + .notifications-menu footer { 41 + animation: slide-up 0.3s ease-out 0.2s both; 42 + position: sticky; 43 + bottom: 0; 44 + border-top: var(--hairline-width) solid var(--outline-color); 45 + background-color: var(--bg-blur-color); 46 + backdrop-filter: blur(16px); 47 + padding: 16px; 48 + gap: 8px; 49 + display: flex; 50 + justify-content: space-between; 51 + }
+2 -285
src/pages/notifications.jsx
··· 4 4 import { useEffect, useRef, useState } from 'preact/hooks'; 5 5 import { useSnapshot } from 'valtio'; 6 6 7 - import Avatar from '../components/avatar'; 8 7 import Icon from '../components/icon'; 9 8 import Link from '../components/link'; 10 9 import Loader from '../components/loader'; 11 - import NameText from '../components/name-text'; 12 10 import NavMenu from '../components/nav-menu'; 13 - import RelativeTime from '../components/relative-time'; 14 - import Status from '../components/status'; 11 + import Notification from '../components/notification'; 15 12 import { api } from '../utils/api'; 13 + import groupNotifications from '../utils/group-notifications'; 16 14 import niceDateTime from '../utils/nice-date-time'; 17 15 import states, { saveStatus } from '../utils/states'; 18 - import store from '../utils/store'; 19 16 import useScroll from '../utils/useScroll'; 20 17 import useTitle from '../utils/useTitle'; 21 - 22 - /* 23 - Notification types 24 - ================== 25 - mention = Someone mentioned you in their status 26 - status = Someone you enabled notifications for has posted a status 27 - reblog = Someone boosted one of your statuses 28 - follow = Someone followed you 29 - follow_request = Someone requested to follow you 30 - favourite = Someone favourited one of your statuses 31 - poll = A poll you have voted in or created has ended 32 - update = A status you interacted with has been edited 33 - admin.sign_up = Someone signed up (optionally sent to admins) 34 - admin.report = A new report has been filed 35 - */ 36 - 37 - const contentText = { 38 - mention: 'mentioned you in their post.', 39 - status: 'published a post.', 40 - reblog: 'boosted your post.', 41 - follow: 'followed you.', 42 - follow_request: 'requested to follow you.', 43 - favourite: 'favourited your post.', 44 - poll: 'A poll you have voted in or created has ended.', 45 - 'poll-self': 'A poll you have created has ended.', 46 - 'poll-voted': 'A poll you have voted in has ended.', 47 - update: 'A post you interacted with has been edited.', 48 - 'favourite+reblog': 'boosted & favourited your post.', 49 - }; 50 - 51 - const NOTIFICATION_ICONS = { 52 - mention: 'comment', 53 - status: 'notification', 54 - reblog: 'rocket', 55 - follow: 'follow', 56 - follow_request: 'follow-add', 57 - favourite: 'heart', 58 - poll: 'poll', 59 - update: 'pencil', 60 - }; 61 18 62 19 const LIMIT = 30; // 30 is the maximum limit :( 63 20 ··· 286 243 </div> 287 244 </div> 288 245 ); 289 - } 290 - function Notification({ notification, instance }) { 291 - const { id, status, account, _accounts } = notification; 292 - let { type } = notification; 293 - 294 - // status = Attached when type of the notification is favourite, reblog, status, mention, poll, or update 295 - const actualStatusID = status?.reblog?.id || status?.id; 296 - 297 - const currentAccount = store.session.get('currentAccount'); 298 - const isSelf = currentAccount === account?.id; 299 - const isVoted = status?.poll?.voted; 300 - 301 - let favsCount = 0; 302 - let reblogsCount = 0; 303 - if (type === 'favourite+reblog') { 304 - for (const account of _accounts) { 305 - if (account._types?.includes('favourite')) { 306 - favsCount++; 307 - } 308 - if (account._types?.includes('reblog')) { 309 - reblogsCount++; 310 - } 311 - } 312 - if (!reblogsCount && favsCount) type = 'favourite'; 313 - if (!favsCount && reblogsCount) type = 'reblog'; 314 - } 315 - 316 - const text = 317 - type === 'poll' 318 - ? contentText[isSelf ? 'poll-self' : isVoted ? 'poll-voted' : 'poll'] 319 - : contentText[type]; 320 - 321 - return ( 322 - <div class={`notification notification-${type}`} tabIndex="0"> 323 - <div 324 - class={`notification-type notification-${type}`} 325 - title={new Date(notification.createdAt).toLocaleString()} 326 - > 327 - {type === 'favourite+reblog' ? ( 328 - <> 329 - <Icon icon="rocket" size="xl" alt={type} class="reblog-icon" /> 330 - <Icon icon="heart" size="xl" alt={type} class="favourite-icon" /> 331 - </> 332 - ) : ( 333 - <Icon 334 - icon={NOTIFICATION_ICONS[type] || 'notification'} 335 - size="xl" 336 - alt={type} 337 - /> 338 - )} 339 - </div> 340 - <div class="notification-content"> 341 - {type !== 'mention' && ( 342 - <> 343 - <p> 344 - {!/poll|update/i.test(type) && ( 345 - <> 346 - {_accounts?.length > 1 ? ( 347 - <> 348 - <b>{_accounts.length} people</b>{' '} 349 - </> 350 - ) : ( 351 - <> 352 - <NameText account={account} showAvatar />{' '} 353 - </> 354 - )} 355 - </> 356 - )} 357 - {text} 358 - {type === 'mention' && ( 359 - <span class="insignificant"> 360 - {' '} 361 - •{' '} 362 - <RelativeTime 363 - datetime={notification.createdAt} 364 - format="micro" 365 - /> 366 - </span> 367 - )} 368 - </p> 369 - {type === 'follow_request' && ( 370 - <FollowRequestButtons 371 - accountID={account.id} 372 - onChange={() => { 373 - loadNotifications(true); 374 - }} 375 - /> 376 - )} 377 - </> 378 - )} 379 - {_accounts?.length > 1 && ( 380 - <p class="avatars-stack"> 381 - {_accounts.map((account, i) => ( 382 - <> 383 - <a 384 - href={account.url} 385 - rel="noopener noreferrer" 386 - class="account-avatar-stack" 387 - onClick={(e) => { 388 - e.preventDefault(); 389 - states.showAccount = account; 390 - }} 391 - > 392 - <Avatar 393 - url={account.avatarStatic} 394 - size={ 395 - _accounts.length <= 10 396 - ? 'xxl' 397 - : _accounts.length < 100 398 - ? 'xl' 399 - : _accounts.length < 1000 400 - ? 'l' 401 - : _accounts.length < 2000 402 - ? 'm' 403 - : 's' // My god, this person is popular! 404 - } 405 - key={account.id} 406 - alt={`${account.displayName} @${account.acct}`} 407 - squircle={account?.bot} 408 - /> 409 - {type === 'favourite+reblog' && ( 410 - <div class="account-sub-icons"> 411 - {account._types.map((type) => ( 412 - <Icon 413 - icon={NOTIFICATION_ICONS[type]} 414 - size="s" 415 - class={`${type}-icon`} 416 - /> 417 - ))} 418 - </div> 419 - )} 420 - </a>{' '} 421 - </> 422 - ))} 423 - </p> 424 - )} 425 - {status && ( 426 - <Link 427 - class={`status-link status-type-${type}`} 428 - to={ 429 - instance 430 - ? `/${instance}/s/${actualStatusID}` 431 - : `/s/${actualStatusID}` 432 - } 433 - > 434 - <Status statusID={actualStatusID} size="s" /> 435 - </Link> 436 - )} 437 - </div> 438 - </div> 439 - ); 440 - } 441 - 442 - function FollowRequestButtons({ accountID, onChange }) { 443 - const { masto } = api(); 444 - const [uiState, setUIState] = useState('default'); 445 - return ( 446 - <p> 447 - <button 448 - type="button" 449 - disabled={uiState === 'loading'} 450 - onClick={() => { 451 - setUIState('loading'); 452 - (async () => { 453 - try { 454 - await masto.v1.followRequests.authorize(accountID); 455 - onChange(); 456 - } catch (e) { 457 - console.error(e); 458 - setUIState('default'); 459 - } 460 - })(); 461 - }} 462 - > 463 - Accept 464 - </button>{' '} 465 - <button 466 - type="button" 467 - disabled={uiState === 'loading'} 468 - class="light danger" 469 - onClick={() => { 470 - setUIState('loading'); 471 - (async () => { 472 - try { 473 - await masto.v1.followRequests.reject(accountID); 474 - onChange(); 475 - } catch (e) { 476 - console.error(e); 477 - setUIState('default'); 478 - } 479 - })(); 480 - }} 481 - > 482 - Reject 483 - </button> 484 - <Loader hidden={uiState !== 'loading'} /> 485 - </p> 486 - ); 487 - } 488 - 489 - function groupNotifications(notifications) { 490 - // Create new flat list of notifications 491 - // Combine sibling notifications based on type and status id 492 - // Concat all notification.account into an array of _accounts 493 - const notificationsMap = {}; 494 - const cleanNotifications = []; 495 - for (let i = 0, j = 0; i < notifications.length; i++) { 496 - const notification = notifications[i]; 497 - const { status, account, type, createdAt } = notification; 498 - const date = new Date(createdAt).toLocaleDateString(); 499 - let virtualType = type; 500 - if (type === 'favourite' || type === 'reblog') { 501 - virtualType = 'favourite+reblog'; 502 - } 503 - const key = `${status?.id}-${virtualType}-${date}`; 504 - const mappedNotification = notificationsMap[key]; 505 - if (virtualType === 'follow_request') { 506 - cleanNotifications[j++] = notification; 507 - } else if (mappedNotification?.account) { 508 - const mappedAccount = mappedNotification._accounts.find( 509 - (a) => a.id === account.id, 510 - ); 511 - if (mappedAccount) { 512 - mappedAccount._types.push(type); 513 - mappedAccount._types.sort().reverse(); 514 - } else { 515 - account._types = [type]; 516 - mappedNotification._accounts.push(account); 517 - } 518 - } else { 519 - account._types = [type]; 520 - let n = (notificationsMap[key] = { 521 - ...notification, 522 - type: virtualType, 523 - _accounts: [account], 524 - }); 525 - cleanNotifications[j++] = n; 526 - } 527 - } 528 - return cleanNotifications; 529 246 } 530 247 531 248 export default memo(Notifications);
+43
src/utils/group-notifications.jsx
··· 1 + function groupNotifications(notifications) { 2 + // Create new flat list of notifications 3 + // Combine sibling notifications based on type and status id 4 + // Concat all notification.account into an array of _accounts 5 + const notificationsMap = {}; 6 + const cleanNotifications = []; 7 + for (let i = 0, j = 0; i < notifications.length; i++) { 8 + const notification = notifications[i]; 9 + const { status, account, type, createdAt } = notification; 10 + const date = new Date(createdAt).toLocaleDateString(); 11 + let virtualType = type; 12 + if (type === 'favourite' || type === 'reblog') { 13 + virtualType = 'favourite+reblog'; 14 + } 15 + const key = `${status?.id}-${virtualType}-${date}`; 16 + const mappedNotification = notificationsMap[key]; 17 + if (virtualType === 'follow_request') { 18 + cleanNotifications[j++] = notification; 19 + } else if (mappedNotification?.account) { 20 + const mappedAccount = mappedNotification._accounts.find( 21 + (a) => a.id === account.id, 22 + ); 23 + if (mappedAccount) { 24 + mappedAccount._types.push(type); 25 + mappedAccount._types.sort().reverse(); 26 + } else { 27 + account._types = [type]; 28 + mappedNotification._accounts.push(account); 29 + } 30 + } else { 31 + account._types = [type]; 32 + let n = (notificationsMap[key] = { 33 + ...notification, 34 + type: virtualType, 35 + _accounts: [account], 36 + }); 37 + cleanNotifications[j++] = n; 38 + } 39 + } 40 + return cleanNotifications; 41 + } 42 + 43 + export default groupNotifications;