Rewrote notifications, again
This commit is contained in:
parent
30b747527e
commit
0b6dd07eee
5 changed files with 217 additions and 169 deletions
342
src/app.jsx
342
src/app.jsx
|
@ -55,6 +55,7 @@ import { getAccessToken } from './utils/auth';
|
||||||
import states, { getStatus, saveStatus } from './utils/states';
|
import states, { getStatus, saveStatus } from './utils/states';
|
||||||
import store from './utils/store';
|
import store from './utils/store';
|
||||||
import { getCurrentAccount } from './utils/store-utils';
|
import { getCurrentAccount } from './utils/store-utils';
|
||||||
|
import usePageVisibility from './utils/usePageVisibility';
|
||||||
|
|
||||||
window.__STATES__ = states;
|
window.__STATES__ = states;
|
||||||
|
|
||||||
|
@ -110,6 +111,7 @@ function App() {
|
||||||
if (account) {
|
if (account) {
|
||||||
store.session.set('currentAccount', account.info.id);
|
store.session.set('currentAccount', account.info.id);
|
||||||
const { masto } = api({ account });
|
const { masto } = api({ account });
|
||||||
|
console.log('masto', masto);
|
||||||
initPreferences(masto);
|
initPreferences(masto);
|
||||||
setUIState('loading');
|
setUIState('loading');
|
||||||
(async () => {
|
(async () => {
|
||||||
|
@ -164,6 +166,54 @@ function App() {
|
||||||
// }
|
// }
|
||||||
// }, [isLoggedIn]);
|
// }, [isLoggedIn]);
|
||||||
|
|
||||||
|
// Notifications service
|
||||||
|
// - WebSocket to receive notifications when page is visible
|
||||||
|
const [visible, setVisible] = useState(true);
|
||||||
|
usePageVisibility(setVisible);
|
||||||
|
const notificationStream = useRef();
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn && visible) {
|
||||||
|
const { masto } = api();
|
||||||
|
(async () => {
|
||||||
|
// 1. Get the latest notification
|
||||||
|
if (states.notificationsLast) {
|
||||||
|
const notificationsIterator = masto.v1.notifications.list({
|
||||||
|
limit: 1,
|
||||||
|
since_id: states.notificationsLast.id,
|
||||||
|
});
|
||||||
|
const { value: notifications } = await notificationsIterator.next();
|
||||||
|
if (notifications?.length) {
|
||||||
|
states.notificationsShowNew = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Start streaming
|
||||||
|
notificationStream.current = await masto.ws.stream(
|
||||||
|
'/api/v1/streaming',
|
||||||
|
{
|
||||||
|
stream: 'user:notification',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
console.log('🎏 Streaming notification', notificationStream.current);
|
||||||
|
|
||||||
|
notificationStream.current.on('notification', (notification) => {
|
||||||
|
console.log('🔔🔔 Notification', notification);
|
||||||
|
states.notificationsShowNew = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationStream.current.ws.onclose = () => {
|
||||||
|
console.log('🔔🔔 Notification stream closed');
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (notificationStream.current) {
|
||||||
|
notificationStream.current.ws.close();
|
||||||
|
notificationStream.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [visible, isLoggedIn]);
|
||||||
|
|
||||||
const { prevLocation } = snapStates;
|
const { prevLocation } = snapStates;
|
||||||
const backgroundLocation = useRef(prevLocation || null);
|
const backgroundLocation = useRef(prevLocation || null);
|
||||||
const isModalPage =
|
const isModalPage =
|
||||||
|
@ -360,164 +410,164 @@ function App() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ws;
|
// let ws;
|
||||||
async function startStream() {
|
// async function startStream() {
|
||||||
const { masto, instance } = api();
|
// const { masto, instance } = api();
|
||||||
if (
|
// if (
|
||||||
ws &&
|
// ws &&
|
||||||
(ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
// (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
|
||||||
) {
|
// ) {
|
||||||
return;
|
// 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;
|
// ws = stream.ws;
|
||||||
|
|
||||||
const handleNewStatus = debounce((status) => {
|
// const handleNewStatus = debounce((status) => {
|
||||||
console.log('UPDATE', status);
|
// console.log('UPDATE', status);
|
||||||
if (document.visibilityState === 'hidden') return;
|
// if (document.visibilityState === 'hidden') return;
|
||||||
|
|
||||||
const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
// const inHomeNew = states.homeNew.find((s) => s.id === status.id);
|
||||||
const inHome = status.id === states.homeLast?.id;
|
// const inHome = status.id === states.homeLast?.id;
|
||||||
if (!inHomeNew && !inHome) {
|
// if (!inHomeNew && !inHome) {
|
||||||
if (states.settings.boostsCarousel && status.reblog) {
|
// if (states.settings.boostsCarousel && status.reblog) {
|
||||||
// do nothing
|
// // do nothing
|
||||||
} else {
|
// } 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]);
|
// console.log('homeNew 1', [...states.homeNew]);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
saveStatus(status, instance);
|
// saveStatus(status, instance);
|
||||||
}, 5000);
|
// }, 5000);
|
||||||
stream.on('update', handleNewStatus);
|
// stream.on('update', handleNewStatus);
|
||||||
stream.on('status.update', (status) => {
|
// stream.on('status.update', (status) => {
|
||||||
console.log('STATUS.UPDATE', status);
|
// console.log('STATUS.UPDATE', status);
|
||||||
saveStatus(status, instance);
|
// saveStatus(status, instance);
|
||||||
});
|
// });
|
||||||
stream.on('delete', (statusID) => {
|
// stream.on('delete', (statusID) => {
|
||||||
console.log('DELETE', statusID);
|
// console.log('DELETE', statusID);
|
||||||
// delete states.statuses[statusID];
|
// // delete states.statuses[statusID];
|
||||||
const s = getStatus(statusID);
|
// const s = getStatus(statusID);
|
||||||
if (s) s._deleted = true;
|
// if (s) s._deleted = true;
|
||||||
});
|
// });
|
||||||
stream.on('notification', (notification) => {
|
// stream.on('notification', (notification) => {
|
||||||
console.log('NOTIFICATION', notification);
|
// console.log('NOTIFICATION', notification);
|
||||||
|
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
// const inNotificationsNew = states.notificationsNew.find(
|
||||||
(n) => n.id === notification.id,
|
// (n) => n.id === notification.id,
|
||||||
);
|
// );
|
||||||
const inNotifications = notification.id === states.notificationsLast?.id;
|
// const inNotifications = notification.id === states.notificationsLast?.id;
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
// if (!inNotificationsNew && !inNotifications) {
|
||||||
states.notificationsNew.unshift(notification);
|
// states.notificationsNew.unshift(notification);
|
||||||
}
|
// }
|
||||||
|
|
||||||
saveStatus(notification.status, instance, { override: false });
|
// saveStatus(notification.status, instance, { override: false });
|
||||||
});
|
// });
|
||||||
|
|
||||||
stream.ws.onclose = () => {
|
// stream.ws.onclose = () => {
|
||||||
console.log('STREAM CLOSED!');
|
// console.log('STREAM CLOSED!');
|
||||||
if (document.visibilityState !== 'hidden') {
|
// if (document.visibilityState !== 'hidden') {
|
||||||
startStream();
|
// startStream();
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
stream,
|
// stream,
|
||||||
stopStream: () => {
|
// stopStream: () => {
|
||||||
stream.ws.close();
|
// stream.ws.close();
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
let lastHidden;
|
// let lastHidden;
|
||||||
function startVisibility() {
|
// function startVisibility() {
|
||||||
const { masto, instance } = api();
|
// const { masto, instance } = api();
|
||||||
const handleVisible = (visible) => {
|
// const handleVisible = (visible) => {
|
||||||
if (!visible) {
|
// if (!visible) {
|
||||||
const timestamp = Date.now();
|
// const timestamp = Date.now();
|
||||||
lastHidden = timestamp;
|
// lastHidden = timestamp;
|
||||||
} else {
|
// } else {
|
||||||
const timestamp = Date.now();
|
// const timestamp = Date.now();
|
||||||
const diff = timestamp - lastHidden;
|
// const diff = timestamp - lastHidden;
|
||||||
const diffMins = Math.round(diff / 1000 / 60);
|
// const diffMins = Math.round(diff / 1000 / 60);
|
||||||
console.log(`visible: ${visible}`, { lastHidden, diffMins });
|
// console.log(`visible: ${visible}`, { lastHidden, diffMins });
|
||||||
if (!lastHidden || diffMins > 1) {
|
// if (!lastHidden || diffMins > 1) {
|
||||||
(async () => {
|
// (async () => {
|
||||||
try {
|
// try {
|
||||||
const firstStatusID = states.homeLast?.id;
|
// const firstStatusID = states.homeLast?.id;
|
||||||
const firstNotificationID = states.notificationsLast?.id;
|
// const firstNotificationID = states.notificationsLast?.id;
|
||||||
console.log({ states, firstNotificationID, firstStatusID });
|
// console.log({ states, firstNotificationID, firstStatusID });
|
||||||
const fetchHome = masto.v1.timelines.listHome({
|
// const fetchHome = masto.v1.timelines.listHome({
|
||||||
limit: 5,
|
// limit: 5,
|
||||||
...(firstStatusID && { sinceId: firstStatusID }),
|
// ...(firstStatusID && { sinceId: firstStatusID }),
|
||||||
});
|
// });
|
||||||
const fetchNotifications = masto.v1.notifications.list({
|
// const fetchNotifications = masto.v1.notifications.list({
|
||||||
limit: 1,
|
// limit: 1,
|
||||||
...(firstNotificationID && { sinceId: firstNotificationID }),
|
// ...(firstNotificationID && { sinceId: firstNotificationID }),
|
||||||
});
|
// });
|
||||||
|
|
||||||
const newStatuses = await fetchHome;
|
// const newStatuses = await fetchHome;
|
||||||
const hasOneAndReblog =
|
// const hasOneAndReblog =
|
||||||
newStatuses.length === 1 && newStatuses?.[0]?.reblog;
|
// newStatuses.length === 1 && newStatuses?.[0]?.reblog;
|
||||||
if (newStatuses.length) {
|
// if (newStatuses.length) {
|
||||||
if (states.settings.boostsCarousel && hasOneAndReblog) {
|
// if (states.settings.boostsCarousel && hasOneAndReblog) {
|
||||||
// do nothing
|
// // do nothing
|
||||||
} else {
|
// } else {
|
||||||
states.homeNew = newStatuses.map((status) => {
|
// states.homeNew = newStatuses.map((status) => {
|
||||||
saveStatus(status, instance);
|
// saveStatus(status, instance);
|
||||||
return {
|
// return {
|
||||||
id: status.id,
|
// id: status.id,
|
||||||
reblog: status.reblog?.id,
|
// reblog: status.reblog?.id,
|
||||||
reply: !!status.inReplyToAccountId,
|
// reply: !!status.inReplyToAccountId,
|
||||||
};
|
// };
|
||||||
});
|
// });
|
||||||
console.log('homeNew 2', [...states.homeNew]);
|
// console.log('homeNew 2', [...states.homeNew]);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
const newNotifications = await fetchNotifications;
|
// const newNotifications = await fetchNotifications;
|
||||||
if (newNotifications.length) {
|
// if (newNotifications.length) {
|
||||||
const notification = newNotifications[0];
|
// const notification = newNotifications[0];
|
||||||
const inNotificationsNew = states.notificationsNew.find(
|
// const inNotificationsNew = states.notificationsNew.find(
|
||||||
(n) => n.id === notification.id,
|
// (n) => n.id === notification.id,
|
||||||
);
|
// );
|
||||||
const inNotifications =
|
// const inNotifications =
|
||||||
notification.id === states.notificationsLast?.id;
|
// notification.id === states.notificationsLast?.id;
|
||||||
if (!inNotificationsNew && !inNotifications) {
|
// if (!inNotificationsNew && !inNotifications) {
|
||||||
states.notificationsNew.unshift(notification);
|
// states.notificationsNew.unshift(notification);
|
||||||
}
|
// }
|
||||||
|
|
||||||
saveStatus(notification.status, instance, { override: false });
|
// saveStatus(notification.status, instance, { override: false });
|
||||||
}
|
// }
|
||||||
} catch (e) {
|
// } catch (e) {
|
||||||
// Silently fail
|
// // Silently fail
|
||||||
console.error(e);
|
// console.error(e);
|
||||||
} finally {
|
// } finally {
|
||||||
startStream();
|
// startStream();
|
||||||
}
|
// }
|
||||||
})();
|
// })();
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
const handleVisibilityChange = () => {
|
// const handleVisibilityChange = () => {
|
||||||
const hidden = document.visibilityState === 'hidden';
|
// const hidden = document.visibilityState === 'hidden';
|
||||||
handleVisible(!hidden);
|
// handleVisible(!hidden);
|
||||||
console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible'));
|
// console.log('VISIBILITY: ' + (hidden ? 'hidden' : 'visible'));
|
||||||
};
|
// };
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
// document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
requestAnimationFrame(handleVisibilityChange);
|
// requestAnimationFrame(handleVisibilityChange);
|
||||||
return {
|
// return {
|
||||||
stop: () => {
|
// stop: () => {
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
// document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
},
|
// },
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|
||||||
export { App };
|
export { App };
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
import { FocusableItem, Menu, MenuDivider, MenuItem } from '@szhsin/react-menu';
|
||||||
|
import { useSnapshot } from 'valtio';
|
||||||
|
|
||||||
import { api } from '../utils/api';
|
import { api } from '../utils/api';
|
||||||
import states from '../utils/states';
|
import states from '../utils/states';
|
||||||
|
@ -7,6 +8,7 @@ import Icon from './icon';
|
||||||
import Link from './link';
|
import Link from './link';
|
||||||
|
|
||||||
function NavMenu(props) {
|
function NavMenu(props) {
|
||||||
|
const snapStates = useSnapshot(states);
|
||||||
const { instance, authenticated } = api();
|
const { instance, authenticated } = api();
|
||||||
return (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
|
@ -30,6 +32,12 @@ function NavMenu(props) {
|
||||||
<>
|
<>
|
||||||
<MenuLink to="/notifications">
|
<MenuLink to="/notifications">
|
||||||
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
<Icon icon="notification" size="l" /> <span>Notifications</span>
|
||||||
|
{snapStates.notificationsShowNew && (
|
||||||
|
<sup title="New" style={{ opacity: 0.5 }}>
|
||||||
|
{' '}
|
||||||
|
•
|
||||||
|
</sup>
|
||||||
|
)}
|
||||||
</MenuLink>
|
</MenuLink>
|
||||||
<MenuDivider />
|
<MenuDivider />
|
||||||
<MenuLink to="/l">
|
<MenuLink to="/l">
|
||||||
|
|
|
@ -90,17 +90,6 @@ function Following({ title, path, id, headerStart }) {
|
||||||
if (s) s._deleted = true;
|
if (s) s._deleted = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('notification', (notification) => {
|
|
||||||
console.log('🔔 Notification', notification);
|
|
||||||
const inNotifications =
|
|
||||||
notification.id === snapStates.notificationsLast?.id;
|
|
||||||
if (inNotifications) return;
|
|
||||||
states.notificationsNew.unshift(notification);
|
|
||||||
saveStatus(notification.status, instance, {
|
|
||||||
override: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
stream.ws.onclose = () => {
|
stream.ws.onclose = () => {
|
||||||
console.log('🎏 Streaming user closed');
|
console.log('🎏 Streaming user closed');
|
||||||
};
|
};
|
||||||
|
@ -124,7 +113,7 @@ function Following({ title, path, id, headerStart }) {
|
||||||
<Link
|
<Link
|
||||||
to="/notifications"
|
to="/notifications"
|
||||||
class={`button plain ${
|
class={`button plain ${
|
||||||
snapStates.notificationsNew.length > 0 ? 'has-badge' : ''
|
snapStates.notificationsShowNew ? 'has-badge' : ''
|
||||||
}`}
|
}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
|
@ -64,32 +64,34 @@ function Notifications() {
|
||||||
|
|
||||||
const notificationsIterator = useRef();
|
const notificationsIterator = useRef();
|
||||||
async function fetchNotifications(firstLoad) {
|
async function fetchNotifications(firstLoad) {
|
||||||
if (firstLoad) {
|
if (firstLoad || !notificationsIterator.current) {
|
||||||
// Reset iterator
|
// Reset iterator
|
||||||
notificationsIterator.current = masto.v1.notifications.list({
|
notificationsIterator.current = masto.v1.notifications.list({
|
||||||
limit: LIMIT,
|
limit: LIMIT,
|
||||||
});
|
});
|
||||||
states.notificationsNew = [];
|
|
||||||
}
|
}
|
||||||
const allNotifications = await notificationsIterator.current.next();
|
const allNotifications = await notificationsIterator.current.next();
|
||||||
if (allNotifications.value?.length) {
|
const notifications = allNotifications.value;
|
||||||
const notificationsValues = allNotifications.value.map((notification) => {
|
|
||||||
|
if (notifications?.length) {
|
||||||
|
notifications.forEach((notification) => {
|
||||||
saveStatus(notification.status, {
|
saveStatus(notification.status, {
|
||||||
skipThreading: true,
|
skipThreading: true,
|
||||||
override: false,
|
override: false,
|
||||||
});
|
});
|
||||||
return notification;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupedNotifications = groupNotifications(notificationsValues);
|
const groupedNotifications = groupNotifications(notifications);
|
||||||
|
|
||||||
if (firstLoad) {
|
if (firstLoad) {
|
||||||
states.notificationsLast = notificationsValues[0];
|
states.notificationsLast = notifications[0];
|
||||||
states.notifications = groupedNotifications;
|
states.notifications = groupedNotifications;
|
||||||
} else {
|
} else {
|
||||||
states.notifications.push(...groupedNotifications);
|
states.notifications.push(...groupedNotifications);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
states.notificationsShowNew = false;
|
||||||
states.notificationsLastFetchTime = Date.now();
|
states.notificationsLastFetchTime = Date.now();
|
||||||
return allNotifications;
|
return allNotifications;
|
||||||
}
|
}
|
||||||
|
@ -161,14 +163,12 @@ function Notifications() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{snapStates.notificationsNew.length > 0 && uiState !== 'loading' && (
|
{snapStates.notificationsShowNew && uiState !== 'loading' && (
|
||||||
<button
|
<button
|
||||||
class="updates-button"
|
class="updates-button"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadNotifications(true);
|
loadNotifications(true);
|
||||||
states.notificationsNew = [];
|
|
||||||
|
|
||||||
scrollableRef.current?.scrollTo({
|
scrollableRef.current?.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
|
|
|
@ -16,8 +16,9 @@ const states = proxy({
|
||||||
homeLast: null, // Last item in 'home' list
|
homeLast: null, // Last item in 'home' list
|
||||||
homeLastFetchTime: null,
|
homeLastFetchTime: null,
|
||||||
notifications: [],
|
notifications: [],
|
||||||
notificationsLast: store.account.get('notificationsLast') || null, // Last item in 'notifications' list
|
notificationsLast: store.account.get('notificationsLast') || null, // Last read notification
|
||||||
notificationsNew: [],
|
notificationsNew: [],
|
||||||
|
notificationsShowNew: false,
|
||||||
notificationsLastFetchTime: null,
|
notificationsLastFetchTime: null,
|
||||||
accounts: {},
|
accounts: {},
|
||||||
reloadStatusPage: 0,
|
reloadStatusPage: 0,
|
||||||
|
|
Loading…
Add table
Reference in a new issue