Alright let's get Announcements UI out for now
Not perfect but will iterate later
This commit is contained in:
parent
dcf7d3c750
commit
26af33aa85
|
@ -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');
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<div
|
||||
id="notifications-page"
|
||||
|
@ -214,6 +234,46 @@ function Notifications() {
|
|||
</button>
|
||||
)}
|
||||
</header>
|
||||
{announcements.length > 0 && (
|
||||
<details class="announcements">
|
||||
<summary>
|
||||
<span>
|
||||
<Icon icon="announce" class="announcement-icon" size="l" />{' '}
|
||||
<b>Announcement{announcements.length > 1 ? 's' : ''}</b>{' '}
|
||||
<small class="insignificant">{instance}</small>
|
||||
</span>
|
||||
{announcements.length > 1 && (
|
||||
<span class="announcements-nav-buttons">
|
||||
{announcements.map((announcement, index) => (
|
||||
<button
|
||||
type="button"
|
||||
class="plain2 small"
|
||||
onClick={() => {
|
||||
announcementsListRef.current?.children[
|
||||
index
|
||||
].scrollIntoView({ behavior: 'smooth' });
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
</summary>
|
||||
<ul
|
||||
class={`announcements-list-${
|
||||
announcements.length > 1 ? 'multiple' : 'single'
|
||||
}`}
|
||||
ref={announcementsListRef}
|
||||
>
|
||||
{announcements.map((announcement) => (
|
||||
<li>
|
||||
<AnnouncementBlock announcement={announcement} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{followRequests.length > 0 && (
|
||||
<div class="follow-requests">
|
||||
<h2 class="timeline-header">Follow requests</h2>
|
||||
|
@ -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 (
|
||||
<div class="announcement-block">
|
||||
<AccountBlock account={contactAccount} />
|
||||
<div
|
||||
class="announcement-content"
|
||||
onClick={handleContentLinks({ mentions, instance })}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: enhanceContent(content, {
|
||||
emojis,
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
<p class="insignificant">
|
||||
<time datetime={publishedAtDate.toISOString()}>
|
||||
{niceDateTime(publishedAtDate)}
|
||||
</time>
|
||||
{updatedAt && updatedAtText !== publishedDateText && (
|
||||
<>
|
||||
{' '}
|
||||
•{' '}
|
||||
<span class="ib">
|
||||
Updated{' '}
|
||||
<time datetime={updatedAtDate.toISOString()}>
|
||||
{niceDateTime(updatedAtDate)}
|
||||
</time>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div class="announcement-reactions" hidden>
|
||||
{reactions.map((reaction) => {
|
||||
const { name, count, me, staticUrl, url } = reaction;
|
||||
return (
|
||||
<button type="button" class={`plain4 small ${me ? 'reacted' : ''}`}>
|
||||
{url || staticUrl ? (
|
||||
<img src={url || staticUrl} alt={name} width="16" height="16" />
|
||||
) : (
|
||||
<span>{name}</span>
|
||||
)}{' '}
|
||||
<span class="count">{shortenNumber(count)}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Notifications);
|
||||
|
|
Loading…
Reference in a new issue