(feat): add cache package
All checks were successful
/ build-web (push) Successful in 7m26s
/ build-desktop (linux) (push) Successful in 11m29s
/ build-mobile (push) Successful in 19m15s
/ release (push) Successful in 3m3s

(feat): improve local storage security
(feat): move settings to dedicated settings package
This commit is contained in:
Alois 2026-07-11 22:08:01 +02:00
commit 790a1db788
54 changed files with 1984 additions and 947 deletions

View file

@ -16,6 +16,7 @@
"build": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@tensamin/cache": "workspace:*",
"@tanstack/pacer": "^0.21.1",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-router": "^1.0.0",

View file

@ -27,6 +27,8 @@ import { useMTP } from "@tensamin/mtp";
import { log, toast } from "@tensamin/shared/log";
import { useSession } from "@tensamin/storage/session";
import { useUser } from "@tensamin/user/context";
import { createCache } from "@tensamin/cache";
import { secureValueCodec } from "@tensamin/storage/secure";
export const context = createContext<contextType | undefined>(undefined);
@ -158,6 +160,7 @@ export default function Provider({ children }: { children: ReactNode }) {
const [errorDescription, setErrorDescription] = useState("");
const [liveMessagesState, setLiveMessagesState] = useState<LiveMessage[]>([]);
const [ownId, setOwnId] = useState(0);
const [currentChatSecretState, setCurrentChatSecretState] = useState<{
userId: number;
value: Uint8Array | null;
@ -186,6 +189,10 @@ export default function Provider({ children }: { children: ReactNode }) {
return currentChatSecretState.value;
}, [currentChatSecretState, userIdValue]);
useEffect(() => {
load("user_id").then(setOwnId);
}, [load]);
useEffect(() => {
if (!userIdValue) return;
@ -328,6 +335,44 @@ export default function Provider({ children }: { children: ReactNode }) {
[load, send],
);
const decryptMessages = useCallback(
async (messages: RawMessages) => {
if (!currentChatSecret) return [];
return Promise.all(
messages.map(async (message) => {
try {
return {
...message,
Content: await decryptChatText(
currentChatSecret,
message.Content,
),
};
} catch (err) {
log(1, "chat", "red", "Failed to decrypt historical message", err, {
SendTime: message.SendTime,
});
return {
...message,
Content: "Failed to decrypt message",
decryptionFailed: true,
};
}
}),
);
},
[currentChatSecret],
);
const getCachedMessages = useCallback(async () => {
if (!ownId || !userIdValue || !currentChatSecret) return [];
const cache = createCache(String(ownId), {
codec: secureValueCodec,
});
const window = await cache.conversations.get(userIdValue);
return decryptMessages(window?.Messages ?? []);
}, [currentChatSecret, decryptMessages, ownId, userIdValue]);
const getMessages = useCallback(
async (amount: number, offset: number) => {
if (!currentChatSecret) {
@ -359,36 +404,22 @@ export default function Provider({ children }: { children: ReactNode }) {
});
}
return await Promise.all(
sorted.map(async (message) => {
try {
return {
...message,
Content: await decryptChatText(
currentChatSecret,
message.Content,
),
};
} catch (err) {
log(1, "chat", "red", "Failed to decrypt historical message", err, {
SendTime: message.SendTime,
});
return {
...message,
Content: "Failed to decrypt message",
decryptionFailed: true,
};
}
}),
);
return decryptMessages(sorted);
},
[currentChatSecret, send, userIdValue],
[currentChatSecret, decryptMessages, send, userIdValue],
);
const [ownId, setOwnId] = useState(0);
useEffect(() => {
load("user_id").then(setOwnId);
}, [load]);
if (!currentChatSecret || !ownId || !userIdValue) return;
const queryKey = ["chat-messages", String(userIdValue), true] as const;
void getCachedMessages().then((messages) => {
if (messages.length === 0 || queryClient.getQueryData(queryKey)) return;
queryClient.setQueryData<InfiniteData<RawMessages>>(queryKey, {
pages: [messages],
pageParams: [0],
});
});
}, [currentChatSecret, getCachedMessages, ownId, userIdValue]);
const editMessage = useCallback(
(sendTime: number, edit: MessageEdit) => {
@ -452,7 +483,6 @@ export default function Provider({ children }: { children: ReactNode }) {
setLiveMessagesState((prev) =>
prev.filter((message) => message.SendTime !== sendTime),
);
const queryKey = [
"chat-messages",
String(userIdValue),

View file

@ -6,6 +6,7 @@
- Placeholder image if media fails to load
- Signature verifications via ed25519 key
- Confirmation when exiting with text in the input box.
- Add arrow up hotkey to edit last message
- Add arrow up hotkey to edit last message (req: packages/hotkeys)
- Drop any unique reactions above 10
- Make the emoji picker not get moved with the mini context menu
- Add proper loading skeleton