JavaScript-optional public web frontend for Bluesky anartia.kelinci.net
sveltekit atcute bluesky typescript svelte
at trunk 187 lines 4.0 kB view raw
1import type { AppBskyFeedDefs } from '@atcute/bluesky'; 2import { mapDefined } from '@mary/array-fns'; 3 4export type TimelineItem = AppBskyFeedDefs.FeedViewPost; 5 6// #region TimelineSlice 7export interface TimelineSlice { 8 items: TimelineItem[]; 9} 10 11// #region UiTimelineItem 12const enum TimelineFlags { 13 HAS_PREV = 1 << 0, 14 HAS_NEXT = 1 << 1, 15 IS_REPOSTED = 1 << 2, 16 IS_PINNED = 1 << 3, 17} 18 19export interface UiTimelineItem extends TimelineItem { 20 id: string; 21 prev: boolean; 22 next: boolean; 23} 24 25// #region Filters 26export type SliceFilter = (slice: TimelineSlice) => boolean | TimelineSlice[]; 27export type PostFilter = (item: TimelineItem) => boolean; 28 29const isNextInThread = (slice: TimelineSlice, item: TimelineItem) => { 30 const items = slice.items; 31 const last = items[items.length - 1]; 32 33 const parent = item.reply?.parent; 34 35 return parent?.$type === 'app.bsky.feed.defs#postView' && last.post.cid == parent.cid; 36}; 37 38const isFirstInThread = (slice: TimelineSlice, item: TimelineItem) => { 39 const items = slice.items; 40 const first = items[0]; 41 42 const parent = first.reply?.parent; 43 44 return parent?.$type === 'app.bsky.feed.defs#postView' && parent.cid === item.post.cid; 45}; 46 47export const buildTimelineSlices = ( 48 arr: TimelineItem[], 49 filterSlice?: SliceFilter, 50 filterPost?: PostFilter, 51): TimelineSlice[] => { 52 let slices: TimelineSlice[] = []; 53 let jlen = 0; 54 55 // arrange the posts into connected slices 56 loop: for (let i = arr.length - 1; i >= 0; i--) { 57 const item = arr[i]; 58 59 if (filterPost && !filterPost(item)) { 60 continue; 61 } 62 63 for (let j = 0; j < jlen; j++) { 64 const slice = slices[j]; 65 66 // skip, we already have too much. 67 if (slice.items.length >= 7) { 68 continue; 69 } 70 71 if (isFirstInThread(slice, item)) { 72 slice.items.unshift(item); 73 74 if (j !== 0) { 75 slices.splice(j, 1); 76 slices.unshift(slice); 77 } 78 79 continue loop; 80 } else if (isNextInThread(slice, item)) { 81 slice.items.push(item); 82 83 if (j !== 0) { 84 slices.splice(j, 1); 85 slices.unshift(slice); 86 } 87 88 continue loop; 89 } 90 } 91 92 slices.unshift({ items: [item] }); 93 jlen++; 94 } 95 96 if (filterSlice && jlen > 0) { 97 const unfiltered = slices; 98 slices = []; 99 100 for (let j = 0; j < jlen; j++) { 101 const slice = unfiltered[j]; 102 const result = filterSlice(slice); 103 104 if (result) { 105 if (Array.isArray(result)) { 106 for (let k = 0, klen = result.length; k < klen; k++) { 107 slices.push(result[k]); 108 } 109 } else { 110 slices.push(slice); 111 } 112 } 113 } 114 } 115 116 return slices; 117}; 118 119export const flattenTimelineSlices = (slices: TimelineSlice[]): UiTimelineItem[] => { 120 return slices.flatMap((slice) => { 121 const items = slice.items; 122 const len = items.length; 123 124 return items.map((item, idx): UiTimelineItem => { 125 const post = item.post; 126 const reason = item.reason; 127 128 let flags = 0; 129 130 if (idx !== 0) { 131 flags |= TimelineFlags.HAS_PREV; 132 } 133 if (idx !== len - 1) { 134 flags |= TimelineFlags.HAS_NEXT; 135 } 136 137 switch (reason?.$type) { 138 case 'app.bsky.feed.defs#reasonRepost': { 139 flags |= TimelineFlags.IS_REPOSTED; 140 break; 141 } 142 case 'app.bsky.feed.defs#reasonPin': { 143 flags |= TimelineFlags.IS_PINNED; 144 break; 145 } 146 } 147 148 return { 149 ...item, 150 id: `${post.author.did}-${post.cid}-${flags}`, 151 prev: !!(flags & TimelineFlags.HAS_PREV), 152 next: !!(flags & TimelineFlags.HAS_NEXT), 153 }; 154 }); 155 }); 156}; 157 158export const mapTimelineItems = (arr: TimelineItem[], filterPost?: PostFilter): UiTimelineItem[] => { 159 return mapDefined(arr, (item) => { 160 if (filterPost && !filterPost(item)) { 161 return; 162 } 163 164 const post = item.post; 165 const reason = item.reason; 166 167 let flags = 0; 168 169 switch (reason?.$type) { 170 case 'app.bsky.feed.defs#reasonRepost': { 171 flags |= TimelineFlags.IS_REPOSTED; 172 break; 173 } 174 case 'app.bsky.feed.defs#reasonPin': { 175 flags |= TimelineFlags.IS_PINNED; 176 break; 177 } 178 } 179 180 return { 181 ...item, 182 id: `${post.author.did}-${post.cid}-${flags}`, 183 prev: false, 184 next: false, 185 }; 186 }); 187};