From 48b3b7e7f57997ca0b96cfbc874711988bbf7dc3 Mon Sep 17 00:00:00 2001 From: Walter Scott Date: Wed, 24 Jul 2024 08:15:04 -0500 Subject: [PATCH] move thread_store to dixie --- .vscode/unthread.me.code-workspace | 150 +++++++++++----------- bun.lockb | Bin 222412 -> 223684 bytes package.json | 5 +- src/client/cache_store.ts | 119 +---------------- src/client/hooks/index.ts | 14 +- src/client/hooks/useAccessTokenUpdater.ts | 9 +- src/client/hooks/useInsightsByDate.ts | 29 ++--- src/client/hooks/useRefreshers.ts | 11 +- src/client/hooks/useThread.ts | 14 ++ src/client/hooks/useThreadInfo.ts | 18 --- src/client/hooks/useThreadList.ts | 14 ++ src/client/hooks/useThreadStore.ts | 5 + src/client/hooks/useThreadsListByDate.ts | 5 +- src/client/hooks/useWords.ts | 4 +- src/client/index.ts | 2 + src/client/thread_store.ts | 118 +++++++++++++++++ src/components/Loader.tsx | 4 +- src/components/UserProfile2.tsx | 4 +- src/components/UserThreadsView.tsx | 14 +- src/components/WordSegmentLineChart.tsx | 3 +- src/lib/ml.ts | 11 +- tsconfig.app.json | 17 ++- tsconfig.node.json | 9 +- vite.config.ts | 4 +- 24 files changed, 312 insertions(+), 271 deletions(-) create mode 100644 src/client/hooks/useThread.ts delete mode 100644 src/client/hooks/useThreadInfo.ts create mode 100644 src/client/hooks/useThreadList.ts create mode 100644 src/client/hooks/useThreadStore.ts create mode 100644 src/client/thread_store.ts diff --git a/.vscode/unthread.me.code-workspace b/.vscode/unthread.me.code-workspace index 83c693e..a9cffb4 100644 --- a/.vscode/unthread.me.code-workspace +++ b/.vscode/unthread.me.code-workspace @@ -1,77 +1,77 @@ { - "folders": [ - { - "name": "root", - "path": "../", - }, - { - "name": "terraform", - "path": "../terraform", - } - ], - "settings": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSaveMode": "file", - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "always", - // "source.organizeImports": "always", - "source.fixAll": "always", - }, - "files.associations": { - "tsconfig.*json": "jsonc", - "*.css": "tailwindcss", - }, - "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true, - "editor.detectIndentation": false, - "prettier.requireConfig": true, - "typescript.inlayHints.parameterNames.enabled": "all", - "typescript.inlayHints.variableTypes.enabled": false, - "typescript.inlayHints.propertyDeclarationTypes.enabled": true, - "typescript.tsserver.experimental.enableProjectDiagnostics": true, - "eslint.useESLintClass": true, - "explorer.sortOrder": "type", - "explorer.sortOrderLexicographicOptions": "upper", - "[dotenv]": { - "editor.defaultFormatter": "foxundermoon.shell-format", - }, - "gitlens.codeLens.enabled": false, - "workbench.tree.indent": 16, - "yaml.format.enable": true, - "terraform.experimentalFeatures.validateOnSave": true, - "github.copilot.enable": { - "*": true, - "yaml": false, - "plaintext": false, - "markdown": false - }, - "[terraform]": { - "editor.defaultFormatter": "hashicorp.terraform", - "editor.formatOnSave": false, - "editor.codeActionsOnSave": { - "source.formatAll.terraform": "always" - }, - }, - "[terraform-vars]": { - "editor.defaultFormatter": "hashicorp.terraform", - "editor.formatOnSave": false, - "editor.codeActionsOnSave": { - "source.formatAll.terraform": "always" - }, - }, - "material-icon-theme.folders.associations": { - "dynamo": "database", - "mw": "middleware", - }, - "emeraldwalk.runonsave": { - "shell": "bash", - "commands": [ - { - "isAsync": true, - "match": "\\.tf$|\\.tfvars$|\\.hcl$|\\.hclvars$", - "cmd": "/opt/homebrew/bin/terraform fmt \"${file}\" && sed -e'':a'' -e's/^\\(\\t*\\) /\\1\\t/;ta' \"${file}\" > \"${file}\"-notab && mv \"${file}\"-notab \"${file}\"" - }, - ] - }, - }, + "folders": [ + { + "name": "root", + "path": "../", + }, + { + "name": "terraform", + "path": "../terraform", + }, + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSaveMode": "file", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "always", + // "source.organizeImports": "always", + "source.fixAll": "always", + }, + "files.associations": { + "tsconfig.*json": "jsonc", + "*.css": "tailwindcss", + }, + "typescript.inlayHints.parameterNames.suppressWhenArgumentMatchesName": true, + "editor.detectIndentation": false, + "prettier.requireConfig": true, + "typescript.inlayHints.parameterNames.enabled": "all", + "typescript.inlayHints.variableTypes.enabled": false, + "typescript.inlayHints.propertyDeclarationTypes.enabled": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "eslint.useESLintClass": true, + "explorer.sortOrder": "type", + "explorer.sortOrderLexicographicOptions": "upper", + "[dotenv]": { + "editor.defaultFormatter": "foxundermoon.shell-format", + }, + "gitlens.codeLens.enabled": false, + "workbench.tree.indent": 16, + "yaml.format.enable": true, + "terraform.experimentalFeatures.validateOnSave": true, + "github.copilot.enable": { + "*": true, + "yaml": false, + "plaintext": false, + "markdown": false, + }, + "[terraform]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.formatAll.terraform": "always", + }, + }, + "[terraform-vars]": { + "editor.defaultFormatter": "hashicorp.terraform", + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.formatAll.terraform": "always", + }, + }, + "material-icon-theme.folders.associations": { + "dynamo": "database", + "mw": "middleware", + }, + "emeraldwalk.runonsave": { + "shell": "bash", + "commands": [ + { + "isAsync": true, + "match": "\\.tf$|\\.tfvars$|\\.hcl$|\\.hclvars$", + "cmd": "/opt/homebrew/bin/terraform fmt \"${file}\" && sed -e'':a'' -e's/^\\(\\t*\\) /\\1\\t/;ta' \"${file}\" > \"${file}\"-notab && mv \"${file}\"-notab \"${file}\"", + }, + ], + }, + }, } \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index b5e21f2366673b4733001457c80d6c9e6a76b1b4..f3d05315f1b4c3abe8a9d3c31fcc451573dd1b57 100755 GIT binary patch delta 43710 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 delta 42717 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 user_profile_refreshed_at: number; } @@ -51,66 +22,8 @@ export const cache_store = create( user_profile: null, user_insights: null, user_follower_demographics: null, - user_threads: {}, } as CacheStoreState, - (set, get) => { - const loadThreadsData = async (ky: KyInstance, token: AccessTokenResponse, params?: GetUserThreadsParams) => { - const promises: Promise[] = []; - // let count = 0; - const fetchAllPages = async (cursor?: string): Promise => { - const [response, paging] = await fetch_user_threads_page(ky, token, params, cursor).then((data) => { - set((state) => { - for (const thread of data.data) { - const id = makeThreadID(thread.id); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (!state.user_threads[id]) { - state.user_threads[id] = { - id, - media: thread, - replies: null, - insights: null, - }; - } else { - state.user_threads[id].id = id; - state.user_threads[id].media = thread; - } - } - return state; - }); - - return [data.data.map((thread) => makeThreadID(thread.id)), data.paging] as const; - }); - - for (const thread of response) { - promises.push(loadThreadRepliesData(ky, token, thread)); - promises.push(loadThreadInsightsData(ky, token, thread)); - } - - if (paging?.cursors.after && !params?.limit) { - await fetchAllPages(paging.cursors.after); - } - }; - - await fetchAllPages(); - await Promise.all(promises); - }; - const loadThreadInsightsData = async (ky: KyInstance, token: AccessTokenResponse, id: ThreadID) => { - await get_media_insights(ky, token, extractThreadID(id)).then((data) => { - set((state) => { - state.user_threads[id].insights = data; - return state; - }); - }); - }; - const loadThreadRepliesData = async (ky: KyInstance, token: AccessTokenResponse, id: ThreadID) => { - await get_conversation(ky, token, extractThreadID(id)).then((data) => { - set((state) => { - state.user_threads[id].replies = data; - return state; - }); - }); - }; - + (set) => { const loadUserData = async (ky: KyInstance, token: AccessTokenResponse) => { const prof = threadsapi.get_user_profile(ky, token).then((data) => { set(() => ({ @@ -140,18 +53,6 @@ export const cache_store = create( return { loadUserData, - getThreadInsights: (id: ThreadID) => { - return get().user_threads[id].insights; - }, - - getThreadReplies: (id: ThreadID) => { - return get().user_threads[id].replies; - }, - - getThreadMedia: (id: ThreadID) => { - return get().user_threads[id].media; - }, - clearUserData: () => { set(() => { return { @@ -161,26 +62,12 @@ export const cache_store = create( }; }); }, - - loadThreadsData, - - refreshThreadsLast2Days: async (ky: KyInstance, token: AccessTokenResponse) => { - await loadThreadsData(ky, token, { since: `${Math.round((Date.now() - 1000 * 60 * 60 * 24 * 2) / 1000)}` }); - }, - - clearThreads: () => { - set(() => ({ - user_threads: {}, - user_threads_replies: {}, - user_threads_insights: {}, - })); - }, }; }, ), { name: "unthread.me/cache_store", - storage: createJSONStorage(() => localStorage), + storage: createJSONStorage(() => localStorage, {}), version: 10, }, ), diff --git a/src/client/hooks/index.ts b/src/client/hooks/index.ts index 37fef0a..40717e0 100644 --- a/src/client/hooks/index.ts +++ b/src/client/hooks/index.ts @@ -2,28 +2,34 @@ import useAccessTokenUpdater from "./useAccessTokenUpdater"; import useBackgroundUpdater from "./useBackgroundUpdater"; import useCacheStore from "./useCacheStore"; import useDimensions from "./useDimensions"; -import useFeatureFlagStrore from "./useFeatureFlagStore"; +import useFeatureFlagStore from "./useFeatureFlagStore"; +import useFeatureFlagUpdater from "./useFeatureFlagUpdater"; +import useInsightsByDate from "./useInsightsByDate"; +import useMLByDate from "./useMLByDate"; import useModalStore from "./useModalStore"; import useSessionStore from "./useSessionStore"; -import useThreadInfo from "./useThreadInfo"; import useThreadInfoListByDateRange from "./useThreadInfoListByDateRange"; import useThreadsListByDate from "./useThreadsListByDate"; +import useThreadStore from "./useThreadStore"; import useTimePeriod from "./useTimePeriod"; import useTokenStore from "./useTokenStore"; import useUserInsights from "./useUserInsights"; export default { - useFeatureFlagStrore, + useInsightsByDate, useTimePeriod, useTokenStore, useDimensions, + useThreadStore, useUserInsights, useSessionStore, useModalStore, - useThreadInfo, useCacheStore, + useFeatureFlagStore, + useMLByDate, useAccessTokenUpdater, useBackgroundUpdater, + useFeatureFlagUpdater, useThreadsListByDate, useThreadInfoListByDateRange, }; diff --git a/src/client/hooks/useAccessTokenUpdater.ts b/src/client/hooks/useAccessTokenUpdater.ts index 9dad574..e3bdd78 100644 --- a/src/client/hooks/useAccessTokenUpdater.ts +++ b/src/client/hooks/useAccessTokenUpdater.ts @@ -7,6 +7,7 @@ import { useIsLoggedIn } from "@src/client/hooks/useIsLoggedIn"; import useTokenStore from "@src/client/hooks/useTokenStore"; import threadsapi from "@src/threadsapi"; +import thread_store from "../thread_store"; import useCacheStore from "./useCacheStore"; const useAccessTokenUpdater = () => { @@ -20,8 +21,7 @@ const useAccessTokenUpdater = () => { const [isLoggedIn] = useIsLoggedIn(); const refreshUserProfile = useCacheStore((state) => state.loadUserData); - const refreshThreads = useCacheStore((state) => state.refreshThreadsLast2Days); - const refreshAllThreads = useCacheStore((state) => state.loadThreadsData); + // const clearAccessToken = client.token_store((state) => state.clearAccessToken); // update the access token if a code is present in the URL @@ -37,8 +37,7 @@ const useAccessTokenUpdater = () => { updateAccessToken(res); const kyd2 = ky.create({ prefixUrl: "https://graph.threads.net" }); void refreshUserProfile(kyd2, res); - void refreshThreads(kyd2, res); - void refreshAllThreads(kyd2, res); + void thread_store.loadThreadsData(kyd2, res); } catch (error) { console.error("Error updating access token:", error); } finally { @@ -55,7 +54,7 @@ const useAccessTokenUpdater = () => { console.error(err); }); } - }, [searchParams, setSearchParams, updateAccessToken, updateIsLoggingIn, refreshAllThreads, refreshThreads, refreshUserProfile]); + }, [searchParams, setSearchParams, updateAccessToken, updateIsLoggingIn, refreshUserProfile]); /// generate or refresh long-lived access token // if long lived access token is not present and short-lived access token is present -> generate long-lived access token diff --git a/src/client/hooks/useInsightsByDate.ts b/src/client/hooks/useInsightsByDate.ts index 0241bbd..4ed79cd 100644 --- a/src/client/hooks/useInsightsByDate.ts +++ b/src/client/hooks/useInsightsByDate.ts @@ -1,42 +1,35 @@ -import { convertToInsightsByDate, isbd, isdbAll, isdbAllNoRelative, isdbRange } from "@src/lib/ml"; +import { isbd, isdbAll, isdbAllNoRelative, isdbRange } from "@src/lib/ml"; -import { ThreadID } from "../cache_store"; import useCacheStore from "./useCacheStore"; +import useThreadList from "./useThreadList"; const useInsightsByDate = (date: Date) => { const userInsights = useCacheStore((state) => state.user_insights); - const userThreads = useCacheStore((state) => - Object.keys(state.user_threads).map((key) => convertToInsightsByDate(state.user_threads[key as ThreadID])), - ); + const userThreads = useThreadList(); return isbd(date.toISOString().slice(0, 10), userInsights, userThreads); }; +// for fun + export const useInsightsByDateRange = (startDate: Date, endDate: Date) => { const userInsights = useCacheStore((state) => state.user_insights); - const userThreads = useCacheStore((state) => - Object.keys(state.user_threads).map((key) => convertToInsightsByDate(state.user_threads[key as ThreadID])), - ); + const userThreads = useThreadList(); + return isdbRange(startDate, endDate, userInsights, userThreads); }; export const useInsightsByAll = () => { const userInsights = useCacheStore((state) => state.user_insights); - const userThreads = useCacheStore((state) => state.user_threads); + const userThreads = useThreadList(); - return isdbAll( - userInsights, - Object.keys(userThreads).map((key) => convertToInsightsByDate(userThreads[key as ThreadID])), - ); + return isdbAll(userInsights, userThreads); }; export const useDaily = () => { const userInsights = useCacheStore((state) => state.user_insights); - const userThreads = useCacheStore((state) => state.user_threads); + const userThreads = useThreadList(); - return isdbAllNoRelative( - userInsights, - Object.keys(userThreads).map((key) => convertToInsightsByDate(userThreads[key as ThreadID])), - ); + return isdbAllNoRelative(userInsights, userThreads); }; export default useInsightsByDate; diff --git a/src/client/hooks/useRefreshers.ts b/src/client/hooks/useRefreshers.ts index c323248..f48a752 100644 --- a/src/client/hooks/useRefreshers.ts +++ b/src/client/hooks/useRefreshers.ts @@ -3,13 +3,13 @@ import { useCallback, useState } from "react"; import { AccessTokenResponse } from "@src/threadsapi/types"; +import thread_store from "../thread_store"; import useCacheStore from "./useCacheStore"; import { useIsLoggedIn } from "./useIsLoggedIn"; const kyd = ky.create({ prefixUrl: "https://graph.threads.net" }); export const useLast2DaysThreadsRefresher = () => { - const refresh = useCacheStore((state) => state.refreshThreadsLast2Days); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -19,7 +19,7 @@ export const useLast2DaysThreadsRefresher = () => { async function fetchData(token: AccessTokenResponse) { setLoading(true); try { - await refresh(kyd, token); + await thread_store.refreshThreadsLast2Days(kyd, token); setError(null); } catch (error) { console.error(`problem fetching last 2 days threads:`, error); @@ -35,13 +35,12 @@ export const useLast2DaysThreadsRefresher = () => { } return; - }, [isLoggedIn, accessToken, refresh, setLoading, setError]); + }, [isLoggedIn, accessToken, setLoading, setError]); return [caller, isLoading, error] as const; }; export const useAllThreadsRefresher = () => { - const refresh = useCacheStore((state) => state.loadThreadsData); const [isLoading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -51,7 +50,7 @@ export const useAllThreadsRefresher = () => { async function fetchData(token: AccessTokenResponse) { setLoading(true); try { - await refresh(kyd, token); + await thread_store.loadThreadsData(kyd, token); setError(null); } catch (error) { console.error(`problem fetching all threads:`, error); @@ -67,7 +66,7 @@ export const useAllThreadsRefresher = () => { } return; - }, [isLoggedIn, accessToken, refresh, setLoading, setError]); + }, [isLoggedIn, accessToken, setLoading, setError]); return [caller, isLoading, error] as const; }; diff --git a/src/client/hooks/useThread.ts b/src/client/hooks/useThread.ts new file mode 100644 index 0000000..dfaac72 --- /dev/null +++ b/src/client/hooks/useThread.ts @@ -0,0 +1,14 @@ +import { useLiveQuery } from "dexie-react-hooks"; + +import { db, ThreadID } from "../thread_store"; + +const useThread = (id: ThreadID) => { + const thread = useLiveQuery(async () => { + const thread = await db.threads.get(id); + return thread; + }, [id]); + + return thread; +}; + +export default useThread; diff --git a/src/client/hooks/useThreadInfo.ts b/src/client/hooks/useThreadInfo.ts deleted file mode 100644 index 321cfae..0000000 --- a/src/client/hooks/useThreadInfo.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ThreadID } from "../cache_store"; -import useCacheStore from "./useCacheStore"; - -const useThreadInfo = (thread: ThreadID) => { - const likes = useCacheStore((state) => state.user_threads[thread].insights?.total_likes ?? 0); - - const views = useCacheStore((state) => state.user_threads[thread].insights?.total_views ?? 0); - - const replies = useCacheStore((state) => state.user_threads[thread].replies?.data ?? []); - - const quotes = useCacheStore((state) => state.user_threads[thread].insights?.total_quotes ?? 0); - - const reposts = useCacheStore((state) => state.user_threads[thread].insights?.total_reposts ?? 0); - - return [likes, views, replies, quotes, reposts] as const; -}; - -export default useThreadInfo; diff --git a/src/client/hooks/useThreadList.ts b/src/client/hooks/useThreadList.ts new file mode 100644 index 0000000..525069d --- /dev/null +++ b/src/client/hooks/useThreadList.ts @@ -0,0 +1,14 @@ +import { useLiveQuery } from "dexie-react-hooks"; + +import { db } from "../thread_store"; + +const useThreadList = () => { + const thread = useLiveQuery(async () => { + const thread = await db.threads.toArray(); + return thread; + }, []); + + return thread ?? []; +}; + +export default useThreadList; diff --git a/src/client/hooks/useThreadStore.ts b/src/client/hooks/useThreadStore.ts new file mode 100644 index 0000000..327d552 --- /dev/null +++ b/src/client/hooks/useThreadStore.ts @@ -0,0 +1,5 @@ +import client from ".."; + +const useThreadStore = client.thread_store; + +export default useThreadStore; diff --git a/src/client/hooks/useThreadsListByDate.ts b/src/client/hooks/useThreadsListByDate.ts index 42eacf2..ebe0f12 100644 --- a/src/client/hooks/useThreadsListByDate.ts +++ b/src/client/hooks/useThreadsListByDate.ts @@ -1,13 +1,12 @@ import { useMemo } from "react"; -import useCacheStore from "./useCacheStore"; +import useThreadList from "./useThreadList"; const useThreadsListSortedByDate = () => { - const threads = useCacheStore((state) => Object.values(state.user_threads)); + const threads = useThreadList(); const filteredThreads = useMemo(() => { return threads.sort((a, b) => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return new Date(b.media.timestamp).getTime() - new Date(a.media.timestamp).getTime(); }); }, [threads]); diff --git a/src/client/hooks/useWords.ts b/src/client/hooks/useWords.ts index ffc9aa0..40f5f78 100644 --- a/src/client/hooks/useWords.ts +++ b/src/client/hooks/useWords.ts @@ -3,7 +3,7 @@ import { outMethods } from "node_modules/compromise/types/misc"; import Three from "node_modules/compromise/types/view/three"; import { useMemo } from "react"; -import { CachedThreadData } from "../cache_store"; +import { CachedThreadData } from "../thread_store"; interface WordInsightStats { total_likes: number; @@ -29,7 +29,7 @@ export const extractMetics = (data: WordInsight, key: MetricKey): number => { export const useByWord = (data: CachedThreadData[]): WordInsight[] => { return useMemo(() => { const resp = data.reduce>((acc, thread) => { - if (!thread.media || !thread.insights) return acc; + if (!thread.insights) return acc; if (thread.media.text) { const likes = thread.insights.total_likes; const views = thread.insights.total_views; diff --git a/src/client/index.ts b/src/client/index.ts index 8883570..d354331 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -2,10 +2,12 @@ import cache_store from "./cache_store"; import feature_flag_store from "./feature_flag_store"; import modal_store from "./modal_store"; import session_store from "./session_store"; +import thread_store from "./thread_store"; import token_store from "./token_store"; export default { session_store, + thread_store, feature_flag_store, token_store, modal_store, diff --git a/src/client/thread_store.ts b/src/client/thread_store.ts new file mode 100644 index 0000000..6fcbe84 --- /dev/null +++ b/src/client/thread_store.ts @@ -0,0 +1,118 @@ +import Dexie, { type EntityTable } from "dexie"; +import { KyInstance } from "ky"; + +import get_conversation from "@src/threadsapi/get_conversation"; +import get_media_insights from "@src/threadsapi/get_media_insights"; +import { fetch_user_threads_page, GetUserThreadsParams } from "@src/threadsapi/get_user_threads"; + +import { AccessTokenResponse, ConversationResponse, SimplifedMediaMetricTypeMap, ThreadMedia } from "../threadsapi/types"; + +export interface CachedThreadData { + thread_id: ThreadID; + media: ThreadMedia; + replies: ConversationResponse | null; + insights: SimplifedMediaMetricTypeMap | null; +} + +export type ThreadID = `thread_${string}`; + +function makeThreadID(id: string): ThreadID { + return `thread_${id}`; +} + +function extractThreadID(id: ThreadID): string { + return id.replace(/^thread_/, "").split("_")[0]; +} + +const db = new Dexie("unthread.me/thread_store") as Dexie & { + threads: EntityTable< + CachedThreadData, + "thread_id" // primary key "id" (for the typings only) + >; +}; + +// Schema declaration: +db.version(1).stores({ + threads: "++thread_id, media, replies, insights", // Primary key and indexed props +}); + +export { db }; + +const loadThreadsData = async (ky: KyInstance, token: AccessTokenResponse, params?: GetUserThreadsParams) => { + const promises: Promise[] = []; + + if (localStorage.getItem("unthread.me/thread_store")) { + localStorage.removeItem("unthread.me/thread_store"); + } + // let count = 0; + const fetchAllPages = async (cursor?: string): Promise => { + const data = await fetch_user_threads_page(ky, token, params, cursor); + + promises.push( + db.threads.bulkPut( + data.data.map((thread) => { + const id = makeThreadID(thread.id); + return { + thread_id: id, + media: thread, + replies: null, + insights: null, + }; + }), + // { + // allKeys: false, + // }, + ), + ); + + for (const thread of data.data) { + const thread_id = makeThreadID(thread.id); + promises.push(loadThreadInsightsData(ky, token, thread_id), loadThreadRepliesData(ky, token, thread_id)); + } + + if (data.paging?.cursors.after && !params?.limit) { + await fetchAllPages(data.paging.cursors.after); + } + }; + + await fetchAllPages(); + await Promise.all(promises); +}; + +const loadThreadInsightsData = async (ky: KyInstance, token: AccessTokenResponse, id: ThreadID) => { + await get_media_insights(ky, token, extractThreadID(id)).then(async (data) => { + await db.threads.update(id, { insights: data }); + }); +}; +const loadThreadRepliesData = async (ky: KyInstance, token: AccessTokenResponse, id: ThreadID) => { + await get_conversation(ky, token, extractThreadID(id)).then(async (data) => { + await db.threads.update(id, { replies: data }); + }); +}; + +export default { + getThreadInsights: async (id: ThreadID) => { + return await db.threads.get(id).then((data) => data?.insights); + }, + + getThreadReplies: async (id: ThreadID) => { + return await db.threads.get(id).then((data) => data?.replies); + }, + + getThreadMedia: async (id: ThreadID) => { + return await db.threads.get(id).then((data) => data?.media); + }, + + loadThreadsData, + + refreshThreadsLast2Days: async (ky: KyInstance, token: AccessTokenResponse) => { + await loadThreadsData(ky, token, { since: `${Math.round((Date.now() - 1000 * 60 * 60 * 24 * 2) / 1000)}` }); + }, + + clearThreads: () => { + void db.threads.clear(); + }, +}; + +/// check if unthread.me/thread_store exists in local storage +// if it does, delete it diff --git a/src/components/Loader.tsx b/src/components/Loader.tsx index 59caedf..587810e 100644 --- a/src/components/Loader.tsx +++ b/src/components/Loader.tsx @@ -1,6 +1,6 @@ const Loader = () => ( -
-
+
+
); diff --git a/src/components/UserProfile2.tsx b/src/components/UserProfile2.tsx index 9e60513..9a72d58 100644 --- a/src/components/UserProfile2.tsx +++ b/src/components/UserProfile2.tsx @@ -8,6 +8,7 @@ import useThreadsListSortedByDate from "@src/client/hooks/useThreadsListByDate"; import { useTimePeriodLastNDaysFromToday } from "@src/client/hooks/useTimePeriod"; import useTokenStore from "@src/client/hooks/useTokenStore"; import useUserInsights from "@src/client/hooks/useUserInsights"; +import thread_store from "@src/client/thread_store"; import { formatNumber, getDateStringInPacificTime, getTimeInPacificTimeWithVeryPoorPerformance } from "@src/lib/ml"; import DailyReportView from "./DailyReportView"; @@ -41,7 +42,6 @@ export default function UserProfile2() { const [refreshAllThreads, refreshAllThreadsLoading, refreshAllThreadsErr] = useAllThreadsRefresher(); const [refreshUserData, refreshUserDataLoading, refreshUserDataError] = useUserDataRefresher(); - const clearThreads = useCacheStore((state) => state.clearThreads); const clearUser = useCacheStore((state) => state.clearUserData); const last30Days = useTimePeriodLastNDaysFromToday(30); @@ -111,7 +111,7 @@ export default function UserProfile2() { }, { label: "clear threads", - action: clearThreads, + action: thread_store.clearThreads, isLoading: false, error: false, emoji: "🗑️", diff --git a/src/components/UserThreadsView.tsx b/src/components/UserThreadsView.tsx index c998fc1..476cd89 100644 --- a/src/components/UserThreadsView.tsx +++ b/src/components/UserThreadsView.tsx @@ -1,8 +1,8 @@ import { FC, useState } from "react"; -import { ThreadID } from "@src/client/cache_store"; -import useCacheStore from "@src/client/hooks/useCacheStore"; +import useThread from "@src/client/hooks/useThread"; import useThreadsListSortedByDate from "@src/client/hooks/useThreadsListByDate"; +import { ThreadID } from "@src/client/thread_store"; import { Reply } from "@src/threadsapi/types"; const UserThreadsView = () => { @@ -30,8 +30,8 @@ const UserThreadsView = () => {
{threads.map((thread, idx) => ( -
- +
+
))}
@@ -42,7 +42,11 @@ const UserThreadsView = () => { const ThreadCard: FC<{ threadid: ThreadID; idx: number }> = ({ threadid, idx }) => { // const [likes, views, replies, quotes, reposts] = useThreadInfo(thread); - const thread = useCacheStore((state) => state.user_threads[threadid]); + const thread = useThread(threadid); + + if (!thread) { + return
Loading...
; + } const likes = thread.insights?.total_likes ?? 0; const views = thread.insights?.total_views ?? 0; diff --git a/src/components/WordSegmentLineChart.tsx b/src/components/WordSegmentLineChart.tsx index 2380ab0..5b813d4 100644 --- a/src/components/WordSegmentLineChart.tsx +++ b/src/components/WordSegmentLineChart.tsx @@ -7,6 +7,7 @@ import useThreadsListSortedByDate from "@src/client/hooks/useThreadsListByDate"; import useTimePeriod, { useTimePeriodFilteredData } from "@src/client/hooks/useTimePeriod"; import { MetricKey, useByWord, WordType, wordTypes } from "@src/client/hooks/useWords"; import ErrorMessage from "@src/components/ErrorMessage"; +import Loader from "@src/components/Loader"; const WordSegmentLineChart: FC = () => { const [threads] = useThreadsListSortedByDate(); @@ -404,7 +405,7 @@ const WordSegmentLineChart: FC = () => { maxHeight: "100%", }} > - {dats.length === 0 ? : Chart} + {dats.length === 0 ? threads.length == 0 ? : : Chart}
); diff --git a/src/lib/ml.ts b/src/lib/ml.ts index fde0b6e..45c26bb 100644 --- a/src/lib/ml.ts +++ b/src/lib/ml.ts @@ -1,6 +1,6 @@ import { linearRegression, linearRegressionLine } from "simple-statistics"; -import { CachedThreadData } from "@src/client/cache_store"; +import { CachedThreadData } from "@src/client/thread_store"; import { SimplifedMetricTypeMap } from "@src/threadsapi/types"; // export function getDateStringInPacificTime(date: Date) { @@ -110,7 +110,7 @@ export const convertToInsightsByDate = (data: CachedThreadData): MinimalThreadDa }; }; -export const isdbAll = (userInsights: SimplifedMetricTypeMap | null, userThreads: MinimalThreadData[]): Record => { +export const isdbAll = (userInsights: SimplifedMetricTypeMap | null, userThreads: CachedThreadData[]): Record => { const startDate = new Date("2024-04-01"); const endDate = new Date(); @@ -119,7 +119,7 @@ export const isdbAll = (userInsights: SimplifedMetricTypeMap | null, userThreads export const isdbAllNoRelative = ( userInsights: SimplifedMetricTypeMap | null, - userThreads: MinimalThreadData[], + userThreads: CachedThreadData[], ): Record => { const startDate = new Date("2024-04-01"); const endDate = new Date(); @@ -131,7 +131,7 @@ export const isdbRange = ( startDate: Date, endDate: Date, userInsights: SimplifedMetricTypeMap | null, - userThreads: MinimalThreadData[], + userThreads: CachedThreadData[], includeRelativeInsights = true, ): Record => { const days: string[] = []; @@ -171,7 +171,7 @@ export const isdbRange = ( return wrk; }; -export const isbd = (date: string, userInsights: SimplifedMetricTypeMap | null, userThreads: MinimalThreadData[]): InsightsByDate => { +export const isbd = (date: string, userInsights: SimplifedMetricTypeMap | null, userThreads: CachedThreadData[]): InsightsByDate => { const ONE_DAY = 24 * 60 * 60 * 1000; const dateInfo = { @@ -202,6 +202,7 @@ export const isbd = (date: string, userInsights: SimplifedMetricTypeMap | null, const totalViews = userInsights.views_by_day.filter((v) => getDateStringInPacificTime(new Date(v.label)) === date)[0]?.value ?? 0; const cumlativePostInsights = userThreads + .map((thread) => convertToInsightsByDate(thread)) .filter((thread) => { return getDateStringInPacificTime(new Date(thread.timestamp)) === date; }) diff --git a/tsconfig.app.json b/tsconfig.app.json index a51059c..413fc93 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -2,7 +2,11 @@ "compilerOptions": { "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], "module": "ESNext", "skipLibCheck": true, /* Bundler mode */ @@ -21,13 +25,18 @@ /* Path */ "baseUrl": "./", "paths": { - "@src/*": ["src/*"] + "@src/*": [ + "src/*" + ] } }, - "include": ["src", "test"], + "include": [ + "src", + "test" + ], "references": [ { "path": "./tsconfig.node.json" } ] -} +} \ No newline at end of file diff --git a/tsconfig.node.json b/tsconfig.node.json index 62b44f3..ce0a1c6 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -9,5 +9,10 @@ "strict": true, "noEmit": false }, - "include": ["vite.config.ts", "tailwind.config.ts", "scripts/**/*.ts", "*.config.ts"] -} + "include": [ + "vite.config.ts", + "tailwind.config.ts", + "scripts/**/*.ts", + "*.config.ts" + ] +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index c488848..595c306 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,11 +2,11 @@ import basicSsl from "@vitejs/plugin-basic-ssl"; import react from "@vitejs/plugin-react-swc"; import { defineConfig } from "vite"; import path from "path"; +import wasm from "vite-plugin-wasm"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react(), basicSsl()], - + plugins: [react(), basicSsl(), wasm()], build: { chunkSizeWarningLimit: 600, sourcemap: false,