(feat): add cache for conversations and communities
(feat): conversations now get moved to the top when engaged with (qol): update todo
This commit is contained in:
parent
006c222cc5
commit
fd15215f15
14 changed files with 178 additions and 67 deletions
|
|
@ -8,13 +8,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger } from "@tensamin/ui";
|
|||
import { displayCallId } from "@tensamin/call/utils";
|
||||
import { useState } from "react";
|
||||
import { SidebarTrigger, useSidebar } from "@tensamin/ui";
|
||||
import { useTTP } from "@tensamin/ttp";
|
||||
import { WindowControls as Controls } from "@tensamin/ui";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
export default function Navbar({ forMobile }: { forMobile: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const callState = useCall((state) => state.state);
|
||||
const { contacts } = useTTP();
|
||||
const { contacts } = useSession();
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,18 @@
|
|||
import * as React from "react";
|
||||
import { useVirtualizer } from "@tanstack/react-virtual";
|
||||
import { useTTP } from "@tensamin/ttp";
|
||||
|
||||
import Switch from "./switch";
|
||||
import ConversationModal from "../modal/conversation";
|
||||
import CommunityModal from "../modal/community";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
/**
|
||||
* Executes List.
|
||||
* @param none This function has no parameters.
|
||||
* @returns unknown.
|
||||
*/
|
||||
export default function List() {
|
||||
const [category, setCategory] = React.useState<
|
||||
"conversations" | "communities"
|
||||
>("conversations");
|
||||
|
||||
const { contacts, communities } = useTTP();
|
||||
const { contacts, communities } = useSession();
|
||||
const items = category === "conversations" ? contacts : communities;
|
||||
|
||||
const scrollRef = React.useRef<HTMLDivElement | null>(null);
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import Login from "@/routes/screens/login";
|
|||
|
||||
import ChatContext from "@tensamin/chat/context";
|
||||
import { useInitializeCall } from "@tensamin/call/store";
|
||||
import { Provider as SocketContext } from "@tensamin/ttp";
|
||||
import { Provider as TTPProvider } from "@tensamin/ttp";
|
||||
import UserContext from "@tensamin/user/context";
|
||||
import DeeplinkContext from "@tensamin/tauri/deeplinkHandler";
|
||||
import NotificationsProvider from "@tensamin/notifications/context";
|
||||
|
|
@ -35,6 +35,7 @@ import z from "zod";
|
|||
import { useEffect, useState, type ReactNode } from "react";
|
||||
|
||||
import Storage from "@tensamin/storage/context";
|
||||
import Session from "@tensamin/storage/session";
|
||||
import Crypto from "@tensamin/crypto/context";
|
||||
import Mobile from "@tensamin/tauri/context";
|
||||
|
||||
|
|
@ -141,20 +142,22 @@ function RootShell() {
|
|||
|
||||
function AppShell() {
|
||||
return (
|
||||
<SocketContext>
|
||||
<UserContext>
|
||||
<CallInit />
|
||||
<TAuthWrapper>
|
||||
<AppLayout>
|
||||
<ChatContext>
|
||||
<NotificationsProvider>
|
||||
<Outlet />
|
||||
</NotificationsProvider>
|
||||
</ChatContext>
|
||||
</AppLayout>
|
||||
</TAuthWrapper>
|
||||
</UserContext>
|
||||
</SocketContext>
|
||||
<TTPProvider>
|
||||
<Session>
|
||||
<UserContext>
|
||||
<CallInit />
|
||||
<TAuthWrapper>
|
||||
<AppLayout>
|
||||
<ChatContext>
|
||||
<NotificationsProvider>
|
||||
<Outlet />
|
||||
</NotificationsProvider>
|
||||
</ChatContext>
|
||||
</AppLayout>
|
||||
</TAuthWrapper>
|
||||
</UserContext>
|
||||
</Session>
|
||||
</TTPProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
1
bun.lock
1
bun.lock
|
|
@ -197,6 +197,7 @@
|
|||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/ui": "*",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { useTTP } from "@tensamin/ttp";
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@tensamin/ui";
|
||||
import Wrapper from "@tensamin/user/wrapper";
|
||||
import { Mail } from "lucide-react";
|
||||
import { sendCallInvite } from "../../store";
|
||||
import { log, toast } from "@tensamin/shared/log";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
export default function InviteButton({
|
||||
className,
|
||||
|
|
@ -12,7 +12,7 @@ export default function InviteButton({
|
|||
className?: string;
|
||||
iconSize?: number;
|
||||
}) {
|
||||
const { contacts } = useTTP();
|
||||
const { contacts } = useSession();
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import { useTTP } from "@tensamin/ttp";
|
|||
import { log, toast } from "@tensamin/shared/log";
|
||||
import { cn, useIsMobile } from "@tensamin/ui";
|
||||
import { encryptText } from "@tensamin/crypto/worker";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
export default function InputComponent({
|
||||
value,
|
||||
|
|
@ -23,6 +24,7 @@ export default function InputComponent({
|
|||
const { send } = useTTP();
|
||||
const { addLiveMessage, sharedSecret, userId, inputBoxRef } = useChat();
|
||||
const { load } = useStorage();
|
||||
const { moveUserIdToTop } = useSession();
|
||||
|
||||
React.useEffect(() => {
|
||||
void load("settings.reverse_enter_behavior").then((shouldInvert) => {
|
||||
|
|
@ -78,6 +80,8 @@ export default function InputComponent({
|
|||
toast("error", "Failed to send message");
|
||||
});
|
||||
|
||||
moveUserIdToTop(userId);
|
||||
|
||||
setValue("");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { useUser } from "@tensamin/user/context";
|
|||
import { useStorage } from "@tensamin/storage/context";
|
||||
import { useTTP } from "@tensamin/ttp";
|
||||
import { log } from "@tensamin/shared/log";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
export const context = createContext<contextType | undefined>(undefined);
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
const { get } = useUser();
|
||||
const { load } = useStorage();
|
||||
const { send, subscribePush } = useTTP();
|
||||
const { moveUserIdToTop } = useSession();
|
||||
|
||||
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
|
||||
const [currentSharedSecretState, setCurrentSharedSecretState] = useState<{
|
||||
|
|
@ -171,32 +173,39 @@ export default function Provider(props: { children: ReactNode }) {
|
|||
[send, userIdValue, currentSharedSecret, decryptText],
|
||||
);
|
||||
|
||||
const addLiveMessage = useCallback((message: RawMessage) => {
|
||||
const localId =
|
||||
globalThis.crypto?.randomUUID?.() ??
|
||||
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
const addLiveMessage = useCallback(
|
||||
(message: RawMessage) => {
|
||||
const localId =
|
||||
globalThis.crypto?.randomUUID?.() ??
|
||||
`${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
|
||||
setLiveMessagesState((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...message,
|
||||
localId,
|
||||
failed: false,
|
||||
},
|
||||
]);
|
||||
if (!message.sent_by_self) {
|
||||
moveUserIdToTop(userIdValue);
|
||||
}
|
||||
|
||||
return {
|
||||
setFailed: (failed: boolean) => {
|
||||
setLiveMessagesState((prev) =>
|
||||
prev.map((liveMessage) =>
|
||||
liveMessage.localId === localId
|
||||
? { ...liveMessage, failed }
|
||||
: liveMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
setLiveMessagesState((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...message,
|
||||
localId,
|
||||
failed: false,
|
||||
},
|
||||
]);
|
||||
|
||||
return {
|
||||
setFailed: (failed: boolean) => {
|
||||
setLiveMessagesState((prev) =>
|
||||
prev.map((liveMessage) =>
|
||||
liveMessage.localId === localId
|
||||
? { ...liveMessage, failed }
|
||||
: liveMessage,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
[userIdValue, moveUserIdToTop],
|
||||
);
|
||||
|
||||
const clearLiveMessages = useCallback(() => {
|
||||
setLiveMessagesState([]);
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { toast as sonnerToast } from "sonner";
|
|||
import { message as messageSchema } from "@tensamin/shared/data";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@tensamin/ui";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { useSession } from "@tensamin/storage/session";
|
||||
|
||||
export const context = createContext<contextType | undefined>(undefined);
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
|||
const { get } = useUser();
|
||||
const { decryptText, getSharedSecret } = useCrypto();
|
||||
const { addLiveMessage, userId } = useChat();
|
||||
const { moveUserIdToTop } = useSession();
|
||||
|
||||
useEffect(() => {
|
||||
return subscribePush(async (ttpMessage) => {
|
||||
|
|
@ -60,6 +62,8 @@ export default function Provider(props: { children: React.ReactNode }) {
|
|||
|
||||
// add notification symbol to conversation cards
|
||||
|
||||
moveUserIdToTop(sender_id);
|
||||
|
||||
if (isTauri()) {
|
||||
console.log("weewoo");
|
||||
} else {
|
||||
|
|
@ -86,6 +90,7 @@ export default function Provider(props: { children: React.ReactNode }) {
|
|||
get,
|
||||
addLiveMessage,
|
||||
userId,
|
||||
moveUserIdToTop,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -39,6 +39,13 @@ export const failedUser = {
|
|||
username: "unknown",
|
||||
} as z.infer<typeof ttp.get_user_data.response>;
|
||||
|
||||
export type Contacts = z.infer<
|
||||
typeof ttp.challenge_response.response.shape.contacts
|
||||
>;
|
||||
export type Communities = z.infer<
|
||||
typeof ttp.challenge_response.response.shape.communities
|
||||
>;
|
||||
|
||||
// TTP
|
||||
export const ttp = {
|
||||
identification: {
|
||||
|
|
@ -230,6 +237,8 @@ export interface Storage extends SettingsStorageDefaults {
|
|||
analytics_usage_data: boolean;
|
||||
analytics_done: boolean;
|
||||
legal_docs: z.infer<typeof legalDocsSchema>;
|
||||
cached_contacts: Contacts;
|
||||
cached_communities: Communities;
|
||||
}
|
||||
|
||||
export const storageDefaults: Storage = {
|
||||
|
|
@ -260,4 +269,6 @@ export const storageDefaults: Storage = {
|
|||
unix: 0,
|
||||
},
|
||||
},
|
||||
cached_contacts: [],
|
||||
cached_communities: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./session": "./src/session.tsx",
|
||||
"./context": "./src/context.tsx",
|
||||
"./indexed-db": "./src/indexed-db.ts"
|
||||
},
|
||||
|
|
@ -14,6 +15,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tensamin/shared": "workspace:*",
|
||||
"@tensamin/ttp": "workspace:*",
|
||||
"@tensamin/ui": "*",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
|
|
|
|||
84
packages/storage/src/session.tsx
Normal file
84
packages/storage/src/session.tsx
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { useTTP } from "@tensamin/ttp";
|
||||
import {
|
||||
createContext,
|
||||
type ReactNode,
|
||||
useState,
|
||||
useContext,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useStorage } from "./context";
|
||||
import type { Contacts, Communities } from "@tensamin/shared/data";
|
||||
|
||||
interface SessionContextType {
|
||||
contacts: Contacts;
|
||||
communities: Communities;
|
||||
moveUserIdToTop: (userId: number) => void;
|
||||
}
|
||||
|
||||
const SessionContext = createContext<SessionContextType | undefined>(undefined);
|
||||
|
||||
export default function SessionProvider({ children }: { children: ReactNode }) {
|
||||
const { freshContacts, freshCommunities } = useTTP();
|
||||
const { load, save } = useStorage();
|
||||
const [contacts, setContacts] = useState<Contacts>([]);
|
||||
const [communities, setCommunities] = useState<Communities>([]);
|
||||
|
||||
// Get cached data and merge fresh data
|
||||
useEffect(() => {
|
||||
load("cached_contacts").then((cachedData) => {
|
||||
setContacts([
|
||||
...freshContacts,
|
||||
...cachedData.filter(
|
||||
(item) =>
|
||||
!freshContacts.some((fresh) => fresh.user_id === item.user_id),
|
||||
),
|
||||
]);
|
||||
});
|
||||
load("cached_communities").then((cachedData) => {
|
||||
if (cachedData && freshCommunities) {
|
||||
setCommunities([
|
||||
...freshCommunities,
|
||||
...cachedData.filter(
|
||||
(item) =>
|
||||
!freshCommunities.some(
|
||||
(fresh) => fresh.community_id === item.community_id,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}, [load, freshContacts, freshCommunities]);
|
||||
|
||||
// Save data
|
||||
useEffect(() => {
|
||||
save("cached_contacts", contacts);
|
||||
}, [contacts, save]);
|
||||
useEffect(() => {
|
||||
save("cached_communities", communities);
|
||||
}, [communities, save]);
|
||||
|
||||
const moveUserIdToTop = (userId: number) => {
|
||||
setContacts((prevContacts) => {
|
||||
const userIndex = prevContacts.findIndex(
|
||||
(contact) => contact.user_id === userId,
|
||||
);
|
||||
if (userIndex === -1) return prevContacts;
|
||||
const [user] = prevContacts.splice(userIndex, 1);
|
||||
return [user, ...prevContacts];
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<SessionContext.Provider value={{ contacts, communities, moveUserIdToTop }}>
|
||||
{children}
|
||||
</SessionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSession(): SessionContextType {
|
||||
const context = useContext(SessionContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useSession must be used within a SessionProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
1
packages/storage/todo.md
Normal file
1
packages/storage/todo.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Add `notifications: number` to contacts
|
||||
|
|
@ -25,15 +25,15 @@ import {
|
|||
TRANSPORT_URL,
|
||||
} from "./values";
|
||||
import {
|
||||
type Communities,
|
||||
type Contacts,
|
||||
ttp as schemas,
|
||||
ttp,
|
||||
type TTP as Schemas,
|
||||
} from "@tensamin/shared/data";
|
||||
import { LoadingScreen as Loading } from "@tensamin/ui";
|
||||
import { ErrorScreen } from "@tensamin/ui";
|
||||
|
||||
import { version } from "../../../package.json";
|
||||
import z from "zod";
|
||||
import { decryptText } from "@tensamin/crypto/worker";
|
||||
|
||||
const FATAL_IDENTIFICATION_ERROR_TYPES = new Set([
|
||||
|
|
@ -147,10 +147,8 @@ type ContextType = {
|
|||
ownPing: number;
|
||||
iotaPing: number;
|
||||
identified: boolean;
|
||||
contacts: z.infer<typeof ttp.challenge_response.response.shape.contacts>;
|
||||
communities: z.infer<
|
||||
typeof ttp.challenge_response.response.shape.communities
|
||||
>;
|
||||
freshContacts: Contacts;
|
||||
freshCommunities: Communities;
|
||||
};
|
||||
|
||||
const TTPContext = createContext<ContextType | undefined>(undefined);
|
||||
|
|
@ -178,12 +176,8 @@ export function Provider(props: {
|
|||
const [error, setError] = useState("");
|
||||
const [errorDescription, setErrorDescription] = useState("");
|
||||
|
||||
const [communities, setCommunities] = useState<
|
||||
z.infer<typeof ttp.challenge_response.response.shape.communities>
|
||||
>([]);
|
||||
const [contacts, setContacts] = useState<
|
||||
z.infer<typeof ttp.challenge_response.response.shape.contacts>
|
||||
>([]);
|
||||
const [freshCommunities, setFreshCommunities] = useState<Communities>([]);
|
||||
const [freshContacts, setFreshContacts] = useState<Contacts>([]);
|
||||
|
||||
const clientRef = useRef<ReturnType<
|
||||
typeof createTransportClient<Schemas>
|
||||
|
|
@ -500,8 +494,8 @@ export function Provider(props: {
|
|||
});
|
||||
|
||||
// Data handling
|
||||
setContacts(finalResponse.data.contacts);
|
||||
setCommunities(finalResponse.data.communities);
|
||||
setFreshContacts(finalResponse.data.contacts);
|
||||
setFreshCommunities(finalResponse.data.communities);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -613,8 +607,8 @@ export function Provider(props: {
|
|||
ownPing,
|
||||
iotaPing,
|
||||
identified,
|
||||
contacts,
|
||||
communities,
|
||||
freshContacts,
|
||||
freshCommunities,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
|
|
|
|||
|
|
@ -474,7 +474,9 @@ function dedupePackageRecords(records: PackageRecord[]): PackageRecord[] {
|
|||
|
||||
unique.set(key, {
|
||||
...existing,
|
||||
copiedFiles: [...new Set([...existing.copiedFiles, ...record.copiedFiles])],
|
||||
copiedFiles: [
|
||||
...new Set([...existing.copiedFiles, ...record.copiedFiles]),
|
||||
],
|
||||
license: existing.license ?? record.license,
|
||||
description: existing.description ?? record.description,
|
||||
homepage: existing.homepage ?? record.homepage,
|
||||
|
|
|
|||
Loading…
Reference in a new issue