mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-03-14 12:18:30 +03:00
Experimental opt-in server-side grouped notifications
This commit is contained in:
parent
57d6889826
commit
a2f7638257
8 changed files with 246 additions and 22 deletions
|
@ -149,6 +149,9 @@ function Notification({
|
|||
moderation_warning,
|
||||
_accounts,
|
||||
_statuses,
|
||||
// Grouped notification
|
||||
sampleAccounts,
|
||||
notificationsCount,
|
||||
} = notification;
|
||||
let { type } = notification;
|
||||
|
||||
|
@ -167,12 +170,14 @@ function Notification({
|
|||
let favsCount = 0;
|
||||
let reblogsCount = 0;
|
||||
if (type === 'favourite+reblog') {
|
||||
for (const account of _accounts) {
|
||||
if (account._types?.includes('favourite')) {
|
||||
favsCount++;
|
||||
}
|
||||
if (account._types?.includes('reblog')) {
|
||||
reblogsCount++;
|
||||
if (_accounts) {
|
||||
for (const account of _accounts) {
|
||||
if (account._types?.includes('favourite')) {
|
||||
favsCount++;
|
||||
}
|
||||
if (account._types?.includes('reblog')) {
|
||||
reblogsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!reblogsCount && favsCount) type = 'favourite';
|
||||
|
@ -296,6 +301,15 @@ function Notification({
|
|||
people
|
||||
</b>{' '}
|
||||
</>
|
||||
) : notificationsCount > 1 ? (
|
||||
<>
|
||||
<b>
|
||||
<span title={notificationsCount}>
|
||||
{shortenNumber(notificationsCount)}
|
||||
</span>{' '}
|
||||
people
|
||||
</b>{' '}
|
||||
</>
|
||||
) : (
|
||||
account && (
|
||||
<>
|
||||
|
@ -405,6 +419,54 @@ function Notification({
|
|||
</button>
|
||||
</p>
|
||||
)}
|
||||
{!_accounts?.length && sampleAccounts?.length > 1 && (
|
||||
<p class="avatars-stack">
|
||||
{sampleAccounts.map((account) => (
|
||||
<Fragment key={account.id}>
|
||||
<a
|
||||
key={account.id}
|
||||
href={account.url}
|
||||
rel="noopener noreferrer"
|
||||
class="account-avatar-stack"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
states.showAccount = account;
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
url={account.avatarStatic}
|
||||
size="xxl"
|
||||
key={account.id}
|
||||
alt={`${account.displayName} @${account.acct}`}
|
||||
squircle={account?.bot}
|
||||
/>
|
||||
{/* {type === 'favourite+reblog' && (
|
||||
<div class="account-sub-icons">
|
||||
{account._types.map((type) => (
|
||||
<Icon
|
||||
icon={NOTIFICATION_ICONS[type]}
|
||||
size="s"
|
||||
class={`${type}-icon`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)} */}
|
||||
</a>{' '}
|
||||
</Fragment>
|
||||
))}
|
||||
{notificationsCount > sampleAccounts.length && (
|
||||
<Link
|
||||
to={
|
||||
instance ? `/${instance}/s/${status.id}` : `/s/${status.id}`
|
||||
}
|
||||
class="button small plain centered"
|
||||
>
|
||||
+{notificationsCount - sampleAccounts.length}
|
||||
<Icon icon="chevron-right" />
|
||||
</Link>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{_statuses?.length > 1 && (
|
||||
<ul class="notification-group-statuses">
|
||||
{_statuses.map((status) => (
|
||||
|
|
|
@ -3,5 +3,6 @@
|
|||
"@mastodon/list-exclusive": ">=4.2",
|
||||
"@mastodon/filtered-notifications": "~4.3 || >=4.3",
|
||||
"@mastodon/fetch-multiple-statuses": "~4.3 || >=4.3",
|
||||
"@mastodon/trending-link-posts": "~4.3 || >=4.3"
|
||||
"@mastodon/trending-link-posts": "~4.3 || >=4.3",
|
||||
"@mastodon/grouped-notifications": "~4.3 || >=4.3"
|
||||
}
|
||||
|
|
|
@ -378,11 +378,17 @@ textarea:disabled {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
button.small {
|
||||
:is(button, .button).small {
|
||||
font-size: 90%;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.button.centered {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
select.plain {
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
|
|
|
@ -17,6 +17,10 @@ import states, { saveStatus } from '../utils/states';
|
|||
import { getCurrentAccountNS } from '../utils/store-utils';
|
||||
|
||||
import Following from './following';
|
||||
import {
|
||||
getGroupedNotifications,
|
||||
mastoFetchNotifications,
|
||||
} from './notifications';
|
||||
|
||||
function Home() {
|
||||
const snapStates = useSnapshot(states);
|
||||
|
@ -84,16 +88,13 @@ function NotificationsLink() {
|
|||
);
|
||||
}
|
||||
|
||||
const NOTIFICATIONS_LIMIT = 80;
|
||||
const NOTIFICATIONS_DISPLAY_LIMIT = 5;
|
||||
function NotificationsMenu({ anchorRef, state, onClose }) {
|
||||
const { masto, instance } = api();
|
||||
const snapStates = useSnapshot(states);
|
||||
const [uiState, setUIState] = useState('default');
|
||||
|
||||
const notificationsIterator = masto.v1.notifications.list({
|
||||
limit: NOTIFICATIONS_LIMIT,
|
||||
});
|
||||
const notificationsIterator = mastoFetchNotifications();
|
||||
|
||||
async function fetchNotifications() {
|
||||
const allNotifications = await notificationsIterator.next();
|
||||
|
@ -106,7 +107,7 @@ function NotificationsMenu({ anchorRef, state, onClose }) {
|
|||
});
|
||||
});
|
||||
|
||||
const groupedNotifications = groupNotifications(notifications);
|
||||
const groupedNotifications = getGroupedNotifications(notifications);
|
||||
|
||||
states.notificationsLast = notifications[0];
|
||||
states.notifications = groupedNotifications;
|
||||
|
|
|
@ -20,8 +20,11 @@ import Notification from '../components/notification';
|
|||
import Status from '../components/status';
|
||||
import { api } from '../utils/api';
|
||||
import enhanceContent from '../utils/enhance-content';
|
||||
import groupNotifications from '../utils/group-notifications';
|
||||
import groupNotifications, {
|
||||
groupNotifications2,
|
||||
} from '../utils/group-notifications';
|
||||
import handleContentLinks from '../utils/handle-content-links';
|
||||
import mem from '../utils/mem';
|
||||
import niceDateTime from '../utils/nice-date-time';
|
||||
import { getRegistration } from '../utils/push-notifications';
|
||||
import shortenNumber from '../utils/shorten-number';
|
||||
|
@ -33,7 +36,8 @@ import usePageVisibility from '../utils/usePageVisibility';
|
|||
import useScroll from '../utils/useScroll';
|
||||
import useTitle from '../utils/useTitle';
|
||||
|
||||
const LIMIT = 80;
|
||||
const NOTIFICATIONS_LIMIT = 80;
|
||||
const NOTIFICATIONS_GROUPED_LIMIT = 20;
|
||||
const emptySearchParams = new URLSearchParams();
|
||||
|
||||
const scrollIntoViewOptions = {
|
||||
|
@ -42,6 +46,43 @@ const scrollIntoViewOptions = {
|
|||
behavior: 'smooth',
|
||||
};
|
||||
|
||||
const memSupportsGroupedNotifications = mem(
|
||||
() => supports('@mastodon/grouped-notifications'),
|
||||
{
|
||||
maxAge: 1000 * 60 * 5, // 5 minutes
|
||||
},
|
||||
);
|
||||
|
||||
export function mastoFetchNotifications(opts = {}) {
|
||||
const { masto } = api();
|
||||
if (
|
||||
states.settings.groupedNotificationsAlpha &&
|
||||
memSupportsGroupedNotifications()
|
||||
) {
|
||||
// https://github.com/mastodon/mastodon/pull/29889
|
||||
return masto.v2_alpha.notifications.list({
|
||||
limit: NOTIFICATIONS_GROUPED_LIMIT,
|
||||
...opts,
|
||||
});
|
||||
} else {
|
||||
return masto.v1.notifications.list({
|
||||
limit: NOTIFICATIONS_LIMIT,
|
||||
...opts,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getGroupedNotifications(notifications) {
|
||||
if (
|
||||
states.settings.groupedNotificationsAlpha &&
|
||||
memSupportsGroupedNotifications()
|
||||
) {
|
||||
return groupNotifications2(notifications);
|
||||
} else {
|
||||
return groupNotifications(notifications);
|
||||
}
|
||||
}
|
||||
|
||||
function Notifications({ columnMode }) {
|
||||
useTitle('Notifications', '/notifications');
|
||||
const { masto, instance } = api();
|
||||
|
@ -67,8 +108,7 @@ function Notifications({ columnMode }) {
|
|||
async function fetchNotifications(firstLoad) {
|
||||
if (firstLoad || !notificationsIterator.current) {
|
||||
// Reset iterator
|
||||
notificationsIterator.current = masto.v1.notifications.list({
|
||||
limit: LIMIT,
|
||||
notificationsIterator.current = mastoFetchNotifications({
|
||||
excludeTypes: ['follow_request'],
|
||||
});
|
||||
}
|
||||
|
@ -115,10 +155,10 @@ function Notifications({ columnMode }) {
|
|||
|
||||
// console.log({ notifications });
|
||||
|
||||
const groupedNotifications = groupNotifications(notifications);
|
||||
const groupedNotifications = getGroupedNotifications(notifications);
|
||||
|
||||
if (firstLoad) {
|
||||
states.notificationsLast = notifications[0];
|
||||
states.notificationsLast = groupedNotifications[0];
|
||||
states.notifications = groupedNotifications;
|
||||
|
||||
// Update last read marker
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
import showToast from '../utils/show-toast';
|
||||
import states from '../utils/states';
|
||||
import store from '../utils/store';
|
||||
import supports from '../utils/supports';
|
||||
|
||||
const DEFAULT_TEXT_SIZE = 16;
|
||||
const TEXT_SIZES = [14, 15, 16, 17, 18, 19, 20];
|
||||
|
@ -496,6 +497,27 @@ function Settings({ onClose }) {
|
|||
</div>
|
||||
</li>
|
||||
)}
|
||||
{authenticated && supports('@mastodon/grouped-notifications') && (
|
||||
<li>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={snapStates.settings.groupedNotificationsAlpha}
|
||||
onChange={(e) => {
|
||||
states.settings.groupedNotificationsAlpha =
|
||||
e.target.checked;
|
||||
}}
|
||||
/>{' '}
|
||||
Server-side grouped notifications
|
||||
</label>
|
||||
<div class="sub-section insignificant">
|
||||
<small>
|
||||
Alpha-stage feature. Potentially improved grouping window
|
||||
but basic grouping logic.
|
||||
</small>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{authenticated && (
|
||||
<li>
|
||||
<label>
|
||||
|
|
|
@ -28,7 +28,95 @@ export function fixNotifications(notifications) {
|
|||
});
|
||||
}
|
||||
|
||||
function groupNotifications(notifications) {
|
||||
export function groupNotifications2(groupNotifications) {
|
||||
// Massage grouped notifications to look like faux grouped notifications above
|
||||
const newGroupNotifications = groupNotifications.map((gn) => {
|
||||
const {
|
||||
latestPageNotificationAt,
|
||||
mostRecentNotificationId,
|
||||
sampleAccounts,
|
||||
notificationsCount,
|
||||
} = gn;
|
||||
|
||||
return {
|
||||
id: '' + mostRecentNotificationId,
|
||||
createdAt: latestPageNotificationAt,
|
||||
account: sampleAccounts[0],
|
||||
...gn,
|
||||
};
|
||||
});
|
||||
|
||||
// DISABLED FOR NOW.
|
||||
// Merge favourited and reblogged of same status into a single notification
|
||||
// - new type: "favourite+reblog"
|
||||
// - sum numbers for `notificationsCount` and `sampleAccounts`
|
||||
// const mappedNotifications = {};
|
||||
// const newNewGroupNotifications = [];
|
||||
// for (let i = 0; i < newGroupNotifications.length; i++) {
|
||||
// const gn = newGroupNotifications[i];
|
||||
// const { type, status, createdAt, notificationsCount, sampleAccounts } = gn;
|
||||
// const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
|
||||
// let virtualType = type;
|
||||
// if (type === 'favourite' || type === 'reblog') {
|
||||
// virtualType = 'favourite+reblog';
|
||||
// }
|
||||
// const key = `${status?.id}-${virtualType}-${date}`;
|
||||
// const mappedNotification = mappedNotifications[key];
|
||||
// if (mappedNotification) {
|
||||
// const accountIDs = mappedNotification.sampleAccounts.map((a) => a.id);
|
||||
// sampleAccounts.forEach((a) => {
|
||||
// if (!accountIDs.includes(a.id)) {
|
||||
// mappedNotification.sampleAccounts.push(a);
|
||||
// }
|
||||
// });
|
||||
// mappedNotification.notificationsCount = Math.max(
|
||||
// mappedNotification.notificationsCount,
|
||||
// notificationsCount,
|
||||
// mappedNotification.sampleAccounts.length,
|
||||
// );
|
||||
// } else {
|
||||
// mappedNotifications[key] = {
|
||||
// ...gn,
|
||||
// type: virtualType,
|
||||
// };
|
||||
// newNewGroupNotifications.push(mappedNotifications[key]);
|
||||
// }
|
||||
// }
|
||||
|
||||
// 2nd pass.
|
||||
// - Group 1 account favourte/reblog multiple posts
|
||||
// - _statuses: [status, status, ...]
|
||||
const notificationsMap2 = {};
|
||||
const newGroupNotifications2 = [];
|
||||
for (let i = 0; i < newGroupNotifications.length; i++) {
|
||||
const gn = newGroupNotifications[i];
|
||||
const { type, account, _accounts, sampleAccounts, createdAt } = gn;
|
||||
const date = createdAt ? new Date(createdAt).toLocaleDateString() : '';
|
||||
const hasOneAccount =
|
||||
sampleAccounts?.length === 1 || _accounts?.length === 1;
|
||||
if ((type === 'favourite' || type === 'reblog') && hasOneAccount) {
|
||||
const key = `${account?.id}-${type}-${date}`;
|
||||
const mappedNotification = notificationsMap2[key];
|
||||
if (mappedNotification) {
|
||||
mappedNotification._statuses.push(gn.status);
|
||||
mappedNotification.id += `-${gn.id}`;
|
||||
} else {
|
||||
let n = (notificationsMap2[key] = {
|
||||
...gn,
|
||||
type,
|
||||
_statuses: [gn.status],
|
||||
});
|
||||
newGroupNotifications2.push(n);
|
||||
}
|
||||
} else {
|
||||
newGroupNotifications2.push(gn);
|
||||
}
|
||||
}
|
||||
|
||||
return newGroupNotifications2;
|
||||
}
|
||||
|
||||
export default function groupNotifications(notifications) {
|
||||
// Filter out invalid notifications
|
||||
notifications = fixNotifications(notifications);
|
||||
|
||||
|
@ -108,5 +196,3 @@ function groupNotifications(notifications) {
|
|||
// return cleanNotifications;
|
||||
return cleanNotifications2;
|
||||
}
|
||||
|
||||
export default groupNotifications;
|
||||
|
|
|
@ -70,6 +70,7 @@ const states = proxy({
|
|||
mediaAltGenerator: false,
|
||||
composerGIFPicker: false,
|
||||
cloakMode: false,
|
||||
groupedNotificationsAlpha: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -104,6 +105,8 @@ export function initStates() {
|
|||
states.settings.composerGIFPicker =
|
||||
store.account.get('settings-composerGIFPicker') ?? false;
|
||||
states.settings.cloakMode = store.account.get('settings-cloakMode') ?? false;
|
||||
states.settings.groupedNotificationsAlpha =
|
||||
store.account.get('settings-groupedNotificationsAlpha') ?? false;
|
||||
}
|
||||
|
||||
subscribeKey(states, 'notificationsLast', (v) => {
|
||||
|
@ -153,6 +156,9 @@ subscribe(states, (changes) => {
|
|||
if (path.join('.') === 'settings.cloakMode') {
|
||||
store.account.set('settings-cloakMode', !!value);
|
||||
}
|
||||
if (path.join('.') === 'settings.groupedNotificationsAlpha') {
|
||||
store.account.set('settings-groupedNotificationsAlpha', !!value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue