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 b5e21f2..f3d0531 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index f1eb290..5ad721a 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,8 @@ "@types/react-dom": "npm:types-react-dom@rc" }, "devDependencies": { + "dexie": "^4.0.8", + "dexie-react-hooks": "^1.1.7", "@headlessui/react": "^2.1.2", "@react-spring/web": "^9.7.3", "@tailwindcss/forms": "^0.5.7", @@ -66,6 +68,7 @@ "typescript": "^5.5.3", "ua-parser-js": "^1.0.38", "vite": "^5.3.4", + "vite-plugin-wasm": "^3.3.0", "zustand": "^4.5.4" } -} +} \ No newline at end of file diff --git a/src/client/cache_store.ts b/src/client/cache_store.ts index 6eb27fd..b13b41d 100644 --- a/src/client/cache_store.ts +++ b/src/client/cache_store.ts @@ -3,42 +3,13 @@ import { create } from "zustand"; import { combine, createJSONStorage, devtools, persist } from "zustand/middleware"; import threadsapi from "@src/threadsapi"; -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, - BreakdownMetricTypeMap, - ConversationResponse, - SimplifedMediaMetricTypeMap, - SimplifedMetricTypeMap, - ThreadMedia, - UserProfileResponse, -} from "../threadsapi/types"; - -export interface CachedThreadData { - 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_/, ""); -} +import { AccessTokenResponse, BreakdownMetricTypeMap, SimplifedMetricTypeMap, UserProfileResponse } from "../threadsapi/types"; interface CacheStoreState { user_profile: UserProfileResponse | null; user_insights: SimplifedMetricTypeMap | null; user_follower_demographics: BreakdownMetricTypeMap | null; - user_threads: Record; 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,