[Threads V2] Preliminary integration of unspecced V2 APIs (#8443)
* WIP * Sorting working * Rough handling of hidden/muted * Better muted/hidden sorting and handling * Clarify some naming * Fix parents * Handle first reply under highlighted/composer * WIP RaW * WIP optimistic * Optimistic WIP * Little cleanup, inserting dupes * Re-org * Add in new optimistic insert logic * Update types * Sorta working linear view optimistic state * Simple working version, no pref for OP * Working optimistic reply insertions, preference for OP * Ensure deletes are coming through * WIP scroll handling * WIP scroll tweaks * Clean up scrolling * Clean up onPostSuccess * Add annotations * Fix highlighted post calc * WIP kill me * Update APIs * Nvm don't kill me * Fix optimistic insert * Handle read more cases in tree view * Basically working read more * Handle linear view * Reorg * More reorg * Split up thread post components * New reply tree layout * Fix up traversal metadata * Tighten some spacing * Use indent ya idiot * Some linear mode cleanup * Fix lines on read more items * Vibe coding to success * Almost there with read mores * Update APIs * Bump sdk * Update import * Checkpoint new traversal * Checkpoint cleanup * Checkpoint, need to fix blocked posts * Checkpoint: think we're good, needs more cleanup * Clean it up * Two passes only * Set to default params, update comment * Fix render bug on native * Checkpoint parent rendering, can opt for slower handling here * Clean up parent handling, reply handling * Fix read more extra space * Fix read more in linear view * Fix hidden reply handling, seen count, before/after calc * Update naming * Rename Slice to ThreadItem * Add basic post and anchor skeletons * Refactor client-side hidden * WIP hidden fetching * Update types * Clean up query a bit * Scrolling still broken * Ok maybe fix scrolling * Checkpoint move state into meta query * Don't load remote hidden items unless needed * skeleton view * Reset hidden items when params change * Split up traversal and avoid multiple passes * Clean up * Checkpoint: handling exhausted replies * Clean up traversal functions further * Clean up pagination * Limit optimistic reply depth * Handle optimistic insert in hidden replies * Share root query key for easier cache extraction * Make blurred posts not look like ass * Fix double deleted item * Make optimistic deleted state not look like crap in tree view * Fix parents traversal 4 real * Rename tree post * Make optimistic deletions of linear posts not look bad * Rename linear post components * Handle tombstone views * Rename read more component * Add moreParents handling * Align interaction states of read more * Fix read more on FF * Tree view skeleton * Reply composer skele * Remove hack for showing more replies * Checkpoint: sort change scrolling fixed * Checkpoint: learned new things, reset to base * Feature gate * Rename * Replace show more * Update settings screen * Update pkg and endpoint * Remove console * Eureka * Cleanup last commit * No tests atm * Remove scroll provider * Clean up callbacks, better error state * Remove todo * Remove todo * Remove todos * Format * Ok I think scrolling is solid * Add back mobile compose input * Ok need to compute headerHeight every time * Update comments * Ok button up web too * Threads v2 tweaks (#8467) * fix error screen collapsing * use personx icon for blocked posts * Remove height/width * Revert unused Header change * Clarify code * Relate consts to theme values * Remove debug code * Typo * Fix debounce of threads prefs * Update metadata comments, dev mode * Missed a spot * Clean up todo * Fix up no-unauthenticated posts * Truncate parents if no-unauth * Update getBranch docs * Remove debug code * Expand fetching in some cases * Clear scroll need for root post to fix jump bug * Fix reply composer skeleton state * Remove uneeded initialized value * Add profile shadow cache * Some metrics * prettier tweak * eslint ignore * Fix optimistic insertion * Typo * Rename, comment * Remove wait * Counter naming * Replies seen counter for moderated sub-trees * Remove borders on skeleton * Align tombstone with optimistic deletion state * Fix optimistic deletion for thread * Add tree view icon * Rename * Cleanup * Update settings copy * Header menu open metric * Bump package * Better reply prompt (#8474) * restyle reply prompt * hide bottom bar border for cleaner look * use new border hiding hook in DMs * create `transparentifyColor` function * adjust padding * fix padding in immersive lpayer * Apply suggestions from code review Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com> * Integrate post-source (cherry picked from commit fe053e9b38395a4fcb30a4367bc800f64ea84fe9) --------- Co-authored-by: Samuel Newman <mozzius@protonmail.com> Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,539 @@
|
||||
/* eslint-disable no-labels */
|
||||
import {AppBskyUnspeccedDefs, type ModerationOpts} from '@atproto/api'
|
||||
|
||||
import {
|
||||
type ApiThreadItem,
|
||||
type PostThreadParams,
|
||||
type ThreadItem,
|
||||
type TraversalMetadata,
|
||||
} from '#/state/queries/usePostThread/types'
|
||||
import {
|
||||
getPostRecord,
|
||||
getThreadPostNoUnauthenticatedUI,
|
||||
getThreadPostUI,
|
||||
getTraversalMetadata,
|
||||
storeTraversalMetadata,
|
||||
} from '#/state/queries/usePostThread/utils'
|
||||
import * as views from '#/state/queries/usePostThread/views'
|
||||
|
||||
export function sortAndAnnotateThreadItems(
|
||||
thread: ApiThreadItem[],
|
||||
{
|
||||
threadgateHiddenReplies,
|
||||
moderationOpts,
|
||||
view,
|
||||
skipModerationHandling,
|
||||
}: {
|
||||
threadgateHiddenReplies: Set<string>
|
||||
moderationOpts: ModerationOpts
|
||||
view: PostThreadParams['view']
|
||||
/**
|
||||
* Set to `true` in cases where we already know the moderation state of the
|
||||
* post e.g. when fetching additional replies from the server. This will
|
||||
* prevent additional sorting or nested-branch truncation, and all replies,
|
||||
* regardless of moderation state, will be included in the resulting
|
||||
* `threadItems` array.
|
||||
*/
|
||||
skipModerationHandling?: boolean
|
||||
},
|
||||
) {
|
||||
const threadItems: ThreadItem[] = []
|
||||
const otherThreadItems: ThreadItem[] = []
|
||||
const metadatas = new Map<string, TraversalMetadata>()
|
||||
|
||||
traversal: for (let i = 0; i < thread.length; i++) {
|
||||
const item = thread[i]
|
||||
let parentMetadata: TraversalMetadata | undefined
|
||||
let metadata: TraversalMetadata | undefined
|
||||
|
||||
if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
|
||||
parentMetadata = metadatas.get(
|
||||
getPostRecord(item.value.post).reply?.parent?.uri || '',
|
||||
)
|
||||
metadata = getTraversalMetadata({
|
||||
item,
|
||||
parentMetadata,
|
||||
prevItem: thread.at(i - 1),
|
||||
nextItem: thread.at(i + 1),
|
||||
})
|
||||
storeTraversalMetadata(metadatas, metadata)
|
||||
}
|
||||
|
||||
if (item.depth < 0) {
|
||||
/*
|
||||
* Parents are ignored until we find the anchor post, then we walk
|
||||
* _up_ from there.
|
||||
*/
|
||||
} else if (item.depth === 0) {
|
||||
if (AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value)) {
|
||||
threadItems.push(views.threadPostNoUnauthenticated(item))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(item.value)) {
|
||||
threadItems.push(views.threadPostNotFound(item))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)) {
|
||||
threadItems.push(views.threadPostBlocked(item))
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
|
||||
const post = views.threadPost({
|
||||
uri: item.uri,
|
||||
depth: item.depth,
|
||||
value: item.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
threadItems.push(post)
|
||||
|
||||
parentTraversal: for (let pi = i - 1; pi >= 0; pi--) {
|
||||
const parent = thread[pi]
|
||||
|
||||
if (
|
||||
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(parent.value)
|
||||
) {
|
||||
const post = views.threadPostNoUnauthenticated(parent)
|
||||
post.ui = getThreadPostNoUnauthenticatedUI({
|
||||
depth: parent.depth,
|
||||
// ignore for now
|
||||
// prevItemDepth: thread[pi - 1]?.depth,
|
||||
nextItemDepth: thread[pi + 1]?.depth,
|
||||
})
|
||||
threadItems.unshift(post)
|
||||
// for now, break parent traversal at first no-unauthed
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemNotFound(parent.value)) {
|
||||
threadItems.unshift(views.threadPostNotFound(parent))
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemBlocked(parent.value)) {
|
||||
threadItems.unshift(views.threadPostBlocked(parent))
|
||||
break parentTraversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(parent.value)) {
|
||||
threadItems.unshift(
|
||||
views.threadPost({
|
||||
uri: parent.uri,
|
||||
depth: parent.depth,
|
||||
value: parent.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (item.depth > 0) {
|
||||
/*
|
||||
* The API does not send down any unavailable replies, so this will
|
||||
* always be false (for now). If we ever wanted to tombstone them here,
|
||||
* we could.
|
||||
*/
|
||||
const shouldBreak =
|
||||
AppBskyUnspeccedDefs.isThreadItemNoUnauthenticated(item.value) ||
|
||||
AppBskyUnspeccedDefs.isThreadItemNotFound(item.value) ||
|
||||
AppBskyUnspeccedDefs.isThreadItemBlocked(item.value)
|
||||
|
||||
if (shouldBreak) {
|
||||
const branch = getBranch(thread, i, item.depth)
|
||||
// could insert tombstone
|
||||
i = branch.end
|
||||
continue traversal
|
||||
} else if (AppBskyUnspeccedDefs.isThreadItemPost(item.value)) {
|
||||
if (parentMetadata) {
|
||||
/*
|
||||
* Set this value before incrementing the parent's repliesSeenCounter
|
||||
*/
|
||||
metadata!.replyIndex = parentMetadata.repliesIndexCounter
|
||||
// Increment the parent's repliesIndexCounter
|
||||
parentMetadata.repliesIndexCounter += 1
|
||||
}
|
||||
|
||||
const post = views.threadPost({
|
||||
uri: item.uri,
|
||||
depth: item.depth,
|
||||
value: item.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
|
||||
if (!post.isBlurred || skipModerationHandling) {
|
||||
/*
|
||||
* Not moderated, need to insert it
|
||||
*/
|
||||
threadItems.push(post)
|
||||
|
||||
/*
|
||||
* Update seen reply count of parent
|
||||
*/
|
||||
if (parentMetadata) {
|
||||
parentMetadata.repliesSeenCounter += 1
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Moderated in some way, we're going to walk children
|
||||
*/
|
||||
const parent = post
|
||||
const parentIsTopLevelReply = parent.depth === 1
|
||||
// get sub tree
|
||||
const branch = getBranch(thread, i, item.depth)
|
||||
|
||||
if (parentIsTopLevelReply) {
|
||||
// push branch anchor into sorted array
|
||||
otherThreadItems.push(parent)
|
||||
// skip branch anchor in branch traversal
|
||||
const startIndex = branch.start + 1
|
||||
|
||||
for (let ci = startIndex; ci <= branch.end; ci++) {
|
||||
const child = thread[ci]
|
||||
|
||||
if (AppBskyUnspeccedDefs.isThreadItemPost(child.value)) {
|
||||
const childParentMetadata = metadatas.get(
|
||||
getPostRecord(child.value.post).reply?.parent?.uri || '',
|
||||
)
|
||||
const childMetadata = getTraversalMetadata({
|
||||
item: child,
|
||||
prevItem: thread[ci - 1],
|
||||
nextItem: thread[ci + 1],
|
||||
parentMetadata: childParentMetadata,
|
||||
})
|
||||
storeTraversalMetadata(metadatas, childMetadata)
|
||||
if (childParentMetadata) {
|
||||
/*
|
||||
* Set this value before incrementing the parent's repliesIndexCounter
|
||||
*/
|
||||
childMetadata!.replyIndex =
|
||||
childParentMetadata.repliesIndexCounter
|
||||
childParentMetadata.repliesIndexCounter += 1
|
||||
}
|
||||
|
||||
const childPost = views.threadPost({
|
||||
uri: child.uri,
|
||||
depth: child.depth,
|
||||
value: child.value,
|
||||
moderationOpts,
|
||||
threadgateHiddenReplies,
|
||||
})
|
||||
|
||||
/*
|
||||
* If a child is moderated in any way, drop it an its sub-branch
|
||||
* entirely. To reveal these, the user must navigate to the
|
||||
* parent post directly.
|
||||
*/
|
||||
if (childPost.isBlurred) {
|
||||
ci = getBranch(thread, ci, child.depth).end
|
||||
} else {
|
||||
otherThreadItems.push(childPost)
|
||||
|
||||
if (childParentMetadata) {
|
||||
childParentMetadata.repliesSeenCounter += 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Drop the rest of the branch if we hit anything unexpected
|
||||
*/
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Skip to next branch
|
||||
*/
|
||||
i = branch.end
|
||||
continue traversal
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Both `threadItems` and `otherThreadItems` now need to be traversed again to fully compute
|
||||
* UI state based on collected metadata. These arrays will be muted in situ.
|
||||
*/
|
||||
for (const subset of [threadItems, otherThreadItems]) {
|
||||
for (let i = 0; i < subset.length; i++) {
|
||||
const item = subset[i]
|
||||
const prevItem = subset.at(i - 1)
|
||||
const nextItem = subset.at(i + 1)
|
||||
|
||||
if (item.type === 'threadPost') {
|
||||
const metadata = metadatas.get(item.uri)
|
||||
|
||||
if (metadata) {
|
||||
if (metadata.parentMetadata) {
|
||||
/*
|
||||
* Track what's before/after now that we've applied moderation
|
||||
*/
|
||||
if (prevItem?.type === 'threadPost')
|
||||
metadata.prevItemDepth = prevItem?.depth
|
||||
if (nextItem?.type === 'threadPost')
|
||||
metadata.nextItemDepth = nextItem?.depth
|
||||
|
||||
/*
|
||||
* We can now officially calculate `isLastSibling` and `isLastChild`
|
||||
* based on the actual data that we've seen.
|
||||
*/
|
||||
metadata.isLastSibling =
|
||||
metadata.replyIndex ===
|
||||
metadata.parentMetadata.repliesSeenCounter - 1
|
||||
metadata.isLastChild =
|
||||
metadata.nextItemDepth === undefined ||
|
||||
metadata.nextItemDepth <= metadata.depth
|
||||
|
||||
/*
|
||||
* If this is the last sibling, it's implicitly part of the last
|
||||
* branch of this sub-tree.
|
||||
*/
|
||||
if (metadata.isLastSibling) {
|
||||
metadata.isPartOfLastBranchFromDepth = metadata.depth
|
||||
|
||||
/**
|
||||
* If the parent is part of the last branch of the sub-tree, so is the child.
|
||||
*/
|
||||
if (metadata.parentMetadata.isPartOfLastBranchFromDepth) {
|
||||
metadata.isPartOfLastBranchFromDepth =
|
||||
metadata.parentMetadata.isPartOfLastBranchFromDepth
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If this is the last sibling, and the parent has unhydrated replies,
|
||||
* at some point down the line we will need to show a "read more".
|
||||
*/
|
||||
if (
|
||||
metadata.parentMetadata.repliesUnhydrated > 0 &&
|
||||
metadata.isLastSibling
|
||||
) {
|
||||
metadata.upcomingParentReadMore = metadata.parentMetadata
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy in the parent's upcoming read more, if it exists. Once we
|
||||
* reach the bottom, we'll insert a "read more"
|
||||
*/
|
||||
if (metadata.parentMetadata.upcomingParentReadMore) {
|
||||
metadata.upcomingParentReadMore =
|
||||
metadata.parentMetadata.upcomingParentReadMore
|
||||
}
|
||||
|
||||
/*
|
||||
* Copy in the parent's skipped indents
|
||||
*/
|
||||
metadata.skippedIndentIndices = new Set([
|
||||
...metadata.parentMetadata.skippedIndentIndices,
|
||||
])
|
||||
|
||||
/**
|
||||
* If this is the last sibling, and the parent has no unhydrated
|
||||
* replies, then we know we can skip an indent line.
|
||||
*/
|
||||
if (
|
||||
metadata.parentMetadata.repliesUnhydrated <= 0 &&
|
||||
metadata.isLastSibling
|
||||
) {
|
||||
/**
|
||||
* Depth is 2 more than the 0-index of the indent calculation
|
||||
* bc of how we render these. So instead of handling that in the
|
||||
* component, we just adjust that back to 0-index here.
|
||||
*/
|
||||
metadata.skippedIndentIndices.add(item.depth - 2)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If this post has unhydrated replies, and it is the last child, then
|
||||
* it itself needs a "read more"
|
||||
*/
|
||||
if (metadata.repliesUnhydrated > 0 && metadata.isLastChild) {
|
||||
metadata.precedesChildReadMore = true
|
||||
subset.splice(i + 1, 0, views.readMore(metadata))
|
||||
i++ // skip next iteration
|
||||
}
|
||||
|
||||
/*
|
||||
* Tree-view only.
|
||||
*
|
||||
* If there's an upcoming parent read more, this branch is part of the
|
||||
* last branch of the sub-tree, and the item itself is the last child,
|
||||
* insert the parent "read more".
|
||||
*/
|
||||
if (
|
||||
view === 'tree' &&
|
||||
metadata.upcomingParentReadMore &&
|
||||
metadata.isPartOfLastBranchFromDepth ===
|
||||
metadata.upcomingParentReadMore.depth &&
|
||||
metadata.isLastChild
|
||||
) {
|
||||
subset.splice(
|
||||
i + 1,
|
||||
0,
|
||||
views.readMore(metadata.upcomingParentReadMore),
|
||||
)
|
||||
i++
|
||||
}
|
||||
|
||||
/**
|
||||
* Only occurs for the first item in the thread, which may have
|
||||
* additional parents not included in this request.
|
||||
*/
|
||||
if (item.value.moreParents) {
|
||||
metadata.followsReadMoreUp = true
|
||||
subset.splice(i, 0, views.readMoreUp(metadata))
|
||||
i++
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate the final UI state for the thread item.
|
||||
*/
|
||||
item.ui = getThreadPostUI(metadata)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
threadItems,
|
||||
otherThreadItems,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildThread({
|
||||
threadItems,
|
||||
otherThreadItems,
|
||||
serverOtherThreadItems,
|
||||
isLoading,
|
||||
hasSession,
|
||||
otherItemsVisible,
|
||||
hasOtherThreadItems,
|
||||
showOtherItems,
|
||||
}: {
|
||||
threadItems: ThreadItem[]
|
||||
otherThreadItems: ThreadItem[]
|
||||
serverOtherThreadItems: ThreadItem[]
|
||||
isLoading: boolean
|
||||
hasSession: boolean
|
||||
otherItemsVisible: boolean
|
||||
hasOtherThreadItems: boolean
|
||||
showOtherItems: () => void
|
||||
}) {
|
||||
/**
|
||||
* `threadItems` is memoized here, so don't mutate it directly.
|
||||
*/
|
||||
const items = [...threadItems]
|
||||
|
||||
if (isLoading) {
|
||||
const anchorPost = items.at(0)
|
||||
const hasAnchorFromCache = anchorPost && anchorPost.type === 'threadPost'
|
||||
const skeletonReplies = hasAnchorFromCache
|
||||
? anchorPost.value.post.replyCount ?? 4
|
||||
: 4
|
||||
|
||||
if (!items.length) {
|
||||
items.push(
|
||||
views.skeleton({
|
||||
key: 'anchor-skeleton',
|
||||
item: 'anchor',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
if (hasSession) {
|
||||
// we might have this from cache
|
||||
const replyDisabled =
|
||||
hasAnchorFromCache &&
|
||||
anchorPost.value.post.viewer?.replyDisabled === true
|
||||
|
||||
if (hasAnchorFromCache) {
|
||||
if (!replyDisabled) {
|
||||
items.push({
|
||||
type: 'replyComposer',
|
||||
key: 'replyComposer',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
items.push(
|
||||
views.skeleton({
|
||||
key: 'replyComposer',
|
||||
item: 'replyComposer',
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < skeletonReplies; i++) {
|
||||
items.push(
|
||||
views.skeleton({
|
||||
key: `anchor-skeleton-reply-${i}`,
|
||||
item: 'reply',
|
||||
}),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
if (
|
||||
item.type === 'threadPost' &&
|
||||
item.depth === 0 &&
|
||||
!item.value.post.viewer?.replyDisabled &&
|
||||
hasSession
|
||||
) {
|
||||
items.splice(i + 1, 0, {
|
||||
type: 'replyComposer',
|
||||
key: 'replyComposer',
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (otherThreadItems.length || hasOtherThreadItems) {
|
||||
if (otherItemsVisible) {
|
||||
items.push(...otherThreadItems)
|
||||
items.push(...serverOtherThreadItems)
|
||||
} else {
|
||||
items.push({
|
||||
type: 'showOtherReplies',
|
||||
key: 'showOtherReplies',
|
||||
onPress: showOtherItems,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the start and end index of a "branch" of the thread. A "branch" is a
|
||||
* parent and it's children (not siblings). Returned indices are inclusive of
|
||||
* the parent and its last child.
|
||||
*
|
||||
* items[] (index, depth)
|
||||
* └─┬ anchor ──────── (0, 0)
|
||||
* ├─── branch ───── (1, 1)
|
||||
* ├──┬ branch ───── (2, 1) (start)
|
||||
* │ ├──┬ leaf ──── (3, 2)
|
||||
* │ │ └── leaf ── (4, 3)
|
||||
* │ └─── leaf ──── (5, 2) (end)
|
||||
* ├─── branch ───── (6, 1)
|
||||
* └─── branch ───── (7, 1)
|
||||
*
|
||||
* const { start: 2, end: 5, length: 3 } = getBranch(items, 2, 1)
|
||||
*/
|
||||
export function getBranch(
|
||||
thread: ApiThreadItem[],
|
||||
branchStartIndex: number,
|
||||
branchStartDepth: number,
|
||||
) {
|
||||
let end = branchStartIndex
|
||||
|
||||
for (let ci = branchStartIndex + 1; ci < thread.length; ci++) {
|
||||
const next = thread[ci]
|
||||
if (next.depth > branchStartDepth) {
|
||||
end = ci
|
||||
} else {
|
||||
end = ci - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start: branchStartIndex,
|
||||
end,
|
||||
length: end - branchStartIndex,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user