Alright let's get Announcements UI out for now

Not perfect but will iterate later
This commit is contained in:
Lim Chee Aun 2023-05-07 11:12:59 +08:00
parent dcf7d3c750
commit 26af33aa85
3 changed files with 276 additions and 5 deletions

View file

@ -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');

View file

@ -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);
}

View file

@ -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 && (
<>
{' '}
&bull;{' '}
<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);