diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx index 6487ba1..7ea7afc 100644 --- a/apps/web/src/index.tsx +++ b/apps/web/src/index.tsx @@ -163,7 +163,9 @@ function AppShell() { } function CallInit() { - return useInitializeCall(); + useInitializeCall(); + + return null; } const rootRoute = createRootRoute({ diff --git a/flake.nix b/flake.nix index 41b621a..3773515 100644 --- a/flake.nix +++ b/flake.nix @@ -6,8 +6,8 @@ outputs = {nixpkgs, ...}: let systems = ["x86_64-linux"]; forAllSystems = nixpkgs.lib.genAttrs systems; - version = "0.0.7"; - x86_64DebHash = "sha256-gRkUEx1T7mrCgFEVp0X/ZiaGrKWzO+cCvv33DKCkB+o="; + version = "0.0.6"; + x86_64DebHash = "sha256-rNlyNZFgYVRvJzX1sBz/qLHUdZoyfvHl5xuwb2BtU48="; forgejoBaseUrl = "https://git.methanium.net/tensamin/client/releases/download/${version}"; in { packages = forAllSystems (system: let diff --git a/packages/call/src/components/invitePopup.tsx b/packages/call/src/components/invitePopup.tsx deleted file mode 100644 index f531913..0000000 --- a/packages/call/src/components/invitePopup.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { - Avatar, - AvatarFallback, - AvatarImage, - Button, - Dialog, - DialogContent, -} from "@tensamin/ui"; -import Wrapper from "@tensamin/user/wrapper"; -import { PhoneIncoming, PhoneMissed } from "lucide-react"; - -export default function InvitePopup({ - open, - setOpen, - onAccept, - user, -}: { - open: boolean; - setOpen: (value: boolean) => void; - onAccept: (value: boolean) => void; - user: number; -}) { - return ( - ( - - - - - - {user.display.slice(0, 2).toUpperCase()} - - -

{user.display}

-
- - -
-
-
- )} - /> - ); -} diff --git a/packages/call/src/store.tsx b/packages/call/src/store.tsx index 6f3955c..0d28c61 100644 --- a/packages/call/src/store.tsx +++ b/packages/call/src/store.tsx @@ -6,7 +6,6 @@ import { log, toast } from "@tensamin/shared/log"; import { ttp } from "@tensamin/shared/data"; import { useCrypto } from "@tensamin/crypto/context"; import { useStorage } from "@tensamin/storage/context"; -import { useSession } from "@tensamin/storage/session"; import { useUser } from "@tensamin/user/context"; import { DeepFilterNoiseFilterProcessor } from "deepfilternet3-noise-filter"; import { @@ -25,6 +24,7 @@ import { getLogger, } from "livekit-client"; import z from "zod"; +import { toast as sonnerToast } from "sonner"; import { createScreenShareController, type ScreenShareSession, @@ -33,7 +33,6 @@ import { getSpeakingDetector, disposeSpeakingDetector, } from "./speakingIndicator"; -import InvitePopup from "./components/invitePopup"; // logging setLogExtension( @@ -46,11 +45,6 @@ setLogExtension( type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting"; type CallView = "preview" | "focused" | "grid"; -type IncomingCallInvite = { - callId: string; - callSecret: string; - senderId: number; -}; type CurrentCallData = | (z.infer & { exists: boolean }) | null; @@ -89,7 +83,6 @@ type CallStore = { view: CallView; invitedUserId: number | null; callId: string | null; - incomingCallInvite: IncomingCallInvite | null; callSecret: string | null; livekitToken: string | null; currentCallData: CurrentCallData; @@ -844,7 +837,6 @@ export async function disconnect() { state: "closing", invitedUserId: null, callId: null, - incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -891,7 +883,7 @@ export async function joinCall( log(2, "call", "purple", "Call creation initialised"); useCall.setState({ state: "encrypting", - invitedUserId: sendInvite && !existingCallId ? userId : null, + invitedUserId: sendInvite ? userId : null, }); if (callSecret) { @@ -1007,7 +999,6 @@ export function resetCallState() { view: "preview", invitedUserId: null, callId: null, - incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -1059,7 +1050,6 @@ export const useCall = create(() => ({ view: "preview", invitedUserId: null, callId: null, - incomingCallInvite: null, callSecret: null, livekitToken: null, currentCallData: null, @@ -1090,12 +1080,10 @@ export function useInitializeCall() { const { send, subscribePush } = useTTP(); const { getSharedSecret, decryptText, encryptText } = useCrypto(); const { load } = useStorage(); - const { insertCall } = useSession(); const { get } = useUser(); const callId = useCall((state) => state.callId); const view = useCall((state) => state.view); - const incomingCallInvite = useCall((state) => state.incomingCallInvite); const listenersRegistered = useRef(false); const noiseFilter = useMemo( @@ -1125,54 +1113,30 @@ export function useInitializeCall() { }, [decryptText, encryptText, get, getSharedSecret, load, navigate, send]); const showCallingScreen = useCallback( - (callId: string, callSecret: string, senderId: number) => { - useCall.setState({ - incomingCallInvite: { callId, callSecret, senderId }, + async (callId: string, callSecret: string, senderId: number) => { + const senderName = await get(senderId).then((data) => data.display); + + sonnerToast(`Incoming call from ${senderName}`, { + action: { + label: "Accept", + onClick: () => { + joinCall(senderId, callSecret, callId, false).catch((err) => { + log(1, "call", "red", "Failed to join call", err); + }); + }, + }, + cancel: { + label: "Decline", + onClick: () => { + log(2, "call", "purple", "Declined call invite", { + callId, + senderId, + }); + }, + }, }); }, - [], - ); - - const setInvitePopupOpen = useCallback((open: boolean) => { - if (!open) { - useCall.setState({ incomingCallInvite: null }); - } - }, []); - - const respondToInvite = useCallback( - (accepted: boolean) => { - const invite = useCall.getState().incomingCallInvite; - - useCall.setState({ incomingCallInvite: null }); - - if (!invite) { - return; - } - - insertCall({ - call_id: invite.callId, - call_secret: invite.callSecret, - call_members: [invite.senderId], - }); - - if (accepted) { - joinCall( - invite.senderId, - invite.callSecret, - invite.callId, - false, - ).catch((err) => { - log(1, "call", "red", "Failed to join call", err); - }); - return; - } - - log(2, "call", "purple", "Declined call invite", { - callId: invite.callId, - senderId: invite.senderId, - }); - }, - [insertCall], + [get], ); // listen to call invites @@ -1518,13 +1482,4 @@ export function useInitializeCall() { }); }); }, [callId, send, view]); - - return incomingCallInvite ? ( - - ) : null; } diff --git a/packages/chat/src/components/message.tsx b/packages/chat/src/components/message.tsx index 60afa75..18cf792 100644 --- a/packages/chat/src/components/message.tsx +++ b/packages/chat/src/components/message.tsx @@ -12,9 +12,14 @@ import { TooltipContent, TooltipTrigger, } from "@tensamin/ui"; +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuTrigger, +} from "@tensamin/ui"; import Wrapper from "@tensamin/user/wrapper"; import { useChat } from "../context"; -import MessageContextMenu from "./messageContextMenu"; function MessageComponent({ grouped, @@ -33,66 +38,72 @@ function MessageComponent({ // pt-3 is to get a gap between messages className={`${grouped ? "" : "pt-3"} w-full flex justify-start transition-opacity duration-150 ${actuallyFailed || message.message_state === "awaiting" ? "opacity-50" : ""}`} > - -
- ( - <> - {grouped ? ( -

- {new Date(message.send_time).toLocaleString([], { - hour: "2-digit", - minute: "2-digit", - })} -

- ) : ( - - - - {user.display.slice(0, 2).toUpperCase()} - - - )} - {message.failed && message.message_state === "awaiting" && ( - - -

Failed to send message

-
- } /> -
- )} -
- {!grouped && ( -
-

{user.display}

-

+ + + ( + <> + {grouped ? ( +

{new Date(message.send_time).toLocaleString([], { hour: "2-digit", minute: "2-digit", })}

+ ) : ( + + + + {user.display.slice(0, 2).toUpperCase()} + + + )} + {message.failed && message.message_state === "awaiting" && ( + + +

Failed to send message

+
+ } /> +
+ )} +
+ {!grouped && ( +
+

{user.display}

+

+ {new Date(message.send_time).toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + })} +

+
+ )} +
- )} - -
- - )} - /> -
- + + )} + /> +
+ } + /> + + + + + + ); } diff --git a/packages/chat/src/components/messageContextMenu.tsx b/packages/chat/src/components/messageContextMenu.tsx deleted file mode 100644 index dcbc707..0000000 --- a/packages/chat/src/components/messageContextMenu.tsx +++ /dev/null @@ -1,242 +0,0 @@ -import { - cn, - ContextMenu, - ContextMenuContent, - ContextMenuGroup, - ContextMenuItem, - ContextMenuSeparator, - ContextMenuSub, - ContextMenuSubContent, - ContextMenuSubTrigger, - ContextMenuTrigger, - Drawer, - DrawerContent, - DrawerTrigger, - useIsMobile, -} from "@tensamin/ui"; -import { Pin, Clipboard, Pen, Reply, Forward, Trash } from "lucide-react"; -import { useMemo, useState } from "react"; -import type { ReactElement, ReactNode } from "react"; - -async function copyText(text: string) { - await navigator.clipboard.writeText(text); -} - -type MenuComponents = { - Content: (props: { className?: string; children: ReactNode }) => ReactElement; - Group: (props: { children: ReactNode }) => ReactElement; - Item: (props: { - children: ReactNode; - className?: string; - disabled?: boolean; - onClick?: () => void | Promise; - variant?: "default" | "destructive"; - }) => ReactElement; - Separator: (props: { className?: string }) => ReactElement; - Sub: (props: { children: ReactNode }) => ReactElement; - SubTrigger: (props: { - children: ReactNode; - onClick?: () => void | Promise; - }) => ReactElement; - SubContent: (props: { children: ReactNode }) => ReactElement; -}; - -const desktopMenuComponents: MenuComponents = { - Content: ContextMenuContent, - Group: ContextMenuGroup, - Item: ContextMenuItem, - Separator: ContextMenuSeparator, - Sub: ContextMenuSub, - SubTrigger: ContextMenuSubTrigger, - SubContent: ContextMenuSubContent, -}; - -function getMobileMenuComponents(onClose: () => void): MenuComponents { - return { - Content: ({ className, children }) => ( - -
{children}
-
- ), - Group: ({ children }) =>
{children}
, - Item: ({ children, className, disabled, onClick, variant = "default" }) => { - async function handleClick() { - await onClick?.(); - onClose(); - } - - return ( - - ); - }, - Separator: ({ className }) => ( -
- ), - Sub: ({ children }) =>
{children}
, - SubTrigger: ({ children, onClick }) => { - async function handleClick() { - await onClick?.(); - onClose(); - } - - return ( - - ); - }, - SubContent: ({ children }) =>
{children}
, - }; -} - -function ReactionItems({ Item }: { Item: MenuComponents["Item"] }) { - return Cool item; -} - -function MessageMenuContent({ - components, - content, - devEnabled, - messageId, - onAddReaction, - showReactionItems = true, -}: { - components: MenuComponents; - content: string; - devEnabled: boolean; - messageId: number; - onAddReaction?: () => void | Promise; - showReactionItems?: boolean; -}) { - const { Content, Group, Item, Separator, Sub, SubContent, SubTrigger } = - components; - - return ( - - - - Add Reaction - {showReactionItems && ( - - - - )} - - -

Pin Message

-
- copyText(content)} - > -

Copy Raw

-
-
- - - -

Edit Message

-
- -

Reply

-
- -

Forward

-
-
- - - -

Delete Message

-
-
- {devEnabled && ( - <> - - - copyText(String(messageId))} - > -

Copy ID

-
-
- - )} -
- ); -} - -export default function MessageContextMenu({ - children, - content, - messageId, -}: { - children: ReactElement; - content: string; - messageId: number; -}) { - const isMobile = useIsMobile(); - const devEnabled = useMemo( - () => Number(localStorage.getItem("log_level")) >= 3, - [], - ); - const [mainDrawerOpen, setMainDrawerOpen] = useState(false); - const [reactionDrawerOpen, setReactionDrawerOpen] = useState(false); - const mainDrawerComponents = getMobileMenuComponents(() => - setMainDrawerOpen(false), - ); - const reactionDrawerComponents = getMobileMenuComponents(() => - setReactionDrawerOpen(false), - ); - - if (isMobile) { - return ( - <> - - {children} - setReactionDrawerOpen(true)} - showReactionItems={false} - /> - - - - - - - - ); - } - - return ( - - - - - ); -} diff --git a/packages/chat/todo.md b/packages/chat/todo.md index 63466f3..122f14e 100644 --- a/packages/chat/todo.md +++ b/packages/chat/todo.md @@ -1,2 +1 @@ -- Implement context menu features -- Add default-emoji-hotkey in settings +- Add context menu to messages diff --git a/packages/storage/src/session.tsx b/packages/storage/src/session.tsx index 892f3e6..d992a03 100644 --- a/packages/storage/src/session.tsx +++ b/packages/storage/src/session.tsx @@ -15,7 +15,6 @@ interface SessionContextType { calls: Calls; moveUserIdToTop: (userId: number) => void; insertContact: (userId: number) => void; - insertCall: (call: Calls[number]) => void; } const SessionContext = createContext(undefined); @@ -25,7 +24,6 @@ export default function SessionProvider({ children }: { children: ReactNode }) { const { load, save } = useStorage(); const [contacts, setContacts] = useState([]); const [communities, setCommunities] = useState([]); - const [calls, setCalls] = useState([]); // Get cached data and merge fresh data useEffect(() => { @@ -53,15 +51,6 @@ export default function SessionProvider({ children }: { children: ReactNode }) { }); }, [load, freshContacts, freshCommunities]); - useEffect(() => { - setCalls((prevCalls) => [ - ...freshCalls, - ...prevCalls.filter( - (call) => !freshCalls.some((fresh) => fresh.call_id === call.call_id), - ), - ]); - }, [freshCalls]); - // Save data useEffect(() => { save("cached_contacts", contacts); @@ -97,25 +86,14 @@ export default function SessionProvider({ children }: { children: ReactNode }) { }); }; - const insertCall = (call: Calls[number]) => { - setCalls((prevCalls) => { - if (prevCalls.some((prevCall) => prevCall.call_id === call.call_id)) { - return prevCalls; - } - - return [call, ...prevCalls]; - }); - }; - return ( {children}