diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 29297965..3bbdc003 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -79,6 +79,7 @@ const ICONS = { react: 'mingcute:react-line', layout4: 'mingcute:layout-4-line', layout5: 'mingcute:layout-5-line', + announce: 'mingcute:announcement-line', }; const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js'); diff --git a/src/pages/notifications.css b/src/pages/notifications.css index 3f399ccc..f4ee59a0 100644 --- a/src/pages/notifications.css +++ b/src/pages/notifications.css @@ -3,6 +3,7 @@ padding: 16px !important; gap: 12px; animation: appear 0.2s ease-out; + clear: both; } .notification.notification-mention { margin-top: 16px; @@ -153,6 +154,8 @@ background-color: var(--bg-color); } +/* FOLLOW REQUESTS */ + .follow-requests { padding-block-end: 16px; } @@ -190,3 +193,136 @@ .follow-requests ul li .follow-request-buttons .loader-container { order: -1; } + +/* ANNOUNCEMENTS */ + +.announcements { + border: 1px solid var(--outline-color); + background-color: var(--bg-blur-color); + border-radius: 16px; + margin: 8px; + overflow: hidden; +} +.announcements summary { + list-style: none; + padding: 8px 16px; + cursor: pointer; + display: flex; + gap: 8px; + align-items: center; + justify-content: space-between; + user-select: none; + flex-wrap: wrap; +} +.announcements summary .announcement-icon { + color: var(--red-color); +} +.announcements[open] summary { + background-color: var(--bg-faded-color); +} +.announcements summary > span { + display: flex; + align-items: center; + gap: 8px; +} +@keyframes wiggle { + 0% { + transform: rotate(0deg); + } + 25% { + transform: rotate(-25deg) scale(1.1); + } + 50% { + transform: rotate(5deg); + } + 75% { + transform: rotate(-15deg); + } + 100% { + transform: rotate(0deg); + } +} +.announcements summary .announcements-nav-buttons { + transition: all 0.2s ease-in-out; + opacity: 0; + pointer-events: none; + display: none; +} +.announcements[open] summary .announcements-nav-buttons { + display: flex; + opacity: 1; + pointer-events: auto; +} +.announcements summary:hover .announcement-icon { + animation: wiggle 0.5s 1; +} +.announcements:not([open]):hover { + background-color: var(--bg-faded-color); +} +.announcements[open] summary { + color: var(--text-color); +} +.announcements summary::-webkit-details-marker { + display: none; +} +.announcements > ul { + display: flex; + overflow-x: auto; + overflow-y: hidden; + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + margin: 0; + padding: 8px; + gap: 8px; + background-color: var(--bg-faded-color); +} +.announcements > ul > li { + background-color: var(--bg-color); + scroll-snap-align: center; + scroll-snap-stop: always; + flex-shrink: 0; + display: flex; + width: 100%; + list-style: none; + margin: 0; + padding: 0; + position: relative; + border-radius: 8px; + box-shadow: 0 8px 16px -4px var(--drop-shadow-color); +} +.announcements > ul.announcements-list-multiple > li { + width: calc(100% - 16px); +} +.announcements > ul > li:last-child { + border-right: none; +} +.announcements .announcement-block { + padding: 16px; + max-height: 50vh; + max-height: 50dvh; + overflow: auto; + mask-image: linear-gradient( + to top, + transparent 1px, + black 48px, + black calc(100% - 16px), + transparent calc(100% - 1px) + ); +} +.announcements .announcement-content { + line-height: 1.4; +} +.announcements .announcement-content p { + margin-block: min(0.75em, 12px); + white-space: pre-wrap; + tab-size: 2; +} +.announcements .announcement-reactions:not(:hidden) { + display: flex; + flex-wrap: wrap; + gap: 8px; +} +.announcements .announcement-reactions button.reacted { + color: var(--text-color); + background-color: var(--link-faded-color); +} diff --git a/src/pages/notifications.jsx b/src/pages/notifications.jsx index db525401..1ecbe8c6 100644 --- a/src/pages/notifications.jsx +++ b/src/pages/notifications.jsx @@ -12,9 +12,13 @@ import Loader from '../components/loader'; import NavMenu from '../components/nav-menu'; import Notification from '../components/notification'; import { api } from '../utils/api'; +import enhanceContent from '../utils/enhance-content'; import groupNotifications from '../utils/group-notifications'; +import handleContentLinks from '../utils/handle-content-links'; import niceDateTime from '../utils/nice-date-time'; +import shortenNumber from '../utils/shorten-number'; import states, { saveStatus } from '../utils/states'; +import { getCurrentInstance } from '../utils/store-utils'; import useScroll from '../utils/useScroll'; import useTitle from '../utils/useTitle'; @@ -34,6 +38,7 @@ function Notifications() { }); const hiddenUI = scrollDirection === 'end' && !nearReachStart; const [followRequests, setFollowRequests] = useState([]); + const [announcements, setAnnouncements] = useState([]); console.debug('RENDER Notifications'); @@ -70,12 +75,11 @@ function Notifications() { return allNotifications; } - async function fetchFollowRequests() { - const followRequests = await masto.v1.followRequests.list({ + function fetchFollowRequests() { + // Note: no pagination here yet because this better be on a separate page. Should be rare use-case??? + return masto.v1.followRequests.list({ limit: 80, }); - // Note: no pagination here yet because this better be on a separate page. Should be rare use-case??? - return followRequests; } const loadFollowRequests = () => { @@ -91,16 +95,30 @@ function Notifications() { })(); }; + function fetchAnnouncements() { + return masto.v1.announcements.list(); + } + const loadNotifications = (firstLoad) => { setUIState('loading'); (async () => { try { + const fetchFollowRequestsPromise = fetchFollowRequests(); + const fetchAnnouncementsPromise = fetchAnnouncements(); const { done } = await fetchNotifications(firstLoad); setShowMore(!done); if (firstLoad) { - const requests = await fetchFollowRequests(); + const requests = await fetchFollowRequestsPromise; setFollowRequests(requests); + const announcements = await fetchAnnouncementsPromise; + announcements.sort((a, b) => { + // Sort by updatedAt first, then createdAt + const aDate = new Date(a.updatedAt || a.createdAt); + const bDate = new Date(b.updatedAt || b.createdAt); + return bDate - aDate; + }); + setAnnouncements(announcements); } setUIState('default'); @@ -161,6 +179,8 @@ function Notifications() { todayDate.toDateString(), ); + const announcementsListRef = useRef(); + return (
)} + {announcements.length > 0 && ( +
+ + + {' '} + Announcement{announcements.length > 1 ? 's' : ''}{' '} + {instance} + + {announcements.length > 1 && ( + + {announcements.map((announcement, index) => ( + + ))} + + )} + + +
+ )} {followRequests.length > 0 && (

Follow requests

@@ -332,4 +392,78 @@ function inBackground() { return !!document.querySelector('.deck-backdrop, #modal-container > *'); } +function AnnouncementBlock({ announcement }) { + const { instance } = api(); + const { contact } = getCurrentInstance(); + const contactAccount = contact?.account; + const { + id, + content, + startsAt, + endsAt, + published, + allDay, + publishedAt, + updatedAt, + read, + mentions, + statuses, + tags, + emojis, + reactions, + } = announcement; + + const publishedAtDate = new Date(publishedAt); + const publishedDateText = niceDateTime(publishedAtDate); + const updatedAtDate = new Date(updatedAt); + const updatedAtText = niceDateTime(updatedAtDate); + + return ( +
+ +
+

+ + {updatedAt && updatedAtText !== publishedDateText && ( + <> + {' '} + •{' '} + + Updated{' '} + + + + )} +

+ +
+ ); +} + export default memo(Notifications);