mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-24 06:24:42 +01:00
Alright let's get Announcements UI out for now
Not perfect but will iterate later
This commit is contained in:
parent
dcf7d3c750
commit
26af33aa85
3 changed files with 276 additions and 5 deletions
|
@ -79,6 +79,7 @@ const ICONS = {
|
||||||
react: 'mingcute:react-line',
|
react: 'mingcute:react-line',
|
||||||
layout4: 'mingcute:layout-4-line',
|
layout4: 'mingcute:layout-4-line',
|
||||||
layout5: 'mingcute:layout-5-line',
|
layout5: 'mingcute:layout-5-line',
|
||||||
|
announce: 'mingcute:announcement-line',
|
||||||
};
|
};
|
||||||
|
|
||||||
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
padding: 16px !important;
|
padding: 16px !important;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
animation: appear 0.2s ease-out;
|
animation: appear 0.2s ease-out;
|
||||||
|
clear: both;
|
||||||
}
|
}
|
||||||
.notification.notification-mention {
|
.notification.notification-mention {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
@ -153,6 +154,8 @@
|
||||||
background-color: var(--bg-color);
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* FOLLOW REQUESTS */
|
||||||
|
|
||||||
.follow-requests {
|
.follow-requests {
|
||||||
padding-block-end: 16px;
|
padding-block-end: 16px;
|
||||||
}
|
}
|
||||||
|
@ -190,3 +193,136 @@
|
||||||
.follow-requests ul li .follow-request-buttons .loader-container {
|
.follow-requests ul li .follow-request-buttons .loader-container {
|
||||||
order: -1;
|
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 NavMenu from '../components/nav-menu';
|
||||||
import Notification from '../components/notification';
|
import Notification from '../components/notification';
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
|
import enhanceContent from '../utils/enhance-content';
|
||||||
import groupNotifications from '../utils/group-notifications';
|
import groupNotifications from '../utils/group-notifications';
|
||||||
|
import handleContentLinks from '../utils/handle-content-links';
|
||||||
import niceDateTime from '../utils/nice-date-time';
|
import niceDateTime from '../utils/nice-date-time';
|
||||||
|
import shortenNumber from '../utils/shorten-number';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
|
import { getCurrentInstance } from '../utils/store-utils';
|
||||||
import useScroll from '../utils/useScroll';
|
import useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
|
@ -34,6 +38,7 @@ function Notifications() {
|
||||||
});
|
});
|
||||||
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
const hiddenUI = scrollDirection === 'end' && !nearReachStart;
|
||||||
const [followRequests, setFollowRequests] = useState([]);
|
const [followRequests, setFollowRequests] = useState([]);
|
||||||
|
const [announcements, setAnnouncements] = useState([]);
|
||||||
|
|
||||||
console.debug('RENDER Notifications');
|
console.debug('RENDER Notifications');
|
||||||
|
|
||||||
|
@ -70,12 +75,11 @@ function Notifications() {
|
||||||
return allNotifications;
|
return allNotifications;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchFollowRequests() {
|
function fetchFollowRequests() {
|
||||||
const followRequests = await masto.v1.followRequests.list({
|
// 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,
|
limit: 80,
|
||||||
});
|
});
|
||||||
// Note: no pagination here yet because this better be on a separate page. Should be rare use-case???
|
|
||||||
return followRequests;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadFollowRequests = () => {
|
const loadFollowRequests = () => {
|
||||||
|
@ -91,16 +95,30 @@ function Notifications() {
|
||||||
})();
|
})();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function fetchAnnouncements() {
|
||||||
|
return masto.v1.announcements.list();
|
||||||
|
}
|
||||||
|
|
||||||
const loadNotifications = (firstLoad) => {
|
const loadNotifications = (firstLoad) => {
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
|
const fetchFollowRequestsPromise = fetchFollowRequests();
|
||||||
|
const fetchAnnouncementsPromise = fetchAnnouncements();
|
||||||
const { done } = await fetchNotifications(firstLoad);
|
const { done } = await fetchNotifications(firstLoad);
|
||||||
setShowMore(!done);
|
setShowMore(!done);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
const requests = await fetchFollowRequests();
|
const requests = await fetchFollowRequestsPromise;
|
||||||
setFollowRequests(requests);
|
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');
|
setUIState('default');
|
||||||
|
@ -161,6 +179,8 @@ function Notifications() {
|
||||||
todayDate.toDateString(),
|
todayDate.toDateString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const announcementsListRef = useRef();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="notifications-page"
|
id="notifications-page"
|
||||||
|
@ -214,6 +234,46 @@ function Notifications() {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</header>
|
</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 && (
|
{followRequests.length > 0 && (
|
||||||
<div class="follow-requests">
|
<div class="follow-requests">
|
||||||
<h2 class="timeline-header">Follow requests</h2>
|
<h2 class="timeline-header">Follow requests</h2>
|
||||||
|
@ -332,4 +392,78 @@ function inBackground() {
|
||||||
return !!document.querySelector('.deck-backdrop, #modal-container > *');
|
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);
|
export default memo(Notifications);
|
||||||
|
|
Loading…
Add table
Reference in a new issue