diff --git a/.fallowrc.json b/.fallowrc.json deleted file mode 100644 index 320cbb5..0000000 --- a/.fallowrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json", - "entry": ["src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}"], - "workspaces": { - "packages": ["packages/*", "apps/*"] - }, - "duplicates": { - "minOccurrences": 3, - "ignore": ["**/scripts/**", "**.test.ts", "licenses/**"] - }, - "rules": {} -} diff --git a/.forgejo/workflows/cleanup.yml b/.forgejo/workflows/cleanup.yml index a8de85f..48305c8 100644 --- a/.forgejo/workflows/cleanup.yml +++ b/.forgejo/workflows/cleanup.yml @@ -31,18 +31,16 @@ jobs: "$API/repos/$REPO/actions/runs?page=$PAGE&limit=50" \ -o runs.json - COUNT="$(jq 'if type == "array" then length else (.workflow_runs // []) | length end' runs.json)" + COUNT="$(jq '.workflow_runs | length' runs.json)" test "$COUNT" -gt 0 || break jq -r \ --arg cutoff "$CUTOFF" \ --arg current "$CURRENT_RUN_ID" \ - 'if type == "array" then . else (.workflow_runs // []) end - | .[] - | select(type == "object") + '.workflow_runs[] | select((.id | tostring) != $current) - | select((.status // .conclusion) as $status | ["success", "failure", "cancelled", "skipped", "succeeded", "failed"] | index($status)) - | select((.created // .created_at) < $cutoff) + | select(["success", "failure", "cancelled", "skipped", "succeeded", "failed"] | index(.status)) + | select(.created < $cutoff) | .id' runs.json \ >> "$DELETE_IDS" diff --git a/.gitignore b/.gitignore index 87d0fc3..a63f278 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ node_modules releases -.fallow/ 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/bun.lock b/bun.lock index 02db475..f3f63a0 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,6 @@ "@typescript-eslint/parser": "^8.59.1", "eslint": "^10.2.1", "eslint-plugin-react-hooks": "^7.1.1", - "fallow": "^2.86.0", "globals": "^17.5.0", "jsonc-parser": "^3.3.1", "prettier": "^3.8.3", @@ -466,22 +465,6 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="], - "@fallow-cli/darwin-arm64": ["@fallow-cli/darwin-arm64@2.86.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MOAi1tP+pho98AUEgmA5ziWSDsbr0KWQXNuk9GrptYDBsH0H+Jf9qdW8m3ilJXcgL5+RO8RTH/5Nfkhq0uoMYA=="], - - "@fallow-cli/darwin-x64": ["@fallow-cli/darwin-x64@2.86.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mzvGhbZZ8KSG2ZEKnVpiL3Vp9lX4fep2HI8eHt3VDuhMkz8t5n3WRBaBS7LLGTCfLDZ9QPDWhSNCTkx/Llhxw=="], - - "@fallow-cli/linux-arm64-gnu": ["@fallow-cli/linux-arm64-gnu@2.86.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-1N/GCf48xp0Rh9T1JrBvKXAwn1V/+UdCsc2QgF5IrUnwD1JjTG1+/mMg5XwKmrJNPfznJzgzyYGlyfdBeJlqwA=="], - - "@fallow-cli/linux-arm64-musl": ["@fallow-cli/linux-arm64-musl@2.86.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-/H8eK3JpLOq3l5ccEoVVuRssM528vYjk06fy9AcjIvVAGnESErfHQkmGkzAU3nquNL/tlqz88OoBvX+rayDKzQ=="], - - "@fallow-cli/linux-x64-gnu": ["@fallow-cli/linux-x64-gnu@2.86.0", "", { "os": "linux", "cpu": "x64" }, "sha512-svu6wYoCnb1zjZFGjWGtmf8PMiN3SLBn7HYrkA/hXsFK3VRQLFXzts2dJOJ2kHN18rhCExBCQcqukuc/SOoKiA=="], - - "@fallow-cli/linux-x64-musl": ["@fallow-cli/linux-x64-musl@2.86.0", "", { "os": "linux", "cpu": "x64" }, "sha512-pOr4ka4lUIdExkxG0/5cofETE1QY0DI1hbnGJxfLu3Ui0gr9A0H1Dl+ipo5oMJbMFaUiEMAFRCrtNx/j3tA/fg=="], - - "@fallow-cli/win32-arm64-msvc": ["@fallow-cli/win32-arm64-msvc@2.86.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-GmUU/l3euQkNcp5i6ZaUttjTrg2WEYohRCLMqWOUrY0CNaj/ejZ3bhDC+ZZIJZVuWg7sEFACi8wbXhc0rwVb9g=="], - - "@fallow-cli/win32-x64-msvc": ["@fallow-cli/win32-x64-msvc@2.86.0", "", { "os": "win32", "cpu": "x64" }, "sha512-6c+y+QrhfiRQGiqo0QbZ5uoMxuSFichOVzPbZtagvUSO6LGogyB0qH+vNL2kk74NpD6knSXO8vrebhnsBJeI7Q=="], - "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], @@ -1208,8 +1191,6 @@ "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], - "fallow": ["fallow@2.86.0", "", { "dependencies": { "detect-libc": "2.1.2" }, "optionalDependencies": { "@fallow-cli/darwin-arm64": "2.86.0", "@fallow-cli/darwin-x64": "2.86.0", "@fallow-cli/linux-arm64-gnu": "2.86.0", "@fallow-cli/linux-arm64-musl": "2.86.0", "@fallow-cli/linux-x64-gnu": "2.86.0", "@fallow-cli/linux-x64-musl": "2.86.0", "@fallow-cli/win32-arm64-msvc": "2.86.0", "@fallow-cli/win32-x64-msvc": "2.86.0" }, "bin": { "fallow": "bin/fallow", "fallow-lsp": "bin/fallow-lsp", "fallow-mcp": "bin/fallow-mcp" } }, "sha512-jHSTWCrvU2Llq7M0bDniTR2aqxWOwJO8BsV2IIl4lOfysJ0bd/3APu3qZ5wJZ+FE40t5DKlMEFQeURMpEb0Rig=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], diff --git a/package.json b/package.json index ae8ec19..0c0e2b1 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "scripts": { "format": "bunx prettier --write .", "lint": "bun scripts/lint-packages.ts", - "check": "fallow && bun run lint", "copy-licenses": "bun scripts/copy-licenses.ts", "copy-releases": "bun scripts/copy-releases.ts", "pre-build": "bun run lint && bun run build:packages", @@ -35,12 +34,11 @@ "@typescript-eslint/parser": "^8.59.1", "eslint": "^10.2.1", "eslint-plugin-react-hooks": "^7.1.1", - "fallow": "^2.86.0", "globals": "^17.5.0", - "jsonc-parser": "^3.3.1", "prettier": "^3.8.3", "typescript": "^6.0.3", - "typescript-eslint": "^8.59.1" + "typescript-eslint": "^8.59.1", + "jsonc-parser": "^3.3.1" }, "overrides": { "@tensamin/ui": "https://git.methanium.net/tensamin/ui/archive/0.0.36.tar.gz", 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}