TTP -> MTP, A lot of other stuff #21
3 changed files with 210 additions and 60 deletions
commit
d57940d1b3
|
|
@ -1,15 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import type { RawMessage } from "../values";
|
import type { RawMessage } from "../values";
|
||||||
import Text from "@tensamin/markdown/text";
|
import Text from "@tensamin/markdown/text";
|
||||||
import {
|
import { AlertTriangle, Check, Ellipse, RefreshCw } from "lucide-react";
|
||||||
AlertTriangle,
|
|
||||||
Check,
|
|
||||||
Ellipse,
|
|
||||||
Forward,
|
|
||||||
Laugh,
|
|
||||||
RefreshCw,
|
|
||||||
Reply,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import type { User } from "@tensamin/user/context";
|
import type { User } from "@tensamin/user/context";
|
||||||
|
|
||||||
|
|
@ -18,9 +10,7 @@ import {
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
|
||||||
cn,
|
cn,
|
||||||
Separator,
|
|
||||||
Skeleton,
|
Skeleton,
|
||||||
} from "@tensamin/ui";
|
} from "@tensamin/ui";
|
||||||
import MessageContextMenu from "./messageContextMenu";
|
import MessageContextMenu from "./messageContextMenu";
|
||||||
|
|
@ -205,9 +195,10 @@ function MessageComponent({
|
||||||
{user && message.Content ? (
|
{user && message.Content ? (
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
content={message.Content}
|
content={message.Content}
|
||||||
|
hideMiniMenu={editing}
|
||||||
|
isOwnMessage={message.SenderId === ownId}
|
||||||
messageId={message.SendTime}
|
messageId={message.SendTime}
|
||||||
onSetEditing={setEditing}
|
onSetEditing={setEditing}
|
||||||
senderId={message.SenderId}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -234,26 +225,11 @@ function MessageComponent({
|
||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col group">
|
<div
|
||||||
<Card className="absolute right-4 -top-3 opacity-0 group-hover:opacity-100 flex flex-row justify-end gap-0! z-10 p-0! shadow-lg rounded-lg!">
|
className={cn("flex flex-col group", {
|
||||||
{[0, 1, 2].map((item) => (
|
"min-w-0 flex-1": editing,
|
||||||
<Button key={item} variant="ghost" className="h-8 w-8">
|
})}
|
||||||
{item === 0 && "👍"}
|
>
|
||||||
{item === 1 && "🔥"}
|
|
||||||
{item === 2 && "✅"}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
<Separator orientation="vertical" className="my-1" />
|
|
||||||
<Button variant="ghost" className="h-8 w-8">
|
|
||||||
<Laugh />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" className="h-8 w-8">
|
|
||||||
<Reply />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" className="h-8 w-8">
|
|
||||||
<Forward />
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
{!grouped && (
|
{!grouped && (
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<p className="font-medium">{user.Display}</p>
|
<p className="font-medium">{user.Display}</p>
|
||||||
|
|
@ -283,8 +259,9 @@ function MessageComponent({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{editing ? (
|
{editing ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex w-full flex-col">
|
||||||
<Input
|
<Input
|
||||||
|
className="w-full"
|
||||||
styled
|
styled
|
||||||
setValue={setEditDraft}
|
setValue={setEditDraft}
|
||||||
value={editDraft}
|
value={editDraft}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import {
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
cn,
|
cn,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
ContextMenuContent,
|
ContextMenuContent,
|
||||||
|
|
@ -13,14 +15,26 @@ import {
|
||||||
DrawerContent,
|
DrawerContent,
|
||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
DrawerTrigger,
|
Separator,
|
||||||
useIsMobile,
|
useIsMobile,
|
||||||
} from "@tensamin/ui";
|
} from "@tensamin/ui";
|
||||||
import { Pin, Clipboard, Pen, Reply, Forward, Trash } from "lucide-react";
|
import {
|
||||||
import { useEffect, useMemo, useState } from "react";
|
Clipboard,
|
||||||
import type { ReactElement, ReactNode } from "react";
|
Ellipsis,
|
||||||
|
Forward,
|
||||||
|
Laugh,
|
||||||
|
Pen,
|
||||||
|
Pin,
|
||||||
|
Reply,
|
||||||
|
Trash,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { cloneElement, useMemo, useState, useSyncExternalStore } from "react";
|
||||||
|
import type {
|
||||||
|
MouseEvent as ReactMouseEvent,
|
||||||
|
ReactElement,
|
||||||
|
ReactNode,
|
||||||
|
} from "react";
|
||||||
import { useChat } from "../context";
|
import { useChat } from "../context";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
|
||||||
|
|
||||||
async function copyText(text: string) {
|
async function copyText(text: string) {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
|
|
@ -130,35 +144,138 @@ function ReactionItems({ Item }: { Item: MenuComponents["Item"] }) {
|
||||||
return <Item>Cool item</Item>;
|
return <Item>Cool item</Item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let shiftPressed = false;
|
||||||
|
const shiftListeners = new Set<() => void>();
|
||||||
|
|
||||||
|
function setShiftPressed(value: boolean) {
|
||||||
|
if (shiftPressed === value) return;
|
||||||
|
|
||||||
|
shiftPressed = value;
|
||||||
|
shiftListeners.forEach((listener) => listener());
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribeToShift(listener: () => void) {
|
||||||
|
shiftListeners.add(listener);
|
||||||
|
|
||||||
|
if (shiftListeners.size === 1) {
|
||||||
|
window.addEventListener("keydown", handleShiftKey);
|
||||||
|
window.addEventListener("keyup", handleShiftKey);
|
||||||
|
window.addEventListener("blur", handleWindowBlur);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
shiftListeners.delete(listener);
|
||||||
|
if (shiftListeners.size === 0) {
|
||||||
|
window.removeEventListener("keydown", handleShiftKey);
|
||||||
|
window.removeEventListener("keyup", handleShiftKey);
|
||||||
|
window.removeEventListener("blur", handleWindowBlur);
|
||||||
|
shiftPressed = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleShiftKey(event: KeyboardEvent) {
|
||||||
|
setShiftPressed(event.shiftKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowBlur() {
|
||||||
|
setShiftPressed(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useShiftPressed() {
|
||||||
|
return useSyncExternalStore(
|
||||||
|
subscribeToShift,
|
||||||
|
() => shiftPressed,
|
||||||
|
() => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MiniMessageMenu({
|
||||||
|
isOwnMessage,
|
||||||
|
onDelete,
|
||||||
|
onEdit,
|
||||||
|
onOpenMenu,
|
||||||
|
onReply,
|
||||||
|
shiftIsPressed,
|
||||||
|
}: {
|
||||||
|
isOwnMessage: boolean;
|
||||||
|
onDelete: () => void;
|
||||||
|
onEdit: () => void;
|
||||||
|
onOpenMenu: (event: ReactMouseEvent<HTMLElement>) => void;
|
||||||
|
onReply: () => void;
|
||||||
|
shiftIsPressed: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Card className="absolute right-4 -top-3 opacity-0 group-hover/message-menu:opacity-100 flex flex-row justify-end gap-0! z-10 p-0! shadow-lg rounded-lg!">
|
||||||
|
{[0, 1, 2].map((item) => (
|
||||||
|
<Button key={item} variant="ghost" className="h-8 w-8">
|
||||||
|
{item === 0 && "👍"}
|
||||||
|
{item === 1 && "🔥"}
|
||||||
|
{item === 2 && "✅"}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Separator orientation="vertical" className="my-1" />
|
||||||
|
<Button variant="ghost" className="h-8 w-8">
|
||||||
|
<Laugh />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8"
|
||||||
|
aria-label={isOwnMessage ? "Edit message" : "Reply to message"}
|
||||||
|
onClick={isOwnMessage ? onEdit : onReply}
|
||||||
|
>
|
||||||
|
{isOwnMessage ? <Pen /> : <Reply />}
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" className="h-8 w-8">
|
||||||
|
<Forward />
|
||||||
|
</Button>
|
||||||
|
<Separator orientation="vertical" className="my-1" />
|
||||||
|
{isOwnMessage && shiftIsPressed ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8 text-destructive"
|
||||||
|
aria-label="Delete message"
|
||||||
|
onClick={onDelete}
|
||||||
|
>
|
||||||
|
<Trash />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 w-8"
|
||||||
|
aria-label="Open message menu"
|
||||||
|
onClick={onOpenMenu}
|
||||||
|
>
|
||||||
|
<Ellipsis />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MessageMenuContent({
|
function MessageMenuContent({
|
||||||
components,
|
components,
|
||||||
content,
|
content,
|
||||||
devEnabled,
|
devEnabled,
|
||||||
|
isOwnMessage,
|
||||||
messageId,
|
messageId,
|
||||||
onAddReaction,
|
onAddReaction,
|
||||||
showReactionItems = true,
|
showReactionItems = true,
|
||||||
onSetEditing,
|
onSetEditing,
|
||||||
senderId,
|
|
||||||
}: {
|
}: {
|
||||||
components: MenuComponents;
|
components: MenuComponents;
|
||||||
content: string;
|
content: string;
|
||||||
devEnabled: boolean;
|
devEnabled: boolean;
|
||||||
|
isOwnMessage: boolean;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
onAddReaction?: () => void | Promise<void>;
|
onAddReaction?: () => void | Promise<void>;
|
||||||
showReactionItems?: boolean;
|
showReactionItems?: boolean;
|
||||||
onSetEditing: (value: boolean) => void;
|
onSetEditing: (value: boolean) => void;
|
||||||
senderId: number;
|
|
||||||
}) {
|
}) {
|
||||||
const { Content, Group, Item, Separator, Sub, SubContent, SubTrigger } =
|
const { Content, Group, Item, Separator, Sub, SubContent, SubTrigger } =
|
||||||
components;
|
components;
|
||||||
|
|
||||||
const { load } = useStorage();
|
const { deleteMessage, setReplyTo } = useChat();
|
||||||
const { setReplyTo } = useChat();
|
|
||||||
|
|
||||||
const [ownId, setOwnId] = useState(0);
|
|
||||||
useEffect(() => {
|
|
||||||
load("user_id").then(setOwnId);
|
|
||||||
}, [load]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Content>
|
<Content>
|
||||||
|
|
@ -183,7 +300,7 @@ function MessageMenuContent({
|
||||||
</Group>
|
</Group>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Group>
|
<Group>
|
||||||
{senderId === ownId && (
|
{isOwnMessage && (
|
||||||
<Item
|
<Item
|
||||||
className="flex justify-between"
|
className="flex justify-between"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|
@ -205,12 +322,20 @@ function MessageMenuContent({
|
||||||
<p>Forward</p> <Forward />
|
<p>Forward</p> <Forward />
|
||||||
</Item>
|
</Item>
|
||||||
</Group>
|
</Group>
|
||||||
<Separator />
|
{isOwnMessage && (
|
||||||
<Group>
|
<>
|
||||||
<Item disabled variant="destructive" className="flex justify-between">
|
<Separator />
|
||||||
<p>Delete Message</p> <Trash />
|
<Group>
|
||||||
</Item>
|
<Item
|
||||||
</Group>
|
variant="destructive"
|
||||||
|
className="flex justify-between"
|
||||||
|
onClick={() => deleteMessage(messageId)}
|
||||||
|
>
|
||||||
|
<p>Delete Message</p> <Trash />
|
||||||
|
</Item>
|
||||||
|
</Group>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{devEnabled && (
|
{devEnabled && (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
@ -231,17 +356,21 @@ function MessageMenuContent({
|
||||||
export default function MessageContextMenu({
|
export default function MessageContextMenu({
|
||||||
children,
|
children,
|
||||||
content,
|
content,
|
||||||
|
hideMiniMenu = false,
|
||||||
|
isOwnMessage,
|
||||||
messageId,
|
messageId,
|
||||||
onSetEditing,
|
onSetEditing,
|
||||||
senderId,
|
|
||||||
}: {
|
}: {
|
||||||
children: ReactElement;
|
children: ReactElement;
|
||||||
content: string;
|
content: string;
|
||||||
|
hideMiniMenu?: boolean;
|
||||||
|
isOwnMessage: boolean;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
onSetEditing: (value: boolean) => void;
|
onSetEditing: (value: boolean) => void;
|
||||||
senderId: number;
|
|
||||||
}) {
|
}) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
const shiftIsPressed = useShiftPressed();
|
||||||
|
const { deleteMessage, setReplyTo } = useChat();
|
||||||
const devEnabled = useMemo(
|
const devEnabled = useMemo(
|
||||||
() => Number(localStorage.getItem("log_level")) >= 3,
|
() => Number(localStorage.getItem("log_level")) >= 3,
|
||||||
[],
|
[],
|
||||||
|
|
@ -267,20 +396,50 @@ export default function MessageContextMenu({
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function renderMessage(
|
||||||
|
openMenu: (event: ReactMouseEvent<HTMLElement>) => void,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="group/message-menu relative w-full">
|
||||||
|
{children}
|
||||||
|
{!hideMiniMenu && (
|
||||||
|
<MiniMessageMenu
|
||||||
|
isOwnMessage={isOwnMessage}
|
||||||
|
onDelete={() => deleteMessage(messageId)}
|
||||||
|
onEdit={() => onSetEditing(true)}
|
||||||
|
onOpenMenu={openMenu}
|
||||||
|
onReply={() => setReplyTo(messageId)}
|
||||||
|
shiftIsPressed={shiftIsPressed}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
const child = renderMessage(() => setMainDrawerOpen(true));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Drawer open={mainDrawerOpen} onOpenChange={setMainDrawerOpen}>
|
<Drawer open={mainDrawerOpen} onOpenChange={setMainDrawerOpen}>
|
||||||
<DrawerTrigger asChild>{children}</DrawerTrigger>
|
{cloneElement(child, {
|
||||||
|
onContextMenu: (event: ReactMouseEvent) => {
|
||||||
|
child.props.onContextMenu?.(event);
|
||||||
|
if (event.defaultPrevented) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
setMainDrawerOpen(true);
|
||||||
|
},
|
||||||
|
})}
|
||||||
<MessageMenuContent
|
<MessageMenuContent
|
||||||
components={mainDrawerComponents}
|
components={mainDrawerComponents}
|
||||||
content={content}
|
content={content}
|
||||||
devEnabled={devEnabled}
|
devEnabled={devEnabled}
|
||||||
|
isOwnMessage={isOwnMessage}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
onAddReaction={() => setReactionDrawerOpen(true)}
|
onAddReaction={() => setReactionDrawerOpen(true)}
|
||||||
showReactionItems={false}
|
showReactionItems={false}
|
||||||
onSetEditing={onSetEditing}
|
onSetEditing={onSetEditing}
|
||||||
senderId={senderId}
|
|
||||||
/>
|
/>
|
||||||
</Drawer>
|
</Drawer>
|
||||||
<Drawer open={reactionDrawerOpen} onOpenChange={setReactionDrawerOpen}>
|
<Drawer open={reactionDrawerOpen} onOpenChange={setReactionDrawerOpen}>
|
||||||
|
|
@ -298,16 +457,29 @@ export default function MessageContextMenu({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const child = renderMessage((event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
event.currentTarget.dispatchEvent(
|
||||||
|
new MouseEvent("contextmenu", {
|
||||||
|
bubbles: true,
|
||||||
|
button: 2,
|
||||||
|
clientX: event.clientX,
|
||||||
|
clientY: event.clientY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger render={children} />
|
<ContextMenuTrigger render={child} />
|
||||||
<MessageMenuContent
|
<MessageMenuContent
|
||||||
components={desktopMenuComponents}
|
components={desktopMenuComponents}
|
||||||
content={content}
|
content={content}
|
||||||
devEnabled={devEnabled}
|
devEnabled={devEnabled}
|
||||||
|
isOwnMessage={isOwnMessage}
|
||||||
messageId={messageId}
|
messageId={messageId}
|
||||||
onSetEditing={onSetEditing}
|
onSetEditing={onSetEditing}
|
||||||
senderId={senderId}
|
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ type_maps:
|
||||||
MessageChunk: 61
|
MessageChunk: 61
|
||||||
MessageGet: 143
|
MessageGet: 143
|
||||||
MessagesGet: 62
|
MessagesGet: 62
|
||||||
|
MessageDelete: 149
|
||||||
PushNotification: 63
|
PushNotification: 63
|
||||||
ReadNotification: 64
|
ReadNotification: 64
|
||||||
GetNotifications: 65
|
GetNotifications: 65
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue