mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-02-24 16:58:47 +01:00
Rewrite Notifications page + experimental fix on getting/showing updates
This commit is contained in:
parent
7c6157d47c
commit
e83d128f62
5 changed files with 346 additions and 325 deletions
60
src/app.jsx
60
src/app.jsx
|
@ -165,7 +165,7 @@ function App() {
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
startStream();
|
// startStream();
|
||||||
startVisibility();
|
startVisibility();
|
||||||
|
|
||||||
// Collect instance info
|
// Collect instance info
|
||||||
|
@ -342,20 +342,35 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ws;
|
||||||
async function startStream() {
|
async function startStream() {
|
||||||
|
if (
|
||||||
|
ws &&
|
||||||
|
(ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await masto.v1.stream.streamUser();
|
const stream = await masto.v1.stream.streamUser();
|
||||||
console.log('STREAM START', { stream });
|
console.log('STREAM START', { stream });
|
||||||
|
ws = stream.ws;
|
||||||
|
|
||||||
const handleNewStatus = debounce((status) => {
|
const handleNewStatus = debounce((status) => {
|
||||||
console.log('UPDATE', status);
|
console.log('UPDATE', status);
|
||||||
|
|
||||||
const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
||||||
const inHome = states.home.find((s) => s.id === status.id);
|
const inHome = status.id === states.homeLast?.id;
|
||||||
if (!inHomeNew && !inHome) {
|
if (!inHomeNew && !inHome) {
|
||||||
|
if (states.settings.boostsCarousel && status.reblog) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
states.homeNew.unshift({
|
states.homeNew.unshift({
|
||||||
id: status.id,
|
id: status.id,
|
||||||
reblog: status.reblog?.id,
|
reblog: status.reblog?.id,
|
||||||
reply: !!status.inReplyToAccountId,
|
reply: !!status.inReplyToAccountId,
|
||||||
});
|
});
|
||||||
|
console.log('homeNew 1', [...states.homeNew]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
saveStatus(status);
|
saveStatus(status);
|
||||||
|
@ -377,9 +392,7 @@ async function startStream() {
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
const inNotificationsNew = states.notificationsNew.find(
|
||||||
(n) => n.id === notification.id,
|
(n) => n.id === notification.id,
|
||||||
);
|
);
|
||||||
const inNotifications = states.notifications.find(
|
const inNotifications = notification.id === states.notificationLast?.id;
|
||||||
(n) => n.id === notification.id,
|
|
||||||
);
|
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
if (!inNotificationsNew && !inNotifications) {
|
||||||
states.notificationsNew.unshift(notification);
|
states.notificationsNew.unshift(notification);
|
||||||
}
|
}
|
||||||
|
@ -389,10 +402,9 @@ async function startStream() {
|
||||||
|
|
||||||
stream.ws.onclose = () => {
|
stream.ws.onclose = () => {
|
||||||
console.log('STREAM CLOSED!');
|
console.log('STREAM CLOSED!');
|
||||||
|
if (document.visibilityState !== 'hidden') {
|
||||||
requestAnimationFrame(() => {
|
|
||||||
startStream();
|
startStream();
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -404,8 +416,8 @@ async function startStream() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function startVisibility() {
|
function startVisibility() {
|
||||||
const handleVisibilityChange = () => {
|
const handleVisible = (visible) => {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (!visible) {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
store.session.set('lastHidden', timestamp);
|
store.session.set('lastHidden', timestamp);
|
||||||
} else {
|
} else {
|
||||||
|
@ -415,8 +427,6 @@ function startVisibility() {
|
||||||
const diffMins = Math.round(diff / 1000 / 60);
|
const diffMins = Math.round(diff / 1000 / 60);
|
||||||
if (diffMins > 1) {
|
if (diffMins > 1) {
|
||||||
console.log('visible', { lastHidden, diffMins });
|
console.log('visible', { lastHidden, diffMins });
|
||||||
setTimeout(() => {
|
|
||||||
// Buffer for WS reconnect
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const firstStatusID = states.home[0]?.id;
|
const firstStatusID = states.home[0]?.id;
|
||||||
|
@ -431,10 +441,12 @@ function startVisibility() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const newStatuses = await fetchHome;
|
const newStatuses = await fetchHome;
|
||||||
if (
|
const newStatus = newStatuses?.[0];
|
||||||
newStatuses.length &&
|
const inHome = newStatus?.id !== states.homeLast?.id;
|
||||||
newStatuses[0].id !== states.home[0].id
|
if (newStatuses.length && !inHome) {
|
||||||
) {
|
if (states.settings.boostsCarousel && newStatus.reblog) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
states.homeNew = newStatuses.map((status) => {
|
states.homeNew = newStatuses.map((status) => {
|
||||||
saveStatus(status);
|
saveStatus(status);
|
||||||
return {
|
return {
|
||||||
|
@ -443,6 +455,8 @@ function startVisibility() {
|
||||||
reply: !!status.inReplyToAccountId,
|
reply: !!status.inReplyToAccountId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
console.log('homeNew 2', [...states.homeNew]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const newNotifications = await fetchNotifications;
|
const newNotifications = await fetchNotifications;
|
||||||
|
@ -451,9 +465,8 @@ function startVisibility() {
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
const inNotificationsNew = states.notificationsNew.find(
|
||||||
(n) => n.id === notification.id,
|
(n) => n.id === notification.id,
|
||||||
);
|
);
|
||||||
const inNotifications = states.notifications.find(
|
const inNotifications =
|
||||||
(n) => n.id === notification.id,
|
notification.id === states.notificationLast?.id;
|
||||||
);
|
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
if (!inNotificationsNew && !inNotifications) {
|
||||||
states.notificationsNew.unshift(notification);
|
states.notificationsNew.unshift(notification);
|
||||||
}
|
}
|
||||||
|
@ -463,12 +476,19 @@ function startVisibility() {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silently fail
|
// Silently fail
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
startStream();
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
}, 100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVisibilityChange = () => {
|
||||||
|
const hidden = document.visibilityState === 'hidden';
|
||||||
|
handleVisible(!hidden);
|
||||||
|
console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible'));
|
||||||
|
};
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
return {
|
return {
|
||||||
stop: () => {
|
stop: () => {
|
||||||
|
|
|
@ -22,11 +22,7 @@ function Home({ hidden }) {
|
||||||
|
|
||||||
console.debug('RENDER Home');
|
console.debug('RENDER Home');
|
||||||
|
|
||||||
const homeIterator = useRef(
|
const homeIterator = useRef();
|
||||||
masto.v1.timelines.listHome({
|
|
||||||
limit: LIMIT,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
async function fetchStatuses(firstLoad) {
|
async function fetchStatuses(firstLoad) {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
// Reset iterator
|
// Reset iterator
|
||||||
|
@ -94,12 +90,14 @@ function Home({ hidden }) {
|
||||||
specialHome,
|
specialHome,
|
||||||
});
|
});
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
|
states.homeLast = specialHome[0];
|
||||||
states.home = specialHome;
|
states.home = specialHome;
|
||||||
} else {
|
} else {
|
||||||
states.home.push(...specialHome);
|
states.home.push(...specialHome);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
|
states.homeLast = homeValues[0];
|
||||||
states.home = homeValues;
|
states.home = homeValues;
|
||||||
} else {
|
} else {
|
||||||
states.home.push(...homeValues);
|
states.home.push(...homeValues);
|
||||||
|
@ -272,6 +270,13 @@ function Home({ hidden }) {
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// const showUpdatesButton = snapStates.homeNew.length > 0 && reachStart;
|
||||||
|
const [showUpdatesButton, setShowUpdatesButton] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
const isNewAndTop = snapStates.homeNew.length > 0 && reachStart;
|
||||||
|
setShowUpdatesButton(isNewAndTop);
|
||||||
|
}, [snapStates.homeNew.length]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
@ -321,9 +326,10 @@ function Home({ hidden }) {
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{snapStates.homeNew.length > 0 &&
|
{snapStates.homeNew.length > 0 &&
|
||||||
scrollDirection === 'start' &&
|
((scrollDirection === 'start' &&
|
||||||
!nearReachStart &&
|
!nearReachStart &&
|
||||||
!nearReachEnd && (
|
!nearReachEnd) ||
|
||||||
|
showUpdatesButton) && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 16px !important;
|
padding: 16px !important;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
animation: appear 0.2s ease-out;
|
||||||
}
|
}
|
||||||
.notification.mention {
|
.notification.mention {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
|
|
|
@ -13,6 +13,7 @@ import RelativeTime from '../components/relative-time';
|
||||||
import Status from '../components/status';
|
import Status from '../components/status';
|
||||||
import states, { saveStatus } from '../utils/states';
|
import states, { saveStatus } from '../utils/states';
|
||||||
import store from '../utils/store';
|
import store from '../utils/store';
|
||||||
|
import useScroll from '../utils/useScroll';
|
||||||
import useTitle from '../utils/useTitle';
|
import useTitle from '../utils/useTitle';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -45,6 +46,228 @@ const contentText = {
|
||||||
|
|
||||||
const LIMIT = 30; // 30 is the maximum limit :(
|
const LIMIT = 30; // 30 is the maximum limit :(
|
||||||
|
|
||||||
|
function Notifications() {
|
||||||
|
useTitle('Notifications');
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
|
const [uiState, setUIState] = useState('default');
|
||||||
|
const [showMore, setShowMore] = useState(false);
|
||||||
|
const [onlyMentions, setOnlyMentions] = useState(false);
|
||||||
|
const scrollableRef = useRef();
|
||||||
|
const { nearReachEnd, reachStart } = useScroll({
|
||||||
|
scrollableElement: scrollableRef.current,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.debug('RENDER Notifications');
|
||||||
|
|
||||||
|
const notificationsIterator = useRef();
|
||||||
|
async function fetchNotifications(firstLoad) {
|
||||||
|
if (firstLoad) {
|
||||||
|
// Reset iterator
|
||||||
|
notificationsIterator.current = masto.v1.notifications.list({
|
||||||
|
limit: LIMIT,
|
||||||
|
});
|
||||||
|
states.notificationsNew = [];
|
||||||
|
}
|
||||||
|
const allNotifications = await notificationsIterator.current.next();
|
||||||
|
if (allNotifications.value?.length) {
|
||||||
|
const notificationsValues = allNotifications.value.map((notification) => {
|
||||||
|
saveStatus(notification.status, {
|
||||||
|
skipThreading: true,
|
||||||
|
override: false,
|
||||||
|
});
|
||||||
|
return notification;
|
||||||
|
});
|
||||||
|
|
||||||
|
const groupedNotifications = groupNotifications(notificationsValues);
|
||||||
|
|
||||||
|
if (firstLoad) {
|
||||||
|
states.notificationLast = notificationsValues[0];
|
||||||
|
states.notifications = groupedNotifications;
|
||||||
|
} else {
|
||||||
|
states.notifications.push(...groupedNotifications);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
states.notificationsLastFetchTime = Date.now();
|
||||||
|
return allNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadNotifications = (firstLoad) => {
|
||||||
|
setUIState('loading');
|
||||||
|
(async () => {
|
||||||
|
try {
|
||||||
|
const { done } = await fetchNotifications(firstLoad);
|
||||||
|
setShowMore(!done);
|
||||||
|
setUIState('default');
|
||||||
|
} catch (e) {
|
||||||
|
setUIState('error');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadNotifications(true);
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
if (reachStart) {
|
||||||
|
loadNotifications(true);
|
||||||
|
}
|
||||||
|
}, [reachStart]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (nearReachEnd && showMore) {
|
||||||
|
loadNotifications();
|
||||||
|
}
|
||||||
|
}, [nearReachEnd, showMore]);
|
||||||
|
|
||||||
|
const todayDate = new Date();
|
||||||
|
const yesterdayDate = new Date(todayDate - 24 * 60 * 60 * 1000);
|
||||||
|
let currentDay = new Date();
|
||||||
|
const showTodayEmpty = !snapStates.notifications.some(
|
||||||
|
(notification) =>
|
||||||
|
new Date(notification.createdAt).toDateString() ===
|
||||||
|
todayDate.toDateString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="notifications-page"
|
||||||
|
class="deck-container"
|
||||||
|
ref={scrollableRef}
|
||||||
|
tabIndex="-1"
|
||||||
|
>
|
||||||
|
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
||||||
|
<header
|
||||||
|
onClick={() => {
|
||||||
|
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="header-side">
|
||||||
|
<Link to="/" class="button plain">
|
||||||
|
<Icon icon="home" size="l" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<h1>Notifications</h1>
|
||||||
|
<div class="header-side">
|
||||||
|
<Loader hidden={uiState !== 'loading'} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
{snapStates.notificationsNew.length > 0 && (
|
||||||
|
<button
|
||||||
|
class="updates-button"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
loadNotifications(true);
|
||||||
|
states.notificationsNew = [];
|
||||||
|
|
||||||
|
scrollableRef.current?.scrollTo({
|
||||||
|
top: 0,
|
||||||
|
behavior: 'smooth',
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon="arrow-up" /> New notifications
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div id="mentions-option">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={onlyMentions}
|
||||||
|
onChange={(e) => {
|
||||||
|
setOnlyMentions(e.target.checked);
|
||||||
|
}}
|
||||||
|
/>{' '}
|
||||||
|
Only mentions
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<h2 class="timeline-header">Today</h2>
|
||||||
|
{showTodayEmpty && !!snapStates.notifications.length && (
|
||||||
|
<p class="ui-state insignificant">
|
||||||
|
{uiState === 'default' ? "You're all caught up." : <>…</>}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{snapStates.notifications.length ? (
|
||||||
|
<>
|
||||||
|
{snapStates.notifications.map((notification) => {
|
||||||
|
if (onlyMentions && notification.type !== 'mention') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const notificationDay = new Date(notification.createdAt);
|
||||||
|
const differentDay =
|
||||||
|
notificationDay.toDateString() !== currentDay.toDateString();
|
||||||
|
if (differentDay) {
|
||||||
|
currentDay = notificationDay;
|
||||||
|
}
|
||||||
|
// if notificationDay is yesterday, show "Yesterday"
|
||||||
|
// if notificationDay is before yesterday, show date
|
||||||
|
const heading =
|
||||||
|
notificationDay.toDateString() === yesterdayDate.toDateString()
|
||||||
|
? 'Yesterday'
|
||||||
|
: Intl.DateTimeFormat('en', {
|
||||||
|
// Show year if not current year
|
||||||
|
year:
|
||||||
|
currentDay.getFullYear() === todayDate.getFullYear()
|
||||||
|
? undefined
|
||||||
|
: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
}).format(currentDay);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{differentDay && <h2 class="timeline-header">{heading}</h2>}
|
||||||
|
<Notification
|
||||||
|
notification={notification}
|
||||||
|
key={notification.id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{uiState === 'loading' && (
|
||||||
|
<>
|
||||||
|
<ul class="timeline flat">
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<li class="notification skeleton">
|
||||||
|
<div class="notification-type">
|
||||||
|
<Icon icon="notification" size="xl" />
|
||||||
|
</div>
|
||||||
|
<div class="notification-content">
|
||||||
|
<p>███████████ ████</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{uiState === 'error' && (
|
||||||
|
<p class="ui-state">
|
||||||
|
Unable to load notifications
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button type="button" onClick={() => loadNotifications(true)}>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{showMore && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="plain block"
|
||||||
|
disabled={uiState === 'loading'}
|
||||||
|
onClick={() => loadNotifications()}
|
||||||
|
style={{ marginBlockEnd: '6em' }}
|
||||||
|
>
|
||||||
|
{uiState === 'loading' ? <Loader abrupt /> : <>Show more…</>}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
function Notification({ notification }) {
|
function Notification({ notification }) {
|
||||||
const { id, type, status, account, _accounts } = notification;
|
const { id, type, status, account, _accounts } = notification;
|
||||||
|
|
||||||
|
@ -61,7 +284,7 @@ function Notification({ notification }) {
|
||||||
: contentText[type];
|
: contentText[type];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div class={`notification ${type}`} tabIndex="0">
|
||||||
<div
|
<div
|
||||||
class={`notification-type notification-${type}`}
|
class={`notification-type notification-${type}`}
|
||||||
title={new Date(notification.createdAt).toLocaleString()}
|
title={new Date(notification.createdAt).toLocaleString()}
|
||||||
|
@ -137,11 +360,13 @@ function Notification({ notification }) {
|
||||||
<Avatar
|
<Avatar
|
||||||
url={account.avatarStatic}
|
url={account.avatarStatic}
|
||||||
size={
|
size={
|
||||||
_accounts.length < 30
|
_accounts.length <= 10
|
||||||
? 'xl'
|
? 'xxl'
|
||||||
: _accounts.length < 100
|
: _accounts.length < 100
|
||||||
? 'l'
|
? 'xl'
|
||||||
: _accounts.length < 1000
|
: _accounts.length < 1000
|
||||||
|
? 'l'
|
||||||
|
: _accounts.length < 2000
|
||||||
? 'm'
|
? 'm'
|
||||||
: 's' // My god, this person is popular!
|
: 's' // My god, this person is popular!
|
||||||
}
|
}
|
||||||
|
@ -162,264 +387,6 @@ function Notification({ notification }) {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationsList({ notifications, emptyCopy }) {
|
|
||||||
if (!notifications.length && emptyCopy) {
|
|
||||||
return <p class="timeline-empty">{emptyCopy}</p>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new flat list of notifications
|
|
||||||
// Combine sibling notifications based on type and status id, ignore the id
|
|
||||||
// Concat all notification.account into an array of _accounts
|
|
||||||
const notificationsMap = {};
|
|
||||||
const cleanNotifications = [];
|
|
||||||
for (let i = 0, j = 0; i < notifications.length; i++) {
|
|
||||||
const notification = notifications[i];
|
|
||||||
// const cleanNotification = cleanNotifications[j];
|
|
||||||
const { status, account, type, created_at } = notification;
|
|
||||||
const createdAt = new Date(created_at).toLocaleDateString();
|
|
||||||
const key = `${status?.id}-${type}-${createdAt}`;
|
|
||||||
const mappedNotification = notificationsMap[key];
|
|
||||||
if (mappedNotification?.account) {
|
|
||||||
mappedNotification._accounts.push(account);
|
|
||||||
} else {
|
|
||||||
let n = (notificationsMap[key] = {
|
|
||||||
...notification,
|
|
||||||
_accounts: [account],
|
|
||||||
});
|
|
||||||
cleanNotifications[j++] = n;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// console.log({ notifications, cleanNotifications });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul class="timeline flat">
|
|
||||||
{cleanNotifications.map((notification, i) => {
|
|
||||||
const { id, type } = notification;
|
|
||||||
return (
|
|
||||||
<li key={id} class={`notification ${type}`} tabIndex="0">
|
|
||||||
<Notification notification={notification} />
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Notifications() {
|
|
||||||
useTitle('Notifications');
|
|
||||||
const snapStates = useSnapshot(states);
|
|
||||||
const [uiState, setUIState] = useState('default');
|
|
||||||
const [showMore, setShowMore] = useState(false);
|
|
||||||
const [onlyMentions, setOnlyMentions] = useState(false);
|
|
||||||
|
|
||||||
console.debug('RENDER Notifications');
|
|
||||||
|
|
||||||
const notificationsIterator = useRef(
|
|
||||||
masto.v1.notifications.list({
|
|
||||||
limit: LIMIT,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
async function fetchNotifications(firstLoad) {
|
|
||||||
if (firstLoad) {
|
|
||||||
// Reset iterator
|
|
||||||
notificationsIterator.current = masto.v1.notifications.list({
|
|
||||||
limit: LIMIT,
|
|
||||||
});
|
|
||||||
states.notificationsNew = [];
|
|
||||||
}
|
|
||||||
const allNotifications = await notificationsIterator.current.next();
|
|
||||||
if (allNotifications.value?.length) {
|
|
||||||
const notificationsValues = allNotifications.value.map((notification) => {
|
|
||||||
saveStatus(notification.status, {
|
|
||||||
skipThreading: true,
|
|
||||||
override: false,
|
|
||||||
});
|
|
||||||
return notification;
|
|
||||||
});
|
|
||||||
if (firstLoad) {
|
|
||||||
states.notifications = notificationsValues;
|
|
||||||
} else {
|
|
||||||
states.notifications.push(...notificationsValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
states.notificationsLastFetchTime = Date.now();
|
|
||||||
return allNotifications;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadNotifications = (firstLoad) => {
|
|
||||||
setUIState('loading');
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
const { done } = await fetchNotifications(firstLoad);
|
|
||||||
setShowMore(!done);
|
|
||||||
setUIState('default');
|
|
||||||
} catch (e) {
|
|
||||||
setUIState('error');
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadNotifications(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const scrollableRef = useRef();
|
|
||||||
|
|
||||||
// Group notifications by today, yesterday, and older
|
|
||||||
const groupedNotifications = snapStates.notifications.reduce(
|
|
||||||
(acc, notification) => {
|
|
||||||
const date = new Date(notification.createdAt);
|
|
||||||
const today = new Date();
|
|
||||||
const yesterday = new Date();
|
|
||||||
yesterday.setDate(today.getDate() - 1);
|
|
||||||
if (
|
|
||||||
date.getDate() === today.getDate() &&
|
|
||||||
date.getMonth() === today.getMonth() &&
|
|
||||||
date.getFullYear() === today.getFullYear()
|
|
||||||
) {
|
|
||||||
acc.today.push(notification);
|
|
||||||
} else if (
|
|
||||||
date.getDate() === yesterday.getDate() &&
|
|
||||||
date.getMonth() === yesterday.getMonth() &&
|
|
||||||
date.getFullYear() === yesterday.getFullYear()
|
|
||||||
) {
|
|
||||||
acc.yesterday.push(notification);
|
|
||||||
} else {
|
|
||||||
acc.older.push(notification);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
},
|
|
||||||
{ today: [], yesterday: [], older: [] },
|
|
||||||
);
|
|
||||||
// console.log(groupedNotifications);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
id="notifications-page"
|
|
||||||
class="deck-container"
|
|
||||||
ref={scrollableRef}
|
|
||||||
tabIndex="-1"
|
|
||||||
>
|
|
||||||
<div class={`timeline-deck deck ${onlyMentions ? 'only-mentions' : ''}`}>
|
|
||||||
<header
|
|
||||||
onClick={() => {
|
|
||||||
scrollableRef.current?.scrollTo({ top: 0, behavior: 'smooth' });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="header-side">
|
|
||||||
<Link to="/" class="button plain">
|
|
||||||
<Icon icon="home" size="l" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<h1>Notifications</h1>
|
|
||||||
<div class="header-side">
|
|
||||||
<Loader hidden={uiState !== 'loading'} />
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
{snapStates.notificationsNew.length > 0 && (
|
|
||||||
<button
|
|
||||||
class="updates-button"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
const uniqueNotificationsNew = snapStates.notificationsNew.filter(
|
|
||||||
(notification) =>
|
|
||||||
!snapStates.notifications.some(
|
|
||||||
(n) => n.id === notification.id,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
states.notifications.unshift(...uniqueNotificationsNew);
|
|
||||||
loadNotifications(true);
|
|
||||||
states.notificationsNew = [];
|
|
||||||
|
|
||||||
scrollableRef.current?.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon icon="arrow-up" /> New notifications
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div id="mentions-option">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={onlyMentions}
|
|
||||||
onChange={(e) => {
|
|
||||||
setOnlyMentions(e.target.checked);
|
|
||||||
}}
|
|
||||||
/>{' '}
|
|
||||||
Only mentions
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{snapStates.notifications.length ? (
|
|
||||||
<>
|
|
||||||
<h2 class="timeline-header">Today</h2>
|
|
||||||
<NotificationsList
|
|
||||||
notifications={groupedNotifications.today}
|
|
||||||
emptyCopy="You're all caught up."
|
|
||||||
/>
|
|
||||||
{groupedNotifications.yesterday.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h2 class="timeline-header">Yesterday</h2>
|
|
||||||
<NotificationsList
|
|
||||||
notifications={groupedNotifications.yesterday}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{groupedNotifications.older.length > 0 && (
|
|
||||||
<>
|
|
||||||
<h2 class="timeline-header">Older</h2>
|
|
||||||
<NotificationsList notifications={groupedNotifications.older} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{showMore && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="plain block"
|
|
||||||
disabled={uiState === 'loading'}
|
|
||||||
onClick={() => loadNotifications()}
|
|
||||||
style={{ marginBlockEnd: '6em' }}
|
|
||||||
>
|
|
||||||
{uiState === 'loading' ? <Loader /> : <>Show more…</>}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{uiState === 'loading' && (
|
|
||||||
<>
|
|
||||||
<h2 class="timeline-header">Today</h2>
|
|
||||||
<ul class="timeline flat">
|
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
|
||||||
<li class="notification skeleton">
|
|
||||||
<div class="notification-type">
|
|
||||||
<Icon icon="notification" size="xl" />
|
|
||||||
</div>
|
|
||||||
<div class="notification-content">
|
|
||||||
<p>███████████ ████</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{uiState === 'error' && (
|
|
||||||
<p class="ui-state">
|
|
||||||
Unable to load notifications
|
|
||||||
<br />
|
|
||||||
<br />
|
|
||||||
<button type="button" onClick={() => loadNotifications(true)}>
|
|
||||||
Try again
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -470,4 +437,29 @@ function FollowRequestButtons({ accountID, onChange }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function groupNotifications(notifications) {
|
||||||
|
// Create new flat list of notifications
|
||||||
|
// Combine sibling notifications based on type and status id
|
||||||
|
// Concat all notification.account into an array of _accounts
|
||||||
|
const notificationsMap = {};
|
||||||
|
const cleanNotifications = [];
|
||||||
|
for (let i = 0, j = 0; i < notifications.length; i++) {
|
||||||
|
const notification = notifications[i];
|
||||||
|
const { status, account, type, created_at } = notification;
|
||||||
|
const createdAt = new Date(created_at).toLocaleDateString();
|
||||||
|
const key = `${status?.id}-${type}-${createdAt}`;
|
||||||
|
const mappedNotification = notificationsMap[key];
|
||||||
|
if (mappedNotification?.account) {
|
||||||
|
mappedNotification._accounts.push(account);
|
||||||
|
} else {
|
||||||
|
let n = (notificationsMap[key] = {
|
||||||
|
...notification,
|
||||||
|
_accounts: [account],
|
||||||
|
});
|
||||||
|
cleanNotifications[j++] = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cleanNotifications;
|
||||||
|
}
|
||||||
|
|
||||||
export default memo(Notifications);
|
export default memo(Notifications);
|
||||||
|
|
|
@ -9,8 +9,10 @@ const states = proxy({
|
||||||
home: [],
|
home: [],
|
||||||
specialHome: [],
|
specialHome: [],
|
||||||
homeNew: [],
|
homeNew: [],
|
||||||
|
homeLast: null, // Last item in 'home' list
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
|
notificationLast: null, // Last item in 'notifications' list
|
||||||
notificationsNew: [],
|
notificationsNew: [],
|
||||||
notificationsLastFetchTime: null,
|
notificationsLastFetchTime: null,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
|
|
Loading…
Reference in a new issue