feat(bookmarks): add thread expansion flags
Adds thread expansion + filtering for bookmarks, with optional ancestor branches and metadata.\n\nCo-authored-by: Klaus <[email protected]>
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.1 — 2026-01-19
|
||||
|
||||
### Added
|
||||
- `bookmarks` thread expansion controls (`--expand-root-only`, `--author-chain`, `--author-only`, `--full-chain-only`, `--include-ancestor-branches`, `--include-parent`, `--thread-meta`, `--sort-chronological`) for richer context exports (#55) — thanks @kkretschmer2.
|
||||
|
||||
### Fixed
|
||||
- `bookmarks` expanded JSON now preserves pagination `nextCursor`, and full-chain filtering only includes ancestor branches when requested.
|
||||
|
||||
## 0.7.0 — 2026-01-12
|
||||
|
||||
### Added
|
||||
|
||||
@@ -63,6 +63,7 @@ bird bookmarks -n 5
|
||||
bird bookmarks --folder-id 123456789123456789 -n 5 # https://x.com/i/bookmarks/<folder-id>
|
||||
bird bookmarks --all --json
|
||||
bird bookmarks --all --max-pages 2 --json
|
||||
bird bookmarks --include-parent --json
|
||||
bird unbookmark 1234567890123456789
|
||||
bird unbookmark https://x.com/user/status/1234567890123456789
|
||||
|
||||
@@ -182,7 +183,7 @@ Fields:
|
||||
- `bird search "<query>" [-n count] [--all] [--max-pages n] [--cursor string] [--json]` — search for tweets matching a query; `--max-pages` requires `--all` or `--cursor`.
|
||||
- `bird mentions [-n count] [--user @handle] [--json]` — find tweets mentioning a user (defaults to the authenticated user).
|
||||
- `bird user-tweets <@handle> [-n count] [--cursor string] [--max-pages n] [--delay ms] [--json]` — get tweets from a user's profile timeline.
|
||||
- `bird bookmarks [-n count] [--folder-id id] [--all] [--max-pages n] [--cursor string] [--json]` — list your bookmarked tweets (or a specific bookmark folder); `--max-pages` requires `--all` or `--cursor`.
|
||||
- `bird bookmarks [-n count] [--folder-id id] [--all] [--max-pages n] [--cursor string] [--expand-root-only] [--author-chain] [--author-only] [--full-chain-only] [--include-ancestor-branches] [--include-parent] [--thread-meta] [--sort-chronological] [--json]` — list your bookmarked tweets (or a specific bookmark folder); expansion flags control thread context; `--max-pages` requires `--all` or `--cursor`.
|
||||
- `bird unbookmark <tweet-id-or-url...>` — remove one or more bookmarks by tweet ID or URL.
|
||||
- `bird likes [-n count] [--all] [--max-pages n] [--cursor string] [--json] [--json-full]` — list your liked tweets; `--max-pages` requires `--all` or `--cursor`.
|
||||
- `bird news [-n count] [--ai-only] [--with-tweets] [--tweets-per-item n] [--for-you] [--news-only] [--sports] [--entertainment] [--trending-only] [--json]` — fetch news and trending topics from X's Explore tabs.
|
||||
@@ -195,6 +196,16 @@ Fields:
|
||||
- `bird whoami` — print which Twitter account your relay belongs to.
|
||||
- `bird check` — verify relay configuration and account access.
|
||||
|
||||
Bookmarks flags:
|
||||
- `--expand-root-only`: expand threads only when the bookmark is a root tweet.
|
||||
- `--author-chain`: keep only the bookmarked author's connected self-reply chain.
|
||||
- `--author-only`: include all tweets from the bookmarked author within the thread.
|
||||
- `--full-chain-only`: keep the entire reply chain connected to the bookmarked tweet (all authors).
|
||||
- `--include-ancestor-branches`: include sibling branches for ancestors when using `--full-chain-only`.
|
||||
- `--include-parent`: include the direct parent tweet for non-root bookmarks.
|
||||
- `--thread-meta`: add thread metadata fields to each tweet.
|
||||
- `--sort-chronological`: sort output globally oldest to newest (default preserves bookmark order).
|
||||
|
||||
Global options:
|
||||
- `--relay-base-url <url>`: twitter safe relay base URL (defaults to `TWITTER_RELAY_BASE_URL`).
|
||||
- `--profile-name <name>`: twitter safe relay profile name (sent as `x-profile-name`).
|
||||
|
||||
+162
-5
@@ -2,7 +2,14 @@ import type { Command } from 'commander';
|
||||
import { parsePaginationFlags } from '../cli/pagination.js';
|
||||
import type { CliContext } from '../cli/shared.js';
|
||||
import { extractBookmarkFolderId } from '../lib/extract-bookmark-folder-id.js';
|
||||
import {
|
||||
addThreadMetadata,
|
||||
filterAuthorChain,
|
||||
filterAuthorOnly,
|
||||
filterFullChain,
|
||||
} from '../lib/thread-filters.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
import type { TweetData, TweetWithMeta } from '../lib/twitter-client-types.js';
|
||||
|
||||
export function registerBookmarksCommand(program: Command, ctx: CliContext): void {
|
||||
program
|
||||
@@ -13,6 +20,14 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
|
||||
.option('--all', 'Fetch all bookmarks (paged)')
|
||||
.option('--max-pages <number>', 'Stop after N pages when using --all')
|
||||
.option('--cursor <string>', 'Resume pagination from a cursor')
|
||||
.option('--expand-root-only', 'Only expand threads when bookmarked tweet is root')
|
||||
.option('--author-chain', 'Only include author self-reply chains connected to the bookmark')
|
||||
.option('--author-only', 'Include all tweets from bookmarked tweet author in thread')
|
||||
.option('--full-chain-only', 'Save entire reply chain connected to the bookmarked tweet')
|
||||
.option('--include-ancestor-branches', 'Include sibling branches for ancestors when using --full-chain-only')
|
||||
.option('--include-parent', 'Include direct parent tweet for non-root bookmarks')
|
||||
.option('--thread-meta', 'Add metadata fields (isThread, threadPosition, etc.)')
|
||||
.option('--sort-chronological', 'Sort output globally oldest -> newest')
|
||||
.option('--json', 'Output as JSON')
|
||||
.option('--json-full', 'Output as JSON with full raw API response in _raw field')
|
||||
.action(
|
||||
@@ -24,6 +39,14 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
|
||||
all?: boolean;
|
||||
maxPages?: string;
|
||||
cursor?: string;
|
||||
expandRootOnly?: boolean;
|
||||
authorChain?: boolean;
|
||||
authorOnly?: boolean;
|
||||
fullChainOnly?: boolean;
|
||||
includeAncestorBranches?: boolean;
|
||||
includeParent?: boolean;
|
||||
threadMeta?: boolean;
|
||||
sortChronological?: boolean;
|
||||
}) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
@@ -65,14 +88,148 @@ export function registerBookmarksCommand(program: Command, ctx: CliContext): voi
|
||||
? await client.getAllBookmarks(paginationOptions)
|
||||
: await client.getBookmarks(count, timelineOptions);
|
||||
|
||||
if (result.success) {
|
||||
const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.';
|
||||
const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull);
|
||||
ctx.printTweetsResult(result, { json: isJson, usePagination, emptyMessage });
|
||||
} else {
|
||||
if (!result.success) {
|
||||
console.error(`${ctx.p('err')}Failed to fetch bookmarks: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (cmdOpts.authorChain && (cmdOpts.authorOnly || cmdOpts.fullChainOnly)) {
|
||||
console.error(
|
||||
`${ctx.p('warn')}--author-chain already limits to the connected self-reply chain; ` +
|
||||
'other chain filters are redundant.',
|
||||
);
|
||||
}
|
||||
if (cmdOpts.includeAncestorBranches && !cmdOpts.fullChainOnly) {
|
||||
console.error(`${ctx.p('warn')}--include-ancestor-branches only applies with --full-chain-only.`);
|
||||
}
|
||||
|
||||
const bookmarks = result.tweets;
|
||||
if (!bookmarks || bookmarks.length === 0) {
|
||||
const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.';
|
||||
const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull);
|
||||
ctx.printTweetsResult(result, { json: isJson, usePagination, emptyMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
const expandedResults: TweetData[] = [];
|
||||
const threadCache = new Map<string, TweetData[]>();
|
||||
const includeMeta = Boolean(cmdOpts.threadMeta);
|
||||
const includeParent = Boolean(cmdOpts.includeParent);
|
||||
const expandRootOnly = Boolean(cmdOpts.expandRootOnly);
|
||||
const filterAuthorChainFlag = Boolean(cmdOpts.authorChain);
|
||||
const filterAuthorOnlyFlag = Boolean(cmdOpts.authorOnly);
|
||||
const filterFullChainFlag = Boolean(cmdOpts.fullChainOnly);
|
||||
const includeAncestorBranches = Boolean(cmdOpts.includeAncestorBranches) && filterFullChainFlag;
|
||||
const useChronologicalSort = Boolean(cmdOpts.sortChronological);
|
||||
|
||||
const shouldAttemptExpand =
|
||||
expandRootOnly || filterAuthorChainFlag || filterAuthorOnlyFlag || filterFullChainFlag;
|
||||
const shouldFetchThread = shouldAttemptExpand || includeMeta;
|
||||
|
||||
const fetchThread = async (tweet: TweetData): Promise<TweetData[] | null> => {
|
||||
const cachedKey = tweet.conversationId ?? tweet.id;
|
||||
const cached = threadCache.get(cachedKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const threadResult = await client.getThread(tweet.id, { includeRaw });
|
||||
if (!threadResult.success) {
|
||||
console.error(
|
||||
`${ctx.p('warn')}Failed to expand thread for ${tweet.id}: ${threadResult.error ?? 'Unknown error'}`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
if (!threadResult.tweets) {
|
||||
console.error(`${ctx.p('warn')}No thread tweets returned for ${tweet.id}.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const rootKey = threadResult.tweets[0]?.conversationId ?? cachedKey;
|
||||
threadCache.set(rootKey, threadResult.tweets);
|
||||
return threadResult.tweets;
|
||||
};
|
||||
|
||||
const delayBetweenExpansionsMs = 1000;
|
||||
|
||||
for (let index = 0; index < bookmarks.length; index += 1) {
|
||||
const bookmark = bookmarks[index];
|
||||
const isRoot = !bookmark.inReplyToStatusId;
|
||||
let threadTweets: TweetData[] | null = null;
|
||||
|
||||
if (shouldFetchThread) {
|
||||
if (!expandRootOnly || isRoot || includeMeta) {
|
||||
if (index > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, delayBetweenExpansionsMs));
|
||||
}
|
||||
threadTweets = await fetchThread(bookmark);
|
||||
}
|
||||
}
|
||||
|
||||
let outputTweets: TweetData[] = [bookmark];
|
||||
|
||||
if (shouldAttemptExpand) {
|
||||
if (expandRootOnly && !isRoot) {
|
||||
outputTweets = [bookmark];
|
||||
} else if (threadTweets) {
|
||||
if (filterAuthorChainFlag) {
|
||||
outputTweets = filterAuthorChain(threadTweets, bookmark);
|
||||
} else {
|
||||
outputTweets = filterFullChainFlag
|
||||
? filterFullChain(threadTweets, bookmark, { includeAncestorBranches })
|
||||
: threadTweets;
|
||||
if (filterAuthorOnlyFlag) {
|
||||
outputTweets = filterAuthorOnly(outputTweets, bookmark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (includeParent && bookmark.inReplyToStatusId) {
|
||||
const alreadyIncluded = outputTweets.some((tweet) => tweet.id === bookmark.inReplyToStatusId);
|
||||
if (!alreadyIncluded) {
|
||||
const parentFromThread = threadTweets?.find((tweet) => tweet.id === bookmark.inReplyToStatusId);
|
||||
if (parentFromThread) {
|
||||
expandedResults.push(parentFromThread);
|
||||
} else {
|
||||
const parentResult = await client.getTweet(bookmark.inReplyToStatusId, { includeRaw });
|
||||
if (parentResult.success && parentResult.tweet) {
|
||||
expandedResults.push(parentResult.tweet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expandedResults.push(...outputTweets);
|
||||
}
|
||||
|
||||
let finalResults: Array<TweetData | TweetWithMeta> = expandedResults;
|
||||
if (includeMeta) {
|
||||
finalResults = expandedResults.map((tweet) => {
|
||||
const cacheKey = tweet.conversationId ?? tweet.id;
|
||||
let conversationTweets = threadCache.get(cacheKey);
|
||||
if (!conversationTweets) {
|
||||
conversationTweets = [tweet];
|
||||
}
|
||||
return addThreadMetadata(tweet, conversationTweets);
|
||||
});
|
||||
}
|
||||
|
||||
const uniqueTweets = Array.from(new Map(finalResults.map((tweet) => [tweet.id, tweet])).values());
|
||||
if (useChronologicalSort) {
|
||||
uniqueTweets.sort((a, b) => {
|
||||
const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
|
||||
const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
|
||||
return aTime - bTime;
|
||||
});
|
||||
}
|
||||
|
||||
const emptyMessage = folderId ? 'No bookmarks found in folder.' : 'No bookmarks found.';
|
||||
const isJson = Boolean(cmdOpts.json || cmdOpts.jsonFull);
|
||||
ctx.printTweetsResult(
|
||||
{ tweets: uniqueTweets, nextCursor: result.nextCursor },
|
||||
{ json: isJson, usePagination, emptyMessage },
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { TweetData, TweetWithMeta } from './twitter-client-types.js';
|
||||
|
||||
const sortByCreatedAt = (tweets: TweetData[]): TweetData[] =>
|
||||
tweets.slice().sort((a, b) => {
|
||||
const aTime = a.createdAt ? Date.parse(a.createdAt) : 0;
|
||||
const bTime = b.createdAt ? Date.parse(b.createdAt) : 0;
|
||||
return aTime - bTime;
|
||||
});
|
||||
|
||||
export function filterAuthorChain(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[] {
|
||||
const author = bookmarkedTweet.author.username;
|
||||
const byId = new Map(tweets.map((tweet) => [tweet.id, tweet]));
|
||||
const chainIds = new Set<string>();
|
||||
|
||||
let current: TweetData | undefined = bookmarkedTweet;
|
||||
while (current && current.author.username === author) {
|
||||
chainIds.add(current.id);
|
||||
const parentId = current.inReplyToStatusId;
|
||||
if (!parentId) {
|
||||
break;
|
||||
}
|
||||
const parent = byId.get(parentId);
|
||||
if (!parent || parent.author.username !== author) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
let changed = true;
|
||||
while (changed) {
|
||||
changed = false;
|
||||
for (const tweet of tweets) {
|
||||
if (tweet.author.username !== author) {
|
||||
continue;
|
||||
}
|
||||
if (chainIds.has(tweet.id)) {
|
||||
continue;
|
||||
}
|
||||
if (tweet.inReplyToStatusId && chainIds.has(tweet.inReplyToStatusId)) {
|
||||
chainIds.add(tweet.id);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id)));
|
||||
}
|
||||
|
||||
export function filterAuthorOnly(tweets: TweetData[], bookmarkedTweet: TweetData): TweetData[] {
|
||||
const author = bookmarkedTweet.author.username;
|
||||
return tweets.filter((tweet) => tweet.author.username === author);
|
||||
}
|
||||
|
||||
export function filterFullChain(
|
||||
tweets: TweetData[],
|
||||
bookmarkedTweet: TweetData,
|
||||
options: { includeAncestorBranches?: boolean } = {},
|
||||
): TweetData[] {
|
||||
const byId = new Map(tweets.map((tweet) => [tweet.id, tweet]));
|
||||
const repliesByParent = new Map<string, TweetData[]>();
|
||||
|
||||
for (const tweet of tweets) {
|
||||
if (!tweet.inReplyToStatusId) {
|
||||
continue;
|
||||
}
|
||||
const list = repliesByParent.get(tweet.inReplyToStatusId) ?? [];
|
||||
list.push(tweet);
|
||||
repliesByParent.set(tweet.inReplyToStatusId, list);
|
||||
}
|
||||
|
||||
const chainIds = new Set<string>();
|
||||
const ancestorIds: string[] = [];
|
||||
|
||||
chainIds.add(bookmarkedTweet.id);
|
||||
let current: TweetData | undefined = bookmarkedTweet;
|
||||
while (current?.inReplyToStatusId) {
|
||||
const parent = byId.get(current.inReplyToStatusId);
|
||||
if (!parent) {
|
||||
break;
|
||||
}
|
||||
if (!chainIds.has(parent.id)) {
|
||||
chainIds.add(parent.id);
|
||||
ancestorIds.push(parent.id);
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
const addDescendants = (startIds: string[]) => {
|
||||
const queue = [...startIds];
|
||||
while (queue.length > 0) {
|
||||
const currentId = queue.shift();
|
||||
if (!currentId) {
|
||||
continue;
|
||||
}
|
||||
if (!chainIds.has(currentId)) {
|
||||
chainIds.add(currentId);
|
||||
}
|
||||
const replies = repliesByParent.get(currentId) ?? [];
|
||||
for (const reply of replies) {
|
||||
if (chainIds.has(reply.id)) {
|
||||
continue;
|
||||
}
|
||||
chainIds.add(reply.id);
|
||||
queue.push(reply.id);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addDescendants([bookmarkedTweet.id]);
|
||||
|
||||
if (options.includeAncestorBranches) {
|
||||
for (const ancestorId of ancestorIds) {
|
||||
addDescendants([ancestorId]);
|
||||
}
|
||||
}
|
||||
|
||||
return sortByCreatedAt(tweets.filter((tweet) => chainIds.has(tweet.id)));
|
||||
}
|
||||
|
||||
export function addThreadMetadata(tweet: TweetData, allConversationTweets: TweetData[]): TweetWithMeta {
|
||||
const author = tweet.author.username;
|
||||
const hasSelfReplies = allConversationTweets.some(
|
||||
(candidate) => candidate.inReplyToStatusId === tweet.id && candidate.author.username === author,
|
||||
);
|
||||
|
||||
const isRoot = !tweet.inReplyToStatusId;
|
||||
let threadPosition: 'root' | 'middle' | 'end' | 'standalone';
|
||||
|
||||
if (isRoot && !hasSelfReplies) {
|
||||
threadPosition = 'standalone';
|
||||
} else if (isRoot && hasSelfReplies) {
|
||||
threadPosition = 'root';
|
||||
} else if (!isRoot && hasSelfReplies) {
|
||||
threadPosition = 'middle';
|
||||
} else {
|
||||
threadPosition = 'end';
|
||||
}
|
||||
|
||||
return {
|
||||
...tweet,
|
||||
isThread: hasSelfReplies || !isRoot,
|
||||
threadPosition,
|
||||
hasSelfReplies,
|
||||
threadRootId: tweet.conversationId ?? null,
|
||||
};
|
||||
}
|
||||
@@ -284,6 +284,13 @@ export interface TweetData {
|
||||
_raw?: GraphqlTweetResult;
|
||||
}
|
||||
|
||||
export interface TweetWithMeta extends TweetData {
|
||||
isThread: boolean;
|
||||
threadPosition: 'root' | 'middle' | 'end' | 'standalone';
|
||||
hasSelfReplies: boolean;
|
||||
threadRootId: string | null;
|
||||
}
|
||||
|
||||
export interface GetTweetResult {
|
||||
success: boolean;
|
||||
tweet?: TweetData;
|
||||
|
||||
@@ -247,6 +247,28 @@ d('live CLI (Twitter/X) all commands', () => {
|
||||
expect(Array.isArray(bookmarkTweets)).toBe(true);
|
||||
});
|
||||
|
||||
it('bookmarks full-chain-only with ancestor branches returns JSON array', async () => {
|
||||
const bookmarks = await runBird(
|
||||
[
|
||||
...baseArgs,
|
||||
'--cookie-timeout',
|
||||
cookieTimeoutArg,
|
||||
'bookmarks',
|
||||
'-n',
|
||||
'1',
|
||||
'--full-chain-only',
|
||||
'--include-ancestor-branches',
|
||||
'--json',
|
||||
],
|
||||
{
|
||||
timeoutMs: 45_000,
|
||||
},
|
||||
);
|
||||
expect(bookmarks.exitCode).toBe(0);
|
||||
const bookmarkTweets = parseJson<Array<{ id?: string }>>(bookmarks.stdout);
|
||||
expect(Array.isArray(bookmarkTweets)).toBe(true);
|
||||
});
|
||||
|
||||
it('likes returns JSON array', async () => {
|
||||
const likes = await runBird([...baseArgs, 'likes', '-n', '10', '--json'], {
|
||||
timeoutMs: 45_000,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { filterFullChain } from '../src/lib/thread-filters.js';
|
||||
import type { TweetData } from '../src/lib/twitter-client-types.js';
|
||||
|
||||
const makeTweet = (
|
||||
id: string,
|
||||
createdAt: string,
|
||||
inReplyToStatusId?: string,
|
||||
conversationId = '1',
|
||||
): TweetData => ({
|
||||
id,
|
||||
text: id,
|
||||
author: { username: 'alice', name: 'Alice' },
|
||||
createdAt,
|
||||
inReplyToStatusId,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
describe('filterFullChain', () => {
|
||||
it('returns ancestors + descendants from the bookmark only', () => {
|
||||
const root = makeTweet('1', '2020-01-01T00:00:00Z');
|
||||
const replyA = makeTweet('2', '2020-01-02T00:00:00Z', '1');
|
||||
const bookmark = makeTweet('3', '2020-01-03T00:00:00Z', '2');
|
||||
const childA = makeTweet('4', '2020-01-04T00:00:00Z', '3');
|
||||
const childB = makeTweet('7', '2020-01-05T00:00:00Z', '4');
|
||||
const siblingOfAncestor = makeTweet('5', '2020-01-06T00:00:00Z', '2');
|
||||
const siblingOfRoot = makeTweet('6', '2020-01-07T00:00:00Z', '1');
|
||||
|
||||
const tweets = [siblingOfAncestor, childB, root, siblingOfRoot, bookmark, childA, replyA];
|
||||
|
||||
const result = filterFullChain(tweets, bookmark);
|
||||
expect(result.map((tweet) => tweet.id)).toEqual(['1', '2', '3', '4', '7']);
|
||||
});
|
||||
|
||||
it('includes ancestor branches when requested', () => {
|
||||
const root = makeTweet('1', '2020-01-01T00:00:00Z');
|
||||
const replyA = makeTweet('2', '2020-01-02T00:00:00Z', '1');
|
||||
const bookmark = makeTweet('3', '2020-01-03T00:00:00Z', '2');
|
||||
const childA = makeTweet('4', '2020-01-04T00:00:00Z', '3');
|
||||
const childB = makeTweet('7', '2020-01-05T00:00:00Z', '4');
|
||||
const siblingOfAncestor = makeTweet('5', '2020-01-06T00:00:00Z', '2');
|
||||
const siblingOfRoot = makeTweet('6', '2020-01-07T00:00:00Z', '1');
|
||||
|
||||
const tweets = [siblingOfRoot, childA, replyA, childB, bookmark, root, siblingOfAncestor];
|
||||
|
||||
const result = filterFullChain(tweets, bookmark, { includeAncestorBranches: true });
|
||||
expect(result.map((tweet) => tweet.id)).toEqual(['1', '2', '3', '4', '7', '5', '6']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user