this repo has no description
0
fork

Configure Feed

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

Refactor code to files

+281 -264
+6 -264
src/app.jsx
··· 13 13 Routes, 14 14 useLocation, 15 15 useNavigate, 16 - useParams, 17 16 } from 'react-router-dom'; 18 17 import 'swiped-events'; 19 18 import { useSnapshot } from 'valtio'; 20 19 21 20 import AccountSheet from './components/account-sheet'; 21 + import BackgroundService from './components/background-service'; 22 22 import Compose from './components/compose'; 23 23 import Drafts from './components/drafts'; 24 24 import Icon, { ICONS } from './components/icon'; 25 - import Link from './components/link'; 26 25 import Loader from './components/loader'; 27 26 import MediaModal from './components/media-modal'; 28 27 import Modal from './components/modal'; 29 - import Notification from './components/notification'; 28 + import NotificationService from './components/notification-service'; 30 29 import Shortcuts from './components/shortcuts'; 31 30 import ShortcutsSettings from './components/shortcuts-settings'; 32 31 import NotFound from './pages/404'; ··· 38 37 import Following from './pages/following'; 39 38 import Hashtag from './pages/hashtag'; 40 39 import Home from './pages/home'; 41 - import HttpRoute from './pages/HttpRoute'; 40 + import HttpRoute from './pages/http-route'; 42 41 import List from './pages/list'; 43 42 import Lists from './pages/lists'; 44 43 import Login from './pages/login'; ··· 47 46 import Public from './pages/public'; 48 47 import Search from './pages/search'; 49 48 import Settings from './pages/settings'; 50 - import Status from './pages/status'; 49 + import StatusRoute from './pages/status-route'; 51 50 import Trending from './pages/trending'; 52 51 import Welcome from './pages/welcome'; 53 52 import { ··· 60 59 import { getAccessToken } from './utils/auth'; 61 60 import openCompose from './utils/open-compose'; 62 61 import showToast from './utils/show-toast'; 63 - import states, { initStates, saveStatus } from './utils/states'; 62 + import states, { initStates } from './utils/states'; 64 63 import store from './utils/store'; 65 - import { 66 - getAccountByAccessToken, 67 - getCurrentAccount, 68 - } from './utils/store-utils'; 64 + import { getCurrentAccount } from './utils/store-utils'; 69 65 import './utils/toast-alert'; 70 - import useInterval from './utils/useInterval'; 71 - import usePageVisibility from './utils/usePageVisibility'; 72 66 73 67 window.__STATES__ = states; 74 68 ··· 457 451 <BackgroundService isLoggedIn={isLoggedIn} /> 458 452 </> 459 453 ); 460 - } 461 - 462 - function BackgroundService({ isLoggedIn }) { 463 - // Notifications service 464 - // - WebSocket to receive notifications when page is visible 465 - const [visible, setVisible] = useState(true); 466 - usePageVisibility(setVisible); 467 - const notificationStream = useRef(); 468 - useEffect(() => { 469 - if (isLoggedIn && visible) { 470 - const { masto, instance } = api(); 471 - (async () => { 472 - // 1. Get the latest notification 473 - if (states.notificationsLast) { 474 - const notificationsIterator = masto.v1.notifications.list({ 475 - limit: 1, 476 - since_id: states.notificationsLast.id, 477 - }); 478 - const { value: notifications } = await notificationsIterator.next(); 479 - if (notifications?.length) { 480 - states.notificationsShowNew = true; 481 - } 482 - } 483 - 484 - // 2. Start streaming 485 - notificationStream.current = await masto.ws.stream( 486 - '/api/v1/streaming', 487 - { 488 - stream: 'user:notification', 489 - }, 490 - ); 491 - console.log('🎏 Streaming notification', notificationStream.current); 492 - 493 - notificationStream.current.on('notification', (notification) => { 494 - console.log('🔔🔔 Notification', notification); 495 - if (notification.status) { 496 - saveStatus(notification.status, instance, { 497 - skipThreading: true, 498 - }); 499 - } 500 - states.notificationsShowNew = true; 501 - }); 502 - 503 - notificationStream.current.ws.onclose = () => { 504 - console.log('🔔🔔 Notification stream closed'); 505 - }; 506 - })(); 507 - } 508 - return () => { 509 - if (notificationStream.current) { 510 - notificationStream.current.ws.close(); 511 - notificationStream.current = null; 512 - } 513 - }; 514 - }, [visible, isLoggedIn]); 515 - 516 - // Check for updates service 517 - const lastCheckDate = useRef(); 518 - const checkForUpdates = () => { 519 - lastCheckDate.current = Date.now(); 520 - console.log('✨ Check app update'); 521 - fetch('./version.json') 522 - .then((r) => r.json()) 523 - .then((info) => { 524 - if (info) states.appVersion = info; 525 - }) 526 - .catch((e) => { 527 - console.error(e); 528 - }); 529 - }; 530 - useInterval(checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes 531 - usePageVisibility((visible) => { 532 - if (visible) { 533 - if (!lastCheckDate.current) { 534 - checkForUpdates(); 535 - } else { 536 - const diff = Date.now() - lastCheckDate.current; 537 - if (diff > 1000 * 60 * 60) { 538 - // 1 hour 539 - checkForUpdates(); 540 - } 541 - } 542 - } 543 - }); 544 - 545 - return null; 546 - } 547 - 548 - function NotificationService() { 549 - if (!('serviceWorker' in navigator)) return null; 550 - 551 - const snapStates = useSnapshot(states); 552 - const { routeNotification } = snapStates; 553 - 554 - console.log('🛎️ Notification service', routeNotification); 555 - 556 - const { id, accessToken } = routeNotification || {}; 557 - const [showNotificationSheet, setShowNotificationSheet] = useState(false); 558 - 559 - useLayoutEffect(() => { 560 - if (!id || !accessToken) return; 561 - const { instance: currentInstance } = api(); 562 - const { masto, instance } = api({ 563 - accessToken, 564 - }); 565 - console.log('API', { accessToken, currentInstance, instance }); 566 - const sameInstance = currentInstance === instance; 567 - const account = accessToken 568 - ? getAccountByAccessToken(accessToken) 569 - : getCurrentAccount(); 570 - (async () => { 571 - const notification = await masto.v1.notifications.fetch(id); 572 - if (notification && account) { 573 - console.log('🛎️ Notification', { id, notification, account }); 574 - const accountInstance = account.instanceURL; 575 - const { type, status, account: notificationAccount } = notification; 576 - const hasModal = !!document.querySelector('#modal-container > *'); 577 - const isFollow = type === 'follow' && !!notificationAccount?.id; 578 - const hasAccount = !!notificationAccount?.id; 579 - const hasStatus = !!status?.id; 580 - if (isFollow && sameInstance) { 581 - // Show account sheet, can handle different instances 582 - states.showAccount = { 583 - account: notificationAccount, 584 - instance: accountInstance, 585 - }; 586 - } else if (hasModal || !sameInstance || (hasAccount && hasStatus)) { 587 - // Show sheet of notification, if 588 - // - there is a modal open 589 - // - the notification is from another instance 590 - // - the notification has both account and status, gives choice for users to go to account or status 591 - setShowNotificationSheet({ 592 - id, 593 - account, 594 - notification, 595 - sameInstance, 596 - }); 597 - } else { 598 - if (hasStatus) { 599 - // Go to status page 600 - location.hash = `/${currentInstance}/s/${status.id}`; 601 - } else if (isFollow) { 602 - // Go to profile page 603 - location.hash = `/${currentInstance}/a/${notificationAccount.id}`; 604 - } else { 605 - // Go to notifications page 606 - location.hash = '/notifications'; 607 - } 608 - } 609 - } else { 610 - console.warn( 611 - '🛎️ Notification not found', 612 - notificationID, 613 - notificationAccessToken, 614 - ); 615 - } 616 - })(); 617 - }, [id, accessToken]); 618 - 619 - useLayoutEffect(() => { 620 - // Listen to message from service worker 621 - const handleMessage = (event) => { 622 - console.log('💥💥💥 Message event', event); 623 - const { type, id, accessToken } = event?.data || {}; 624 - if (type === 'notification') { 625 - states.routeNotification = { 626 - id, 627 - accessToken, 628 - }; 629 - } 630 - }; 631 - console.log('👂👂👂 Listen to message'); 632 - navigator.serviceWorker.addEventListener('message', handleMessage); 633 - return () => { 634 - console.log('👂👂👂 Remove listen to message'); 635 - navigator.serviceWorker.removeEventListener('message', handleMessage); 636 - }; 637 - }, []); 638 - 639 - const onClose = () => { 640 - setShowNotificationSheet(false); 641 - states.routeNotification = null; 642 - 643 - // If url is #/notifications?id=123, go to #/notifications 644 - if (/\/notifications\?id=/i.test(location.hash)) { 645 - location.hash = '/notifications'; 646 - } 647 - }; 648 - 649 - if (showNotificationSheet) { 650 - const { id, account, notification, sameInstance } = showNotificationSheet; 651 - return ( 652 - <Modal 653 - class="light" 654 - onClick={(e) => { 655 - if (e.target === e.currentTarget) { 656 - onClose(); 657 - } 658 - }} 659 - > 660 - <div class="sheet" tabIndex="-1"> 661 - <button type="button" class="sheet-close" onClick={onClose}> 662 - <Icon icon="x" /> 663 - </button> 664 - <header> 665 - <b>Notification</b> 666 - </header> 667 - <main> 668 - {!sameInstance && ( 669 - <p>This notification is from your other account.</p> 670 - )} 671 - <div 672 - class="notification-peek" 673 - // style={{ 674 - // pointerEvents: sameInstance ? '' : 'none', 675 - // }} 676 - onClick={(e) => { 677 - const { target } = e; 678 - // If button or links 679 - if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') { 680 - onClose(); 681 - } 682 - }} 683 - > 684 - <Notification 685 - instance={account.instanceURL} 686 - notification={notification} 687 - isStatic 688 - /> 689 - </div> 690 - <div 691 - style={{ 692 - textAlign: 'end', 693 - }} 694 - > 695 - <Link to="/notifications" class="button light"> 696 - <span>View all notifications</span> <Icon icon="arrow-right" /> 697 - </Link> 698 - </div> 699 - </main> 700 - </div> 701 - </Modal> 702 - ); 703 - } 704 - 705 - return null; 706 - } 707 - 708 - function StatusRoute() { 709 - const params = useParams(); 710 - const { id, instance } = params; 711 - return <Status id={id} instance={instance} />; 712 454 } 713 455 714 456 export { App };
+92
src/components/background-service.jsx
··· 1 + import { useEffect, useRef, useState } from 'preact/hooks'; 2 + 3 + import { api } from '../utils/api'; 4 + import states, { saveStatus } from '../utils/states'; 5 + import useInterval from '../utils/useInterval'; 6 + import usePageVisibility from '../utils/usePageVisibility'; 7 + 8 + export default function BackgroundService({ isLoggedIn }) { 9 + // Notifications service 10 + // - WebSocket to receive notifications when page is visible 11 + const [visible, setVisible] = useState(true); 12 + usePageVisibility(setVisible); 13 + const notificationStream = useRef(); 14 + useEffect(() => { 15 + if (isLoggedIn && visible) { 16 + const { masto, instance } = api(); 17 + (async () => { 18 + // 1. Get the latest notification 19 + if (states.notificationsLast) { 20 + const notificationsIterator = masto.v1.notifications.list({ 21 + limit: 1, 22 + since_id: states.notificationsLast.id, 23 + }); 24 + const { value: notifications } = await notificationsIterator.next(); 25 + if (notifications?.length) { 26 + states.notificationsShowNew = true; 27 + } 28 + } 29 + 30 + // 2. Start streaming 31 + notificationStream.current = await masto.ws.stream( 32 + '/api/v1/streaming', 33 + { 34 + stream: 'user:notification', 35 + }, 36 + ); 37 + console.log('🎏 Streaming notification', notificationStream.current); 38 + 39 + notificationStream.current.on('notification', (notification) => { 40 + console.log('🔔🔔 Notification', notification); 41 + if (notification.status) { 42 + saveStatus(notification.status, instance, { 43 + skipThreading: true, 44 + }); 45 + } 46 + states.notificationsShowNew = true; 47 + }); 48 + 49 + notificationStream.current.ws.onclose = () => { 50 + console.log('🔔🔔 Notification stream closed'); 51 + }; 52 + })(); 53 + } 54 + return () => { 55 + if (notificationStream.current) { 56 + notificationStream.current.ws.close(); 57 + notificationStream.current = null; 58 + } 59 + }; 60 + }, [visible, isLoggedIn]); 61 + 62 + // Check for updates service 63 + const lastCheckDate = useRef(); 64 + const checkForUpdates = () => { 65 + lastCheckDate.current = Date.now(); 66 + console.log('✨ Check app update'); 67 + fetch('./version.json') 68 + .then((r) => r.json()) 69 + .then((info) => { 70 + if (info) states.appVersion = info; 71 + }) 72 + .catch((e) => { 73 + console.error(e); 74 + }); 75 + }; 76 + useInterval(checkForUpdates, visible && 1000 * 60 * 30); // 30 minutes 77 + usePageVisibility((visible) => { 78 + if (visible) { 79 + if (!lastCheckDate.current) { 80 + checkForUpdates(); 81 + } else { 82 + const diff = Date.now() - lastCheckDate.current; 83 + if (diff > 1000 * 60 * 60) { 84 + // 1 hour 85 + checkForUpdates(); 86 + } 87 + } 88 + } 89 + }); 90 + 91 + return null; 92 + }
+174
src/components/notification-service.jsx
··· 1 + import { useLayoutEffect, useState } from 'preact/hooks'; 2 + import { useSnapshot } from 'valtio'; 3 + 4 + import { api } from '../utils/api'; 5 + import states from '../utils/states'; 6 + import { 7 + getAccountByAccessToken, 8 + getCurrentAccount, 9 + } from '../utils/store-utils'; 10 + 11 + import Icon from './icon'; 12 + import Link from './link'; 13 + import Modal from './modal'; 14 + import Notification from './notification'; 15 + 16 + export default function NotificationService() { 17 + if (!('serviceWorker' in navigator)) return null; 18 + 19 + const snapStates = useSnapshot(states); 20 + const { routeNotification } = snapStates; 21 + 22 + console.log('🛎️ Notification service', routeNotification); 23 + 24 + const { id, accessToken } = routeNotification || {}; 25 + const [showNotificationSheet, setShowNotificationSheet] = useState(false); 26 + 27 + useLayoutEffect(() => { 28 + if (!id || !accessToken) return; 29 + const { instance: currentInstance } = api(); 30 + const { masto, instance } = api({ 31 + accessToken, 32 + }); 33 + console.log('API', { accessToken, currentInstance, instance }); 34 + const sameInstance = currentInstance === instance; 35 + const account = accessToken 36 + ? getAccountByAccessToken(accessToken) 37 + : getCurrentAccount(); 38 + (async () => { 39 + const notification = await masto.v1.notifications.fetch(id); 40 + if (notification && account) { 41 + console.log('🛎️ Notification', { id, notification, account }); 42 + const accountInstance = account.instanceURL; 43 + const { type, status, account: notificationAccount } = notification; 44 + const hasModal = !!document.querySelector('#modal-container > *'); 45 + const isFollow = type === 'follow' && !!notificationAccount?.id; 46 + const hasAccount = !!notificationAccount?.id; 47 + const hasStatus = !!status?.id; 48 + if (isFollow && sameInstance) { 49 + // Show account sheet, can handle different instances 50 + states.showAccount = { 51 + account: notificationAccount, 52 + instance: accountInstance, 53 + }; 54 + } else if (hasModal || !sameInstance || (hasAccount && hasStatus)) { 55 + // Show sheet of notification, if 56 + // - there is a modal open 57 + // - the notification is from another instance 58 + // - the notification has both account and status, gives choice for users to go to account or status 59 + setShowNotificationSheet({ 60 + id, 61 + account, 62 + notification, 63 + sameInstance, 64 + }); 65 + } else { 66 + if (hasStatus) { 67 + // Go to status page 68 + location.hash = `/${currentInstance}/s/${status.id}`; 69 + } else if (isFollow) { 70 + // Go to profile page 71 + location.hash = `/${currentInstance}/a/${notificationAccount.id}`; 72 + } else { 73 + // Go to notifications page 74 + location.hash = '/notifications'; 75 + } 76 + } 77 + } else { 78 + console.warn( 79 + '🛎️ Notification not found', 80 + notificationID, 81 + notificationAccessToken, 82 + ); 83 + } 84 + })(); 85 + }, [id, accessToken]); 86 + 87 + useLayoutEffect(() => { 88 + // Listen to message from service worker 89 + const handleMessage = (event) => { 90 + console.log('💥💥💥 Message event', event); 91 + const { type, id, accessToken } = event?.data || {}; 92 + if (type === 'notification') { 93 + states.routeNotification = { 94 + id, 95 + accessToken, 96 + }; 97 + } 98 + }; 99 + console.log('👂👂👂 Listen to message'); 100 + navigator.serviceWorker.addEventListener('message', handleMessage); 101 + return () => { 102 + console.log('👂👂👂 Remove listen to message'); 103 + navigator.serviceWorker.removeEventListener('message', handleMessage); 104 + }; 105 + }, []); 106 + 107 + const onClose = () => { 108 + setShowNotificationSheet(false); 109 + states.routeNotification = null; 110 + 111 + // If url is #/notifications?id=123, go to #/notifications 112 + if (/\/notifications\?id=/i.test(location.hash)) { 113 + location.hash = '/notifications'; 114 + } 115 + }; 116 + 117 + if (showNotificationSheet) { 118 + const { id, account, notification, sameInstance } = showNotificationSheet; 119 + return ( 120 + <Modal 121 + class="light" 122 + onClick={(e) => { 123 + if (e.target === e.currentTarget) { 124 + onClose(); 125 + } 126 + }} 127 + > 128 + <div class="sheet" tabIndex="-1"> 129 + <button type="button" class="sheet-close" onClick={onClose}> 130 + <Icon icon="x" /> 131 + </button> 132 + <header> 133 + <b>Notification</b> 134 + </header> 135 + <main> 136 + {!sameInstance && ( 137 + <p>This notification is from your other account.</p> 138 + )} 139 + <div 140 + class="notification-peek" 141 + // style={{ 142 + // pointerEvents: sameInstance ? '' : 'none', 143 + // }} 144 + onClick={(e) => { 145 + const { target } = e; 146 + // If button or links 147 + if (e.target.tagName === 'BUTTON' || e.target.tagName === 'A') { 148 + onClose(); 149 + } 150 + }} 151 + > 152 + <Notification 153 + instance={account.instanceURL} 154 + notification={notification} 155 + isStatic 156 + /> 157 + </div> 158 + <div 159 + style={{ 160 + textAlign: 'end', 161 + }} 162 + > 163 + <Link to="/notifications" class="button light"> 164 + <span>View all notifications</span> <Icon icon="arrow-right" /> 165 + </Link> 166 + </div> 167 + </main> 168 + </div> 169 + </Modal> 170 + ); 171 + } 172 + 173 + return null; 174 + }
src/pages/HttpRoute.jsx src/pages/http-route.jsx
+9
src/pages/status-route.jsx
··· 1 + import { useParams } from 'react-router-dom'; 2 + 3 + import Status from './status'; 4 + 5 + export default function StatusRoute() { 6 + const params = useParams(); 7 + const { id, instance } = params; 8 + return <Status id={id} instance={instance} />; 9 + }