gubes mirror. how does this work
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

history moment

leah 36310c14 5f1031ff

+222 -148
+13 -41
core/channel.ts
··· 17 17 18 18 /** iterate over the channel's history. 19 19 * todo: improve documentation 20 - * @param [start_at="latest"] the timestamp to start fetching messages from. if the range 21 - * is `{ before: x }`, messages will be fetched in reverse order 22 - * (going forwards through time). 20 + * @param [start_at="latest"] the timestamp to start fetching messages from. 23 21 * @param [chunk_size=50] 24 22 */ 25 23 async *history(start_at: FetchHistoryParams[1] = "latest", chunk_size = 50) { 26 24 let next_range = start_at; 27 25 28 26 while (true) { 29 - // fetch a page of history 30 - let page = await this.fetch_history_page(next_range, chunk_size); 31 - 32 - if (!page) return; 33 - 34 - // are we going backwards or forwards through history? 35 - let going_backwards 36 - = (typeof next_range != 'string' && 'after' in next_range) 37 - || next_range == "latest"; 38 - 39 - // console.log("next_range", next_range); 40 - // are we looking at a limited range of history? 41 - let is_limited = typeof next_range != 'string' && 'during' in next_range; 42 - 43 - console.log(page, page.at(-1)?.timestamp, page[0]?.timestamp) 44 - 45 - if (going_backwards) { 46 - // get the oldest message of the page 47 - let oldest = page.at(-1)?.timestamp; 48 - // if there's no oldest message, we must be out of messages to show. 49 - if (!oldest) break; 50 - 51 - // set the range for the next iteration 52 - next_range = { after: oldest }; 27 + const messages = await this.fetch_history_page(next_range, chunk_size); 28 + // console.log(messages.length, messages[0]) 29 + if (messages.length == 0) { 30 + console.log("exhausted history"); 31 + return; 32 + }; 53 33 54 - page.reverse(); 55 - } else { 56 - // as above, but inverted 57 - let newest = page.at(0)?.timestamp; 58 - if (!newest) break; 59 - 60 - next_range = { before: newest }; 34 + for (const message of messages) { 35 + yield message; 61 36 } 62 37 63 - // yield messages from the page until we're done 64 - for (const msg of page) { 65 - yield msg; 38 + next_range = { 39 + before: messages[0].timestamp! 66 40 } 67 - 68 - // TODO: `during` logic 69 - if (is_limited) throw "todo"; 70 41 } 71 42 } 72 43 73 44 async fetch_history_page(range: FetchHistoryParams[1], chunk_size = 50) { 74 - return this.conn.fetch_history?.( 45 + if (!this.conn.fetch_history) throw new Error("No history fetcher registered") 46 + return this.conn.fetch_history( 75 47 this.name, 76 48 range, 77 49 chunk_size
+5 -2
core/connection.ts
··· 66 66 handler?: MessageHandler, 67 67 history_fetcher?: (conn: Connection, ...params: FetchHistoryParams) => Promise<IrcMessage[]>, 68 68 on_connect?: (conn: Connection) => void, 69 + debug?: boolean, 69 70 } 70 71 71 72 export enum ConnectionState { ··· 117 118 if (opt.handler) this.handler = opt.handler; 118 119 if (opt.history_fetcher) this.fetch_history = (...params) => opt.history_fetcher!(this, ...params); 119 120 if (opt.on_connect) this.on_connect = opt.on_connect; 121 + if (opt.debug) this.debug = opt.debug; 120 122 } 121 123 122 124 this.nickname = config.nickname; ··· 132 134 hostname?: string; 133 135 motd: string | null = null; 134 136 system_msgs = <string[]>[] 137 + debug = true; 135 138 136 139 /** 137 140 * Map of capabilities that have been negotiated and are able to be used. ··· 156 159 on_connect?: (conn: Connection) => void; 157 160 158 161 send(message: string) { 159 - console.debug(`${this.id} ← SENT: ${message}`); 162 + if (this.debug) console.debug(`${this.id} ← SENT: ${message}`); 160 163 this.send_raw(message); 161 164 } 162 165 ··· 194 197 195 198 parsed.timestamp = new Date(server_time ?? Date.now()); 196 199 197 - console.debug(`${this.id} → RECEIVED: ${message}`, parsed); 200 + if (this.debug) console.debug(`${this.id} → RECEIVED: ${message}`, parsed); 198 201 199 202 // resolve pending tasks 200 203 this.queue.resolve_tasks(parsed, { batch });
+10 -8
core/history.ts
··· 1 - import { Connection } from "."; 1 + import { Connection } from "./index"; 2 2 import { IrcMessage } from "./parser"; 3 3 4 4 export type FetchHistoryParams ··· 11 11 limit: number 12 12 ] 13 13 14 - async function chathistory(conn: Connection, ...[target, range, limit]: FetchHistoryParams): Promise<IrcMessage[]> { 14 + /** 15 + * History fetchers fetch history from locations. 16 + * Messages returned from history fetchers must be sorted in ascending order 17 + */ 18 + export type HistoryFetcher = (conn: Connection, params: FetchHistoryParams) => Promise<IrcMessage[]>; 19 + 20 + export const chathistory = async (conn: Connection, ...[target, range, limit]: FetchHistoryParams): Promise<IrcMessage[]> => { 15 21 if (!conn.capabilities.has('draft/chathistory')) { 16 - console.log("no cathistory pensive emoji") 22 + console.log("no chathistory pensive emoji") 17 23 return []; 18 24 } 19 25 ··· 35 41 36 42 const msgs = await conn.collect_batch("chathistory", { mask: true, params: [target] }); 37 43 console.log(msgs); 38 - return msgs.toReversed(); 44 + return msgs; 39 45 } 40 - 41 - export default class History { 42 - static chathistory = chathistory; 43 - }
+1
core/isupport.ts
··· 31 31 TARGMAX?: string, 32 32 TOPICLEN?: string, 33 33 USERLEN?: string, 34 + CHATHISTORY?: string, 34 35 } 35 36 36 37 /**
+1 -1
core/list.ts
··· 1 - import { Connection } from "."; 1 + import { Connection } from "./index"; 2 2 import { Matcher } from "./queue"; 3 3 import { Numeric } from "./support"; 4 4
+1 -1
core/motd.ts
··· 1 - import { Connection } from "."; 1 + import { Connection } from "./index"; 2 2 import { IrcMessage } from "./parser"; 3 3 import { Matcher } from "./queue"; 4 4 import { Numeric } from "./support";
+1
core/parser.ts
··· 30 30 command: Numeric | string; 31 31 params?: string[]; 32 32 timestamp?; 33 + retriver?: string; 33 34 34 35 static parse(message: string) { 35 36 let state = message;
+8 -3
core/queue.ts
··· 1 - import { Connection } from "."; 1 + import { Connection } from "./index"; 2 2 import { IrcMessage, MessageSource } from "./parser"; 3 3 import { Numeric } from "./support"; 4 4 ··· 158 158 return collector.promise; 159 159 } 160 160 161 - collect_batch(kind: string, opt?: { mask?: boolean, params?: string[], }) { 161 + collect_batch(kind: string, opt?: { 162 + /** ask handlers not to touch any messages in this batch. 163 + * this is helpful for implementing, say, `draft/event-playback` support */ 164 + mask?: boolean, 165 + params?: string[], 166 + }) { 162 167 if (!this.conn.supports.batches) { 163 168 throw new Error("Connection does not support batches."); 164 169 } ··· 301 306 if (this.params && !this.params_eq(params ?? [])) { 302 307 return false; 303 308 } 304 - 309 + 305 310 this.ref = first.slice(1); 306 311 this.start_callback?.(this.ref); 307 312 return false;
+1 -1
core/sasl.ts
··· 1 - import { Connection } from "."; 1 + import { Connection } from "./index"; 2 2 import { IrcMessage } from "./parser"; 3 3 import { Matchable, Matcher } from "./queue"; 4 4 import { Numeric } from "./support";
+1 -1
core/ws/connection.ts
··· 124 124 125 125 let reconnect = this.$state.value == ConnectionState.Connected; 126 126 127 - this.$error.value = [ConnectionErrorCode.SocketError]; 127 + this.$error.value = this.$error.value ?? [ConnectionErrorCode.SocketError]; 128 128 this.disconnect(ConnectionState.Failed); 129 129 130 130 if (reconnect) {
+1 -1
neo/package.json
··· 5 5 "type": "module", 6 6 "scripts": { 7 7 "dev": "vite", 8 - "build": "tsc && vite build", 8 + "build": "vite build", 9 9 "preview": "vite preview" 10 10 }, 11 11 "dependencies": {
+5 -1
neo/src/buffer/list-elements.tsx
··· 28 28 --hover: var(--colour-${colour}-50); 29 29 --selection: var(--colour-${colour}-700); 30 30 `; 31 - const date_string = msg.timestamp?.toLocaleString(undefined, { 31 + let date_string = msg.timestamp?.toLocaleString(undefined, { 32 32 dateStyle: "full", 33 33 timeStyle: "medium", 34 34 }); 35 + 36 + if (process.env.NODE_ENV == "development") { 37 + date_string += ` (debug) retrieved by ${msg.retriver}`; 38 + } 35 39 36 40 if (msg.is_action) { 37 41 return <li style={style}>
+7
neo/src/buffer/squisher.tsx
··· 18 18 19 19 const acc = []; 20 20 for (const current of list) { 21 + if (current.command != "PRIVMSG" 22 + && current.command != 'NOTICE' 23 + || current.params?.[0] != buffer.name 24 + ) { 25 + continue; 26 + } 27 + 21 28 if (diff_dates(current, previous)) { 22 29 acc.push(<DateSeperator timestamp={current.timestamp!} />); 23 30 }
+2 -1
neo/src/buffer/view.tsx
··· 17 17 async function load_msgs(buffer: ChatBuffer, limit = 100): Promise<IrcMessage[]> { 18 18 let count = 0; 19 19 const acc = []; 20 - for await (const msg of buffer) { 20 + const batch_size = Number(buffer.conn.isupport?.CHATHISTORY) || 50; 21 + for await (const msg of buffer.history("latest", batch_size)) { 21 22 acc.push(IrcMessage.hydrate(msg)); 22 23 if (count++ >= limit) break; 23 24 }
+1
neo/src/chat/conns.ts
··· 7 7 import Config from "./config"; 8 8 import tubes_handler from "./handler"; 9 9 import tubes_history from "./history"; 10 + import { chathistory } from "tubes_core/history"; 10 11 11 12 const local_connections: Signal<Connection[]> = signal([]); 12 13 export const connections: Signal<Connection[]> = computed(() => [
+37 -24
neo/src/chat/history.ts
··· 1 1 import { Connection } from "tubes_core"; 2 - import { FetchHistoryParams } from "tubes_core/history"; 2 + import { chathistory, FetchHistoryParams } from "tubes_core/history"; 3 3 import { IrcMessage } from "tubes_core/parser"; 4 - import History from "tubes_core/history"; 5 - import Storage, { StoredMessage } from "./storage"; 6 - 7 - const fuzz = 1; 4 + import Storage from "./storage"; 8 5 9 6 export default async function tubes_history( 10 7 conn: Connection, 11 8 ...[target, range, limit]: FetchHistoryParams 12 9 ): Promise<IrcMessage[]> { 13 - let res: StoredMessage[] = []; 10 + let res: IrcMessage[] = []; 14 11 15 12 if (typeof range == "object" && "before" in range) { 16 - res = await Storage.before(range.before); 13 + res = await Storage.before(range.before, target, conn, limit); 17 14 } 18 15 19 16 if (typeof range == "object" && "after" in range) { 20 - res = await Storage.before(range.after); 17 + res = await Storage.before(range.after, target, conn, limit); 21 18 } 22 19 23 20 if (range == "latest") { 24 - res = await Storage.before(new Date(Date.now())); 21 + console.log("fetching latest"); 22 + res = await Storage.before(new Date(Date.now()), target, conn, limit); 25 23 } 26 24 25 + console.log(res.length, limit); 26 + if (conn.debug) { 27 + res = res.map(x => { 28 + x.retriver = "storage"; 29 + return x; 30 + }); 31 + } 27 32 28 - // if (res.length == 0) { 29 - // await History.chathistory(conn, target, range, limit); 30 - // } 31 - 32 - // if (msgs.length < limit) { 33 - // const last = msgs[msgs.length - 1]; 34 - // const range = { before: last.timestamp ?? new Date(Date.now()) }; 35 - // const fetched = await History.chathistory(conn, target, range, limit); 36 - 37 - // for (const msg of fetched) { 38 - // store_message(conn, msg); 39 - // } 40 - 41 - // return fetched.concat(msgs) 42 - // } 33 + if (res.length < limit) { 34 + let more = await chathistory( 35 + conn, 36 + target, 37 + { before: res[0]?.timestamp ?? new Date(Date.now()) }, 38 + limit - res.length 39 + ); 40 + if (conn.debug) { 41 + more = more.map(x => { 42 + x.retriver = "chathistory"; 43 + return x; 44 + }); 45 + } 46 + res = more.concat(res); 47 + // if you start seeing message duplication, this is the place to look 48 + // for (const msg of res) { 49 + // try { 50 + // await Storage.store_message(msg, conn); 51 + // } catch (e) { 52 + // console.error("failed to store chathistory message:", e); 53 + // } 54 + // } 55 + } 43 56 44 57 return res; 45 58 }
+39 -9
neo/src/chat/storage.ts
··· 3 3 import { IrcMessage } from 'tubes_core/parser'; 4 4 5 5 const db_name = "neotubes"; 6 - const version = 1; 6 + const version = 2; 7 7 8 8 export class StoredMessage extends IrcMessage { 9 9 id: string; 10 10 target: string; 11 11 connection_id: string; 12 12 adapter_id: string; 13 + declare timestamp: Date; 13 14 14 15 constructor(msg: IrcMessage, target: string, connection: Connection) { 15 16 super(msg); ··· 25 26 26 27 static async init() { 27 28 const db = await idb.openDB(db_name, version, { 28 - upgrade(db, old) { 29 + upgrade(db, old, _, transaction) { 29 30 switch (old) { 30 31 case 0: { 31 32 const msgs = db.createObjectStore("messages", { 32 33 keyPath: ["id", "target", "connection_id", "adapter_id"], 33 34 }); 34 35 msgs.createIndex("timestamps", "timestamp"); 36 + break; 37 + } 38 + case 1: { 39 + const msgs = transaction.objectStore("messages"); 40 + msgs.createIndex( 41 + "everything_timestamp", 42 + ["timestamp", "target", "connection_id", "adapter_id"], 43 + { unique: true }, 44 + ); 35 45 } 36 46 } 37 47 } ··· 54 64 return key; 55 65 } 56 66 57 - async before(id: Date | string) { 67 + async before(id: Date | string, target: string, conn: Connection, limit: number = 50) { 58 68 const trans = this.db.transaction("messages", "readwrite"); 59 - 60 69 const store = trans.objectStore("messages"); 61 - 62 - console.log(id instanceof Date); 63 70 64 71 if (id instanceof Date) { 65 - const timestamps = store.index("timestamps"); 66 - const msgs = await timestamps.getAll(IDBKeyRange.upperBound(id)) as StoredMessage[]; 67 - return msgs; 72 + const timestamps = store.index("everything_timestamp"); 73 + const range = IDBKeyRange.bound( 74 + [0, target, conn.id, conn.adapter_id ?? ""], 75 + [id, target, conn.id, conn.adapter_id ?? ""], 76 + false, true 77 + ); 78 + 79 + const cursor = await timestamps.openCursor(range, "prev"); 80 + const msgs = []; 81 + let count = 0; 82 + // this could get slow over time. 83 + while (cursor && count < limit) { 84 + const value = cursor.value; 85 + if (!value) break; 86 + if ( 87 + value.target == target 88 + && value.connection_id == conn.id 89 + && value.adapter_id == (conn.adapter_id ?? "") 90 + ) { 91 + msgs.push(cursor.value); 92 + count++; 93 + }; 94 + await cursor.continue(); 95 + } 96 + 97 + return msgs.toReversed(); 68 98 } else { 69 99 return []; 70 100 }
+2 -2
neo/src/settings/index.tsx
··· 53 53 <h2>bouncers</h2> 54 54 <div class="panel"> 55 55 <p class="intro body-small"> 56 - a bouncer is an external service that stays connected to your 56 + bouncers are external services that stay connected to your 57 57 networks while tubes is closed, so you can see what people were 58 58 saying in the chat rooms while you were away, among other things. 59 59 <br /> 60 60 please note that sometimes the network you're connecting will 61 - do all this for you. 61 + do all this for you. we live in exciting times. 62 62 </p> 63 63 64 64 <div>
+3
neo/vite.config.ts
··· 11 11 "@src": path.resolve(__dirname, "./src/"), 12 12 "@css": path.resolve(__dirname, "./src/css"), 13 13 } 14 + }, 15 + build: { 16 + target: ["ESNext"] 14 17 } 15 18 })
+83 -52
package-lock.json
··· 44 44 "async-mutex": "^0.5.0", 45 45 "dayjs": "^1.11.13", 46 46 "dexie": "^4.0.8", 47 - "motion": "^11.12.0", 47 + "motion": "^10.18.0", 48 48 "preact": "^10.23.1", 49 49 "wouter-preact": "^3.3.1" 50 50 }, ··· 57 57 "typescript": "^5.5.4", 58 58 "unplugin-icons": "^0.20.2", 59 59 "vite": "^6.0.1" 60 + } 61 + }, 62 + "neo/node_modules/motion": { 63 + "version": "10.18.0", 64 + "resolved": "https://registry.npmjs.org/motion/-/motion-10.18.0.tgz", 65 + "integrity": "sha512-MVAZZmwM/cp77BrNe1TxTMldxRPjwBNHheU5aPToqT4rJdZxLiADk58H+a0al5jKLxkB0OdgNq6DiVn11cjvIQ==", 66 + "license": "MIT", 67 + "dependencies": { 68 + "@motionone/animation": "^10.18.0", 69 + "@motionone/dom": "^10.18.0", 70 + "@motionone/types": "^10.17.1", 71 + "@motionone/utils": "^10.18.0" 60 72 } 61 73 }, 62 74 "node_modules/@ampproject/remapping": { ··· 914 926 "dependencies": { 915 927 "@jridgewell/resolve-uri": "^3.1.0", 916 928 "@jridgewell/sourcemap-codec": "^1.4.14" 929 + } 930 + }, 931 + "node_modules/@motionone/animation": { 932 + "version": "10.18.0", 933 + "resolved": "https://registry.npmjs.org/@motionone/animation/-/animation-10.18.0.tgz", 934 + "integrity": "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw==", 935 + "license": "MIT", 936 + "dependencies": { 937 + "@motionone/easing": "^10.18.0", 938 + "@motionone/types": "^10.17.1", 939 + "@motionone/utils": "^10.18.0", 940 + "tslib": "^2.3.1" 941 + } 942 + }, 943 + "node_modules/@motionone/dom": { 944 + "version": "10.18.0", 945 + "resolved": "https://registry.npmjs.org/@motionone/dom/-/dom-10.18.0.tgz", 946 + "integrity": "sha512-bKLP7E0eyO4B2UaHBBN55tnppwRnaE3KFfh3Ps9HhnAkar3Cb69kUCJY9as8LrccVYKgHA+JY5dOQqJLOPhF5A==", 947 + "license": "MIT", 948 + "dependencies": { 949 + "@motionone/animation": "^10.18.0", 950 + "@motionone/generators": "^10.18.0", 951 + "@motionone/types": "^10.17.1", 952 + "@motionone/utils": "^10.18.0", 953 + "hey-listen": "^1.0.8", 954 + "tslib": "^2.3.1" 955 + } 956 + }, 957 + "node_modules/@motionone/easing": { 958 + "version": "10.18.0", 959 + "resolved": "https://registry.npmjs.org/@motionone/easing/-/easing-10.18.0.tgz", 960 + "integrity": "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg==", 961 + "license": "MIT", 962 + "dependencies": { 963 + "@motionone/utils": "^10.18.0", 964 + "tslib": "^2.3.1" 965 + } 966 + }, 967 + "node_modules/@motionone/generators": { 968 + "version": "10.18.0", 969 + "resolved": "https://registry.npmjs.org/@motionone/generators/-/generators-10.18.0.tgz", 970 + "integrity": "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg==", 971 + "license": "MIT", 972 + "dependencies": { 973 + "@motionone/types": "^10.17.1", 974 + "@motionone/utils": "^10.18.0", 975 + "tslib": "^2.3.1" 976 + } 977 + }, 978 + "node_modules/@motionone/types": { 979 + "version": "10.17.1", 980 + "resolved": "https://registry.npmjs.org/@motionone/types/-/types-10.17.1.tgz", 981 + "integrity": "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A==", 982 + "license": "MIT" 983 + }, 984 + "node_modules/@motionone/utils": { 985 + "version": "10.18.0", 986 + "resolved": "https://registry.npmjs.org/@motionone/utils/-/utils-10.18.0.tgz", 987 + "integrity": "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw==", 988 + "license": "MIT", 989 + "dependencies": { 990 + "@motionone/types": "^10.17.1", 991 + "hey-listen": "^1.0.8", 992 + "tslib": "^2.3.1" 917 993 } 918 994 }, 919 995 "node_modules/@preact/preset-vite": { ··· 2125 2201 "node": ">=12.0.0" 2126 2202 } 2127 2203 }, 2128 - "node_modules/framer-motion": { 2129 - "version": "11.12.0", 2130 - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.12.0.tgz", 2131 - "integrity": "sha512-gZaZeqFM6pX9kMVti60hYAa75jGpSsGYWAHbBfIkuHN7DkVHVkxSxeNYnrGmHuM0zPkWTzQx10ZT+fDjn7N4SA==", 2132 - "license": "MIT", 2133 - "dependencies": { 2134 - "tslib": "^2.4.0" 2135 - }, 2136 - "peerDependencies": { 2137 - "@emotion/is-prop-valid": "*", 2138 - "react": "^18.0.0", 2139 - "react-dom": "^18.0.0" 2140 - }, 2141 - "peerDependenciesMeta": { 2142 - "@emotion/is-prop-valid": { 2143 - "optional": true 2144 - }, 2145 - "react": { 2146 - "optional": true 2147 - }, 2148 - "react-dom": { 2149 - "optional": true 2150 - } 2151 - } 2152 - }, 2153 2204 "node_modules/fsevents": { 2154 2205 "version": "2.3.3", 2155 2206 "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", ··· 2209 2260 "bin": { 2210 2261 "he": "bin/he" 2211 2262 } 2263 + }, 2264 + "node_modules/hey-listen": { 2265 + "version": "1.0.8", 2266 + "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", 2267 + "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==", 2268 + "license": "MIT" 2212 2269 }, 2213 2270 "node_modules/idb": { 2214 2271 "version": "8.0.0", ··· 2390 2447 "pathe": "^1.1.2", 2391 2448 "pkg-types": "^1.2.1", 2392 2449 "ufo": "^1.5.4" 2393 - } 2394 - }, 2395 - "node_modules/motion": { 2396 - "version": "11.12.0", 2397 - "resolved": "https://registry.npmjs.org/motion/-/motion-11.12.0.tgz", 2398 - "integrity": "sha512-BFH9vwCs4dI9t1W1/1HonahOCnTxcKfzBR8D310wHFdx7oIwlP/51OqLNGO74lxOdCpTLf5BLe233k6yRqJo9Q==", 2399 - "license": "MIT", 2400 - "dependencies": { 2401 - "framer-motion": "^11.12.0", 2402 - "tslib": "^2.4.0" 2403 - }, 2404 - "peerDependencies": { 2405 - "@emotion/is-prop-valid": "*", 2406 - "react": "^18.0.0", 2407 - "react-dom": "^18.0.0" 2408 - }, 2409 - "peerDependenciesMeta": { 2410 - "@emotion/is-prop-valid": { 2411 - "optional": true 2412 - }, 2413 - "react": { 2414 - "optional": true 2415 - }, 2416 - "react-dom": { 2417 - "optional": true 2418 - } 2419 2450 } 2420 2451 }, 2421 2452 "node_modules/ms": {