~ knotless Search/
↑↓ move↵ open⇧↵ all results

grain.social/grain

photography-focused photo sharing platform

grain.social
1 collaborator
0
2
69 lines · 2.0 KB Raw History
/**
 * Client-side notification grouping.
 *
 * Groupable reasons: gallery-favorite, story-favorite, follow.
 * Notifications are grouped when they share the same reason AND the same
 * subject (or no subject, for follows) within a 48-hour window.
 */

const GROUPABLE_REASONS = new Set([
  "gallery-favorite",
  "story-favorite",
  "comment-favorite",
  "follow",
]);

const MS_2DAY = 1e3 * 60 * 60 * 48;

export interface GroupedNotification {
  /** Primary notification (most recent in the group). */
  notification: any;
  /** Additional grouped notifications (oldest first). */
  additional: any[];
  /** Total author count (1 + additional unique authors). */
  authorCount: number;
}

export function groupNotifications(notifs: any[]): GroupedNotification[] {
  const groups: GroupedNotification[] = [];

  for (const notif of notifs) {
    if (!GROUPABLE_REASONS.has(notif.reason)) {
      groups.push({ notification: notif, additional: [], authorCount: 1 });
      continue;
    }

    const ts = +new Date(notif.createdAt);
    let matched = false;

    for (const group of groups) {
      const gts = +new Date(group.notification.createdAt);
      if (
        Math.abs(gts - ts) < MS_2DAY &&
        notif.reason === group.notification.reason &&
        subjectKey(notif) === subjectKey(group.notification) &&
        notif.author?.did !== group.notification.author?.did
      ) {
        // Don't add duplicate authors
        const alreadyHas = group.additional.some((a: any) => a.author?.did === notif.author?.did);
        if (!alreadyHas) {
          group.additional.push(notif);
          group.authorCount = 1 + group.additional.length;
        }
        matched = true;
        break;
      }
    }

    if (!matched) {
      groups.push({ notification: notif, additional: [], authorCount: 1 });
    }
  }

  return groups;
}

function subjectKey(notif: any): string {
  if (notif.reason === "follow") return "__follow__";
  return notif.galleryUri ?? notif.storyUri ?? notif.uri;
}