Skip to content

Commit

Permalink
Merge pull request #155 from spacebarchat/feat/embeds
Browse files Browse the repository at this point in the history
Implement Embeds
  • Loading branch information
Puyodead1 committed Sep 4, 2023
2 parents c1c0ce9 + 6473732 commit 03e3e11
Show file tree
Hide file tree
Showing 5 changed files with 374 additions and 70 deletions.
87 changes: 18 additions & 69 deletions src/components/messaging/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { APIAttachment, MessageType } from "@spacebarchat/spacebar-api-types/v9";
import { APIAttachment, APIEmbed, MessageType } from "@spacebarchat/spacebar-api-types/v9";
import { observer } from "mobx-react-lite";
import React from "react";
import Moment from "react-moment";
import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { default as MessageObject } from "../../stores/objects/Message";
import QueuedMessage from "../../stores/objects/QueuedMessage";
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
import { calendarStrings } from "../../utils/i18n";
import Avatar from "../Avatar";
import { Link } from "../Link";
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";
import { IContextMenuItem } from "./../ContextMenuItem";
import AttachmentUploadProgress from "./AttachmentUploadProgress";
import MessageAttachment from "./MessageAttachment";
import MessageEmbed from "./MessageEmbed";

type MessageLike = MessageObject | QueuedMessage;

Expand Down Expand Up @@ -65,10 +64,6 @@ const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
color: ${(props) => (props.failed ? "var(--error)" : undefined)};
`;

const MessageAttachment = styled.div`
cursor: pointer;
`;

// converts URLs in a string to html links
const Linkify = ({ children }: { children: string }) => {
const urlPattern = /\bhttps?:\/\/\S+\b\/?/g;
Expand Down Expand Up @@ -106,8 +101,8 @@ interface Props {
* Component for rendering a single message
*/
function Message({ message, isHeader, isSending, isFailed }: Props) {
const logger = useLogger("Message.tsx");
const { openModal } = useModals();
const app = useAppStore();

const contextMenu = React.useContext(ContextMenuContext);
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
{
Expand All @@ -121,63 +116,15 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
},
]);

const renderAttachment = React.useCallback((attachment: APIAttachment) => {
let a: JSX.Element = <></>;
if (attachment.content_type?.startsWith("image")) {
const ratio = calculateImageRatio(attachment.width!, attachment.height!);
const { scaledWidth, scaledHeight } = calculateScaledDimensions(
attachment.width!,
attachment.height!,
ratio,
);
a = <img src={attachment.url} alt={attachment.filename} width={scaledWidth} height={scaledHeight} />;
} else if (attachment.content_type?.startsWith("video")) {
{
/* TODO: poster thumbnail */
}
a = (
<video controls preload="metadata" width={400}>
{/* TODO: the server doesn't return height and width yet for videos */}
<source src={attachment.url} type={attachment.content_type} />
</video>
);
} else {
logger.warn(`Unknown attachment type: ${attachment.content_type}`);
}
const renderAttachment = React.useCallback(
(attachment: APIAttachment) => {
return <MessageAttachment attachment={attachment} contextMenuItems={contextMenuItems} />;
},
[contextMenuItems],
);

return (
<MessageAttachment
key={attachment.id}
onContextMenu={(e) => {
// prevent propagation to the message container
e.stopPropagation();
e.preventDefault();
contextMenu.open({
position: {
x: e.pageX,
y: e.pageY,
},
items: [
...contextMenuItems,
{
label: "Copy Attachment URL",
onClick: () => {
navigator.clipboard.writeText(attachment.url);
},
iconProps: {
icon: "mdiLink",
},
} as IContextMenuItem,
],
});
}}
onClick={() => {
openModal(AttachmentPreviewModal, { attachment });
}}
>
{a}
</MessageAttachment>
);
const renderEmbed = React.useCallback((embed: APIEmbed) => {
return <MessageEmbed embed={embed} contextMenuItems={contextMenuItems} />;
}, []);

// construct the context menu options
Expand Down Expand Up @@ -248,8 +195,10 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
{message.type === MessageType.Default ? (
<MessageContent sending={isSending} failed={isFailed}>
{message.content ? <Linkify>{message.content}</Linkify> : null}
{"attachments" in message &&
message.attachments.map((attachment) => renderAttachment(attachment))}
{"attachments" in message
? message.attachments.map((attachment) => renderAttachment(attachment))
: null}
{"embeds" in message ? message.embeds.map((embed) => renderEmbed(embed)) : null}
</MessageContent>
) : (
<div>
Expand Down
95 changes: 95 additions & 0 deletions src/components/messaging/MessageAttachment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useModals } from "@mattjennings/react-modal-stack";
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
import React from "react";
import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import useLogger from "../../hooks/useLogger";
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
import { IContextMenuItem } from "../ContextMenuItem";
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";

const Attachment = styled.div<{ withPointer?: boolean }>`
cursor: ${(props) => (props.withPointer ? "pointer" : "default")};
width: min-content;
`;

const Image = styled.img`
border-radius: 4px;
`;

interface AttachmentProps {
attachment: APIAttachment;
contextMenuItems: IContextMenuItem[];
maxWidth?: number;
maxHeight?: number;
}

export default function MessageAttachment({ attachment, contextMenuItems, maxWidth, maxHeight }: AttachmentProps) {
const logger = useLogger("MessageAttachment");

const { openModal } = useModals();
const contextMenu = React.useContext(ContextMenuContext);

const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url;

let finalElement: JSX.Element = <></>;
if (attachment.content_type?.startsWith("image")) {
const ratio = calculateImageRatio(attachment.width!, attachment.height!, maxWidth, maxHeight);
const { scaledWidth, scaledHeight } = calculateScaledDimensions(
attachment.width!,
attachment.height!,
ratio,
maxWidth,
maxHeight,
);
finalElement = <Image src={url} alt={attachment.filename} width={scaledWidth} height={scaledHeight} />;
} else if (attachment.content_type?.startsWith("video")) {
{
/* TODO: poster thumbnail */
}
finalElement = (
<video playsInline controls preload="metadata" height={200}>
{/* TODO: the server doesn't return height and width yet for videos */}
<source src={url} type={attachment.content_type} />
</video>
);
} else {
logger.warn(`Unknown attachment type: ${attachment.content_type}`);
}

return (
<Attachment
withPointer={attachment.content_type?.startsWith("image")}
key={attachment.id}
onContextMenu={(e) => {
// prevent propagation to the message container
e.stopPropagation();
e.preventDefault();
contextMenu.open({
position: {
x: e.pageX,
y: e.pageY,
},
items: [
...contextMenuItems,
{
label: "Copy Attachment URL",
onClick: () => {
navigator.clipboard.writeText(attachment.url);
},
iconProps: {
icon: "mdiLink",
},
} as IContextMenuItem,
],
});
}}
onClick={() => {
if (!attachment.content_type?.startsWith("image")) return;
openModal(AttachmentPreviewModal, { attachment });
}}
>
{finalElement}
</Attachment>
);
}
Loading

0 comments on commit 03e3e11

Please sign in to comment.