client/packages/notifications/src/context.tsx
Alois 7ea3b32286
All checks were successful
/ build-web (push) Successful in 1m9s
/ build-desktop (push) Successful in 11m16s
/ build-mobile (push) Successful in 15m50s
/ release (push) Successful in 23s
(feat): add native notifications
(feat): add navbar profile popover
(fix): chat input box click area to slim
(qol): format
2026-05-24 18:31:27 +02:00

170 lines
4.6 KiB
TypeScript

import { useCrypto } from "@tensamin/crypto/context";
import { useStorage } from "@tensamin/storage/context";
import { useUser } from "@tensamin/user/context";
import { useChat } from "@tensamin/chat/context";
import { useTTP } from "@tensamin/ttp";
import { createContext, useEffect, useContext } from "react";
import z from "zod";
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";
import { useNavigate } from "@tanstack/react-router";
export const context = createContext<contextType | undefined>(undefined);
async function requestNotificationPermission() {
if (!("Notification" in window)) return false;
if (Notification.permission === "granted") return true;
const permission = await Notification.requestPermission();
return permission === "granted";
}
export default function Provider(props: { children: React.ReactNode }) {
const { subscribePush, send } = useTTP();
const { load } = useStorage();
const { get } = useUser();
const { decryptText, getSharedSecret } = useCrypto();
const { addLiveMessage, userId } = useChat();
const { moveUserIdToTop } = useSession();
const navigate = useNavigate();
useEffect(() => {
return subscribePush(async (ttpMessage) => {
if (ttpMessage.type === "message_live") {
const { message, sender_id } = ttpMessage.data as {
message: z.infer<typeof messageSchema>;
sender_id: number;
};
const user = await get(sender_id);
const decryptedContent = await decryptText(
await getSharedSecret(
await load("private_key"),
await get(await load("user_id")).then((data) => data.public_key),
user.public_key,
),
message.content,
);
// Update message state
if (
(await load("settings.read_confirmations")) &&
userId === sender_id
) {
void send(
"message_state",
{
message_state: "read",
},
{
id: ttpMessage.id,
},
);
} else {
void send(
"message_state",
{
message_state: "received",
},
{
id: ttpMessage.id,
},
);
}
if (userId === sender_id) {
addLiveMessage({
...message,
content: decryptedContent,
sent_by_self: false,
});
return;
}
// todo: add notification symbol to conversation cards (incl. message start)
moveUserIdToTop(sender_id);
if (isTauri()) {
console.log("weewoo");
} else {
const hasPermissions = await requestNotificationPermission();
if (hasPermissions) {
const notification = new Notification(user.display, {
body: decryptedContent,
icon: user.avatar || user.display.slice(0, 2).toUpperCase(),
badge: user.avatar || user.display.slice(0, 2).toUpperCase(),
tag: `message-${user.user_id}`,
silent: true,
});
notification.onclick = () => {
window.focus();
navigate({
to: `/chat?id=${user.user_id}`,
});
notification.close();
};
} else {
sonnerToast(user.display, {
classNames: {
content: "pl-4",
},
description: decryptedContent,
icon: (
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>
{user.display.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
),
});
}
}
}
});
}, [
subscribePush,
decryptText,
getSharedSecret,
load,
get,
addLiveMessage,
userId,
moveUserIdToTop,
send,
navigate,
]);
return (
<context.Provider value={{ test: () => {} }}>
{props.children}
</context.Provider>
);
}
type contextType = {
test: () => void;
};
/**
* Executes useNotifications.
* @param none This function has no parameters.
* @returns contextType.
*/
export function useNotifications(): contextType {
const ctx = useContext(context);
if (!ctx) {
throw new Error("useNotifications must be used within a ChatProvider");
}
return ctx;
}