diff --git a/src/components/ChatViewer.tsx b/src/components/ChatViewer.tsx
index 1f07619..1b716c4 100644
--- a/src/components/ChatViewer.tsx
+++ b/src/components/ChatViewer.tsx
@@ -32,6 +32,11 @@ import type { ChatProtocolAdapter } from "@/lib/chat/adapters/base-adapter";
import type { Message } from "@/types/chat";
import type { ChatAction } from "@/types/chat-actions";
import { parseSlashCommand } from "@/lib/chat/slash-command-parser";
+import {
+ groupSystemMessages,
+ isGroupedSystemMessage,
+ type GroupedSystemMessage,
+} from "@/lib/chat/group-system-messages";
import { UserName } from "./nostr/UserName";
import { RichText } from "./nostr/RichText";
import Timestamp from "./Timestamp";
@@ -252,6 +257,58 @@ const ComposerReplyPreview = memo(function ComposerReplyPreview({
);
});
+/**
+ * GroupedSystemMessageItem - Renders multiple users performing the same action
+ * Example: "alice, bob and 3 others reposted"
+ */
+const GroupedSystemMessageItem = memo(function GroupedSystemMessageItem({
+ grouped,
+}: {
+ grouped: GroupedSystemMessage;
+}) {
+ const { authors, content } = grouped;
+
+ // Format the authors list based on count
+ const formatAuthors = () => {
+ if (authors.length === 1) {
+ return ;
+ } else if (authors.length === 2) {
+ return (
+ <>
+ and{" "}
+
+ >
+ );
+ } else if (authors.length === 3) {
+ return (
+ <>
+ ,{" "}
+ and{" "}
+
+ >
+ );
+ } else {
+ // 4 or more: show first 2 and "X others"
+ const othersCount = authors.length - 2;
+ return (
+ <>
+ ,{" "}
+ and {othersCount}{" "}
+ {othersCount === 1 ? "other" : "others"}
+ >
+ );
+ }
+ };
+
+ return (
+
+
+ * {formatAuthors()} {content}
+
+
+ );
+});
+
/**
* MessageItem - Memoized message component for performance
*/
@@ -547,36 +604,51 @@ export function ChatViewer({
[adapter, conversation],
);
- // Process messages to include day markers
+ // Process messages to include day markers and group system messages
const messagesWithMarkers = useMemo(() => {
if (!messages || messages.length === 0) return [];
+ // First, group consecutive system messages
+ const groupedMessages = groupSystemMessages(messages);
+
const items: Array<
| { type: "message"; data: Message }
+ | { type: "grouped-system"; data: GroupedSystemMessage }
| { type: "day-marker"; data: string; timestamp: number }
> = [];
- messages.forEach((message, index) => {
+ groupedMessages.forEach((item, index) => {
+ const timestamp = isGroupedSystemMessage(item)
+ ? item.timestamp
+ : item.timestamp;
+
// Add day marker if this is the first message or if day changed
if (index === 0) {
items.push({
type: "day-marker",
- data: formatDayMarker(message.timestamp),
- timestamp: message.timestamp,
+ data: formatDayMarker(timestamp),
+ timestamp,
});
} else {
- const prevMessage = messages[index - 1];
- if (isDifferentDay(prevMessage.timestamp, message.timestamp)) {
+ const prevItem = groupedMessages[index - 1];
+ const prevTimestamp = isGroupedSystemMessage(prevItem)
+ ? prevItem.timestamp
+ : prevItem.timestamp;
+ if (isDifferentDay(prevTimestamp, timestamp)) {
items.push({
type: "day-marker",
- data: formatDayMarker(message.timestamp),
- timestamp: message.timestamp,
+ data: formatDayMarker(timestamp),
+ timestamp,
});
}
}
- // Add the message itself
- items.push({ type: "message", data: message });
+ // Add the message or grouped system message
+ if (isGroupedSystemMessage(item)) {
+ items.push({ type: "grouped-system", data: item });
+ } else {
+ items.push({ type: "message", data: item });
+ }
});
return items;
@@ -700,9 +772,12 @@ export function ChatViewer({
const handleScrollToMessage = useCallback(
(messageId: string) => {
if (!messagesWithMarkers) return;
- // Find index in the rendered array (which includes day markers)
+ // Find index in the rendered array (which includes day markers and grouped messages)
const index = messagesWithMarkers.findIndex(
- (item) => item.type === "message" && item.data.id === messageId,
+ (item) =>
+ (item.type === "message" && item.data.id === messageId) ||
+ (item.type === "grouped-system" &&
+ item.data.messageIds.includes(messageId)),
);
if (index !== -1 && virtuosoRef.current) {
virtuosoRef.current.scrollToIndex({
@@ -1031,6 +1106,16 @@ export function ChatViewer({
);
}
+
+ if (item.type === "grouped-system") {
+ return (
+
+ );
+ }
+
// For NIP-10 threads, check if this is the root message
const isRootMessage =
protocol === "nip-10" &&
diff --git a/src/lib/chat/adapters/nip-10-adapter.ts b/src/lib/chat/adapters/nip-10-adapter.ts
index 19beac7..63d18d2 100644
--- a/src/lib/chat/adapters/nip-10-adapter.ts
+++ b/src/lib/chat/adapters/nip-10-adapter.ts
@@ -195,7 +195,9 @@ export class Nip10Adapter extends ChatProtocolAdapter {
// Build filter for all thread events:
// - kind 1: replies to root
+ // - kind 6: reposts (legacy)
// - kind 7: reactions
+ // - kind 16: generic reposts
// - kind 9735: zap receipts
const filters: Filter[] = [
// Replies: kind 1 events with e-tag pointing to root
@@ -210,6 +212,12 @@ export class Nip10Adapter extends ChatProtocolAdapter {
"#e": [rootEventId],
limit: 200, // Reactions are small, fetch more
},
+ // Reposts: kind 6 and 16 events with e-tag pointing to root or replies
+ {
+ kinds: [6, 16],
+ "#e": [rootEventId],
+ limit: 100,
+ },
// Zaps: kind 9735 receipts with e-tag pointing to root or replies
{
kinds: [9735],
@@ -245,7 +253,7 @@ export class Nip10Adapter extends ChatProtocolAdapter {
// Combine root event with replies
const rootEvent$ = eventStore.event(rootEventId);
const replies$ = eventStore.timeline({
- kinds: [1, 7, 9735],
+ kinds: [1, 6, 7, 16, 9735],
"#e": [rootEventId],
});
@@ -308,6 +316,12 @@ export class Nip10Adapter extends ChatProtocolAdapter {
until: before,
limit: 100,
},
+ {
+ kinds: [6, 16],
+ "#e": [rootEventId],
+ until: before,
+ limit: 50,
+ },
{
kinds: [9735],
"#e": [rootEventId],
@@ -802,6 +816,12 @@ export class Nip10Adapter extends ChatProtocolAdapter {
return this.zapToMessage(event, conversationId);
}
+ // Handle reposts (kind 6, 16) - simple system messages
+ // Content is ignored even if present (quotes)
+ if (event.kind === 6 || event.kind === 16) {
+ return this.repostToMessage(event, conversationId);
+ }
+
// Handle reactions (kind 7) - skip for now, handled via MessageReactions
if (event.kind === 7) {
return null;
@@ -892,4 +912,29 @@ export class Nip10Adapter extends ChatProtocolAdapter {
event: zapReceipt,
};
}
+
+ /**
+ * Convert repost event to system Message object
+ */
+ private repostToMessage(
+ repostEvent: NostrEvent,
+ conversationId: string,
+ ): Message {
+ // Find what event is being reposted (e-tag)
+ const eTag = repostEvent.tags.find((t) => t[0] === "e");
+ const replyTo = eTag?.[1];
+
+ return {
+ id: repostEvent.id,
+ conversationId,
+ author: repostEvent.pubkey,
+ content: "reposted",
+ timestamp: repostEvent.created_at,
+ type: "system",
+ replyTo,
+ protocol: "nip-10",
+ metadata: {},
+ event: repostEvent,
+ };
+ }
}
diff --git a/src/lib/chat/group-system-messages.test.ts b/src/lib/chat/group-system-messages.test.ts
new file mode 100644
index 0000000..784694c
--- /dev/null
+++ b/src/lib/chat/group-system-messages.test.ts
@@ -0,0 +1,414 @@
+import { describe, it, expect } from "vitest";
+import {
+ groupSystemMessages,
+ isGroupedSystemMessage,
+ type GroupedSystemMessage,
+} from "./group-system-messages";
+import type { Message } from "@/types/chat";
+
+// Helper to create test messages
+function createMessage(
+ id: string,
+ type: Message["type"],
+ content: string,
+ author: string,
+ timestamp: number,
+): Message {
+ return {
+ id,
+ conversationId: "test-conversation",
+ author,
+ content,
+ timestamp,
+ type,
+ protocol: "nip-10",
+ event: {} as any, // Mock event
+ };
+}
+
+describe("groupSystemMessages", () => {
+ describe("basic grouping", () => {
+ it("should group consecutive system messages with same content", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "system", "reposted", "bob", 1001),
+ createMessage("3", "system", "reposted", "charlie", 1002),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+
+ const group = result[0] as GroupedSystemMessage;
+ expect(group.authors).toEqual(["alice", "bob", "charlie"]);
+ expect(group.content).toBe("reposted");
+ expect(group.timestamp).toBe(1000);
+ expect(group.messageIds).toEqual(["1", "2", "3"]);
+ });
+
+ it("should not group non-consecutive system messages", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "user", "hello", "bob", 1001),
+ createMessage("3", "system", "reposted", "charlie", 1002),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(3);
+
+ // First group (alice)
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+ const group1 = result[0] as GroupedSystemMessage;
+ expect(group1.authors).toEqual(["alice"]);
+
+ // User message (bob)
+ expect(isGroupedSystemMessage(result[1])).toBe(false);
+ expect((result[1] as Message).id).toBe("2");
+
+ // Second group (charlie)
+ expect(isGroupedSystemMessage(result[2])).toBe(true);
+ const group2 = result[2] as GroupedSystemMessage;
+ expect(group2.authors).toEqual(["charlie"]);
+ });
+
+ it("should not group system messages with different content", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "system", "joined", "bob", 1001),
+ createMessage("3", "system", "reposted", "charlie", 1002),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(3);
+
+ // Each should be its own group
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+ expect((result[0] as GroupedSystemMessage).content).toBe("reposted");
+ expect((result[0] as GroupedSystemMessage).authors).toEqual(["alice"]);
+
+ expect(isGroupedSystemMessage(result[1])).toBe(true);
+ expect((result[1] as GroupedSystemMessage).content).toBe("joined");
+ expect((result[1] as GroupedSystemMessage).authors).toEqual(["bob"]);
+
+ expect(isGroupedSystemMessage(result[2])).toBe(true);
+ expect((result[2] as GroupedSystemMessage).content).toBe("reposted");
+ expect((result[2] as GroupedSystemMessage).authors).toEqual(["charlie"]);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should handle empty array", () => {
+ const result = groupSystemMessages([]);
+ expect(result).toEqual([]);
+ });
+
+ it("should handle single system message", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+
+ const group = result[0] as GroupedSystemMessage;
+ expect(group.authors).toEqual(["alice"]);
+ expect(group.messageIds).toEqual(["1"]);
+ });
+
+ it("should handle single user message", () => {
+ const messages: Message[] = [
+ createMessage("1", "user", "hello", "alice", 1000),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ expect(isGroupedSystemMessage(result[0])).toBe(false);
+ expect((result[0] as Message).id).toBe("1");
+ });
+
+ it("should handle only user messages", () => {
+ const messages: Message[] = [
+ createMessage("1", "user", "hello", "alice", 1000),
+ createMessage("2", "user", "world", "bob", 1001),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(2);
+ expect(isGroupedSystemMessage(result[0])).toBe(false);
+ expect(isGroupedSystemMessage(result[1])).toBe(false);
+ });
+
+ it("should handle only system messages", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "joined", "alice", 1000),
+ createMessage("2", "system", "joined", "bob", 1001),
+ createMessage("3", "system", "joined", "charlie", 1002),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+
+ const group = result[0] as GroupedSystemMessage;
+ expect(group.authors).toEqual(["alice", "bob", "charlie"]);
+ });
+ });
+
+ describe("mixed message types", () => {
+ it("should not group system messages separated by user messages", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "system", "reposted", "bob", 1001),
+ createMessage("3", "user", "hello", "charlie", 1002),
+ createMessage("4", "system", "reposted", "dave", 1003),
+ createMessage("5", "system", "reposted", "eve", 1004),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(3);
+
+ // First group (alice, bob)
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+ expect((result[0] as GroupedSystemMessage).authors).toEqual([
+ "alice",
+ "bob",
+ ]);
+
+ // User message (charlie)
+ expect(isGroupedSystemMessage(result[1])).toBe(false);
+ expect((result[1] as Message).id).toBe("3");
+
+ // Second group (dave, eve)
+ expect(isGroupedSystemMessage(result[2])).toBe(true);
+ expect((result[2] as GroupedSystemMessage).authors).toEqual([
+ "dave",
+ "eve",
+ ]);
+ });
+
+ it("should not group system messages separated by zap messages", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "zap", "zapped 1000 sats", "bob", 1001),
+ createMessage("3", "system", "reposted", "charlie", 1002),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(3);
+
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+ expect((result[0] as GroupedSystemMessage).authors).toEqual(["alice"]);
+
+ expect(isGroupedSystemMessage(result[1])).toBe(false);
+ expect((result[1] as Message).type).toBe("zap");
+
+ expect(isGroupedSystemMessage(result[2])).toBe(true);
+ expect((result[2] as GroupedSystemMessage).authors).toEqual(["charlie"]);
+ });
+
+ it("should handle complex alternating pattern", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "joined", "alice", 1000),
+ createMessage("2", "system", "joined", "bob", 1001),
+ createMessage("3", "user", "hello", "alice", 1002),
+ createMessage("4", "system", "reposted", "charlie", 1003),
+ createMessage("5", "user", "world", "bob", 1004),
+ createMessage("6", "system", "left", "dave", 1005),
+ createMessage("7", "system", "left", "eve", 1006),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(5);
+
+ // joined group
+ expect(isGroupedSystemMessage(result[0])).toBe(true);
+ expect((result[0] as GroupedSystemMessage).content).toBe("joined");
+ expect((result[0] as GroupedSystemMessage).authors).toEqual([
+ "alice",
+ "bob",
+ ]);
+
+ // user message
+ expect(isGroupedSystemMessage(result[1])).toBe(false);
+
+ // reposted group (single)
+ expect(isGroupedSystemMessage(result[2])).toBe(true);
+ expect((result[2] as GroupedSystemMessage).content).toBe("reposted");
+ expect((result[2] as GroupedSystemMessage).authors).toEqual(["charlie"]);
+
+ // user message
+ expect(isGroupedSystemMessage(result[3])).toBe(false);
+
+ // left group
+ expect(isGroupedSystemMessage(result[4])).toBe(true);
+ expect((result[4] as GroupedSystemMessage).content).toBe("left");
+ expect((result[4] as GroupedSystemMessage).authors).toEqual([
+ "dave",
+ "eve",
+ ]);
+ });
+ });
+
+ describe("timestamp preservation", () => {
+ it("should use first message timestamp in group", () => {
+ const messages: Message[] = [
+ createMessage("1", "system", "reposted", "alice", 1000),
+ createMessage("2", "system", "reposted", "bob", 2000),
+ createMessage("3", "system", "reposted", "charlie", 3000),
+ ];
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ const group = result[0] as GroupedSystemMessage;
+ expect(group.timestamp).toBe(1000); // Should be first message timestamp
+ });
+ });
+
+ describe("large groups", () => {
+ it("should handle large groups efficiently", () => {
+ const messages: Message[] = [];
+ for (let i = 0; i < 100; i++) {
+ messages.push(
+ createMessage(`${i}`, "system", "reposted", `user${i}`, 1000 + i),
+ );
+ }
+
+ const result = groupSystemMessages(messages);
+
+ expect(result).toHaveLength(1);
+ const group = result[0] as GroupedSystemMessage;
+ expect(group.authors).toHaveLength(100);
+ expect(group.messageIds).toHaveLength(100);
+ });
+ });
+});
+
+describe("isGroupedSystemMessage", () => {
+ it("should return true for valid grouped system message", () => {
+ const group: GroupedSystemMessage = {
+ authors: ["alice", "bob"],
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: ["1", "2"],
+ };
+
+ expect(isGroupedSystemMessage(group)).toBe(true);
+ });
+
+ it("should return false for regular message", () => {
+ const message: Message = createMessage("1", "user", "hello", "alice", 1000);
+
+ expect(isGroupedSystemMessage(message)).toBe(false);
+ });
+
+ it("should return false for null", () => {
+ expect(isGroupedSystemMessage(null)).toBe(false);
+ });
+
+ it("should return false for undefined", () => {
+ expect(isGroupedSystemMessage(undefined)).toBe(false);
+ });
+
+ it("should return false for non-object types", () => {
+ expect(isGroupedSystemMessage("string")).toBe(false);
+ expect(isGroupedSystemMessage(123)).toBe(false);
+ expect(isGroupedSystemMessage(true)).toBe(false);
+ });
+
+ it("should return false for objects missing required fields", () => {
+ expect(isGroupedSystemMessage({})).toBe(false);
+ expect(isGroupedSystemMessage({ authors: [] })).toBe(false);
+ expect(isGroupedSystemMessage({ authors: [], content: "test" })).toBe(
+ false,
+ );
+ });
+
+ it("should return false if authors is not an array", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: "alice",
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: ["1"],
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if authors array is empty", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: [],
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: [],
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if messageIds is not an array", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: ["alice"],
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: "1",
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if messageIds array is empty", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: ["alice"],
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: [],
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if authors and messageIds length mismatch", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: ["alice", "bob"],
+ content: "reposted",
+ timestamp: 1000,
+ messageIds: ["1"], // Only 1 ID for 2 authors
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if content is not a string", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: ["alice"],
+ content: 123,
+ timestamp: 1000,
+ messageIds: ["1"],
+ }),
+ ).toBe(false);
+ });
+
+ it("should return false if timestamp is not a number", () => {
+ expect(
+ isGroupedSystemMessage({
+ authors: ["alice"],
+ content: "reposted",
+ timestamp: "1000",
+ messageIds: ["1"],
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/src/lib/chat/group-system-messages.ts b/src/lib/chat/group-system-messages.ts
new file mode 100644
index 0000000..a153a52
--- /dev/null
+++ b/src/lib/chat/group-system-messages.ts
@@ -0,0 +1,95 @@
+import type { Message } from "@/types/chat";
+
+/**
+ * Grouped system message - multiple users doing the same action
+ */
+export interface GroupedSystemMessage {
+ authors: string[]; // pubkeys of users who performed the action
+ content: string; // action text (e.g., "reposted", "joined", "left")
+ timestamp: number; // timestamp of the first message in the group
+ messageIds: string[]; // IDs of all messages in the group
+}
+
+/**
+ * Helper: Group consecutive system messages with the same content
+ *
+ * Takes a list of messages and groups consecutive system messages that have
+ * the same action (content). Non-system messages break the grouping.
+ *
+ * @example
+ * Input: [
+ * { type: "system", content: "reposted", author: "alice" },
+ * { type: "system", content: "reposted", author: "bob" },
+ * { type: "user", content: "hello" },
+ * { type: "system", content: "reposted", author: "charlie" }
+ * ]
+ *
+ * Output: [
+ * { authors: ["alice", "bob"], content: "reposted", ... },
+ * { type: "user", content: "hello" },
+ * { authors: ["charlie"], content: "reposted", ... }
+ * ]
+ */
+export function groupSystemMessages(
+ messages: Message[],
+): Array {
+ const result: Array = [];
+ let currentGroup: GroupedSystemMessage | null = null;
+
+ for (const message of messages) {
+ // Only group system messages (not user or zap messages)
+ if (message.type === "system") {
+ // Check if we can add to current group
+ if (currentGroup && currentGroup.content === message.content) {
+ // Add to existing group
+ currentGroup.authors.push(message.author);
+ currentGroup.messageIds.push(message.id);
+ } else {
+ // Finalize current group if exists
+ if (currentGroup) {
+ result.push(currentGroup);
+ }
+ // Start new group
+ currentGroup = {
+ authors: [message.author],
+ content: message.content,
+ timestamp: message.timestamp,
+ messageIds: [message.id],
+ };
+ }
+ } else {
+ // Non-system message - finalize any pending group
+ if (currentGroup) {
+ result.push(currentGroup);
+ currentGroup = null;
+ }
+ result.push(message);
+ }
+ }
+
+ // Don't forget the last group if exists
+ if (currentGroup) {
+ result.push(currentGroup);
+ }
+
+ return result;
+}
+
+/**
+ * Type guard to check if item is a grouped system message
+ */
+export function isGroupedSystemMessage(
+ item: unknown,
+): item is GroupedSystemMessage {
+ if (!item || typeof item !== "object") return false;
+ const obj = item as Record;
+ return (
+ Array.isArray(obj.authors) &&
+ obj.authors.length > 0 &&
+ typeof obj.content === "string" &&
+ typeof obj.timestamp === "number" &&
+ Array.isArray(obj.messageIds) &&
+ obj.messageIds.length > 0 &&
+ obj.authors.length === obj.messageIds.length
+ );
+}