mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
an attempt to redo embeds
This commit is contained in:
parent
dec2c9d486
commit
ad9dbb5f93
@ -29,6 +29,7 @@
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"classnames": "^2.3.2",
|
||||
"dayjs": "^1.11.9",
|
||||
"framer-motion": "^10.16.4",
|
||||
"missing-native-js-functions": "^1.4.3",
|
||||
@ -54,7 +55,8 @@
|
||||
"remark-gfm": "^3.0.1",
|
||||
"reoverlay": "^1.0.3",
|
||||
"styled-components": "^5.3.10",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"use-resize-observer": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
|
@ -80,6 +80,9 @@ dependencies:
|
||||
'@testing-library/user-event':
|
||||
specifier: ^13.5.0
|
||||
version: 13.5.0(@testing-library/dom@9.3.1)
|
||||
classnames:
|
||||
specifier: ^2.3.2
|
||||
version: 2.3.2
|
||||
dayjs:
|
||||
specifier: ^1.11.9
|
||||
version: 1.11.9
|
||||
@ -158,6 +161,9 @@ dependencies:
|
||||
unist-util-visit:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0
|
||||
use-resize-observer:
|
||||
specifier: ^9.1.0
|
||||
version: 9.1.0(react-dom@18.2.0)(react@18.2.0)
|
||||
|
||||
devDependencies:
|
||||
'@craco/craco':
|
||||
@ -2785,6 +2791,10 @@ packages:
|
||||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@juggle/resize-observer@3.4.0:
|
||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||
dev: false
|
||||
|
||||
/@leichtgewicht/ip-codec@2.0.4:
|
||||
resolution: {integrity: sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==}
|
||||
dev: true
|
||||
@ -12961,6 +12971,17 @@ packages:
|
||||
requires-port: 1.0.0
|
||||
dev: true
|
||||
|
||||
/use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
|
||||
peerDependencies:
|
||||
react: 16.8.0 - 18
|
||||
react-dom: 16.8.0 - 18
|
||||
dependencies:
|
||||
'@juggle/resize-observer': 3.4.0
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
dev: true
|
||||
|
113
src/components/messaging/Embed.module.css
Normal file
113
src/components/messaging/Embed.module.css
Normal file
@ -0,0 +1,113 @@
|
||||
.embed {
|
||||
margin: 0.2em 0;
|
||||
|
||||
iframe {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.image {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.website {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: row;
|
||||
|
||||
> div:nth-child(1) {
|
||||
gap: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
border-inline-start-width: 4px;
|
||||
border-inline-start-style: solid;
|
||||
|
||||
padding: 12px;
|
||||
width: fit-content;
|
||||
background: var(--background-secondary);
|
||||
border-radius: 4px;
|
||||
|
||||
.embedProvider {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
font-weight: var(--font-weight-regular);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.embedAuthor {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.embedAuthorIcon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
margin-right: 10px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.embedAuthorName {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text);
|
||||
display: inline-block;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.embedAuthorNameLink {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.embedTitle {
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.embedTitleLink {
|
||||
color: var(--text-link);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.embedDescription {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
white-space: pre-wrap;
|
||||
-webkit-line-clamp: 6;
|
||||
-webkit-box-orient: vertical;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
img.image {
|
||||
cursor: pointer;
|
||||
object-fit: fill;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
138
src/components/messaging/EmbedMedia.tsx
Normal file
138
src/components/messaging/EmbedMedia.tsx
Normal file
@ -0,0 +1,138 @@
|
||||
// adapted from Revite
|
||||
// https://github.com/revoltchat/revite/blob/master/src/components/common/messaging/embed/Embed.tsx
|
||||
|
||||
import { APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import styles from "./Embed.module.css";
|
||||
|
||||
interface Props {
|
||||
embed: APIEmbed;
|
||||
width?: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function EmbedMedia({ embed, width, height }: Props) {
|
||||
switch (embed.provider?.name) {
|
||||
case "YouTube": {
|
||||
if (!embed.video?.url) return null;
|
||||
const url = embed.video.url;
|
||||
|
||||
return <iframe loading="lazy" src={url} allowFullScreen style={{ height }} />;
|
||||
}
|
||||
case "Spotify": {
|
||||
const url = embed.url;
|
||||
if (!url) break;
|
||||
// extract type and id from url
|
||||
const match = url.match(/https:\/\/open\.spotify\.com\/(track|album|playlist)\/([a-zA-Z0-9]+)/);
|
||||
if (!match) break;
|
||||
const type = match[1];
|
||||
const id = match[2];
|
||||
|
||||
return (
|
||||
<iframe
|
||||
style={{ borderRadius: "12px", width: "400px", height: "80px" }}
|
||||
src={`https://open.spotify.com/embed/${type}/${id}`}
|
||||
frameBorder="0"
|
||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
);
|
||||
}
|
||||
case "Soundcloud":
|
||||
return (
|
||||
<iframe
|
||||
src={`https://w.soundcloud.com/player/?url=${encodeURIComponent(
|
||||
embed.url!,
|
||||
)}&color=%23FF7F50&auto_play=false&hide_related=false&show_comments=true&show_user=true&show_reposts=false&show_teaser=true&visual=true`}
|
||||
frameBorder="0"
|
||||
scrolling="no"
|
||||
loading="lazy"
|
||||
style={{ height }}
|
||||
/>
|
||||
);
|
||||
// not supported by the server
|
||||
// case "Bandcamp": {
|
||||
// const url = embed.url;
|
||||
// if (!url) break;
|
||||
// // extract type and id from url
|
||||
// const match = url.match(/https:\/\/([a-zA-Z0-9-]+)\.bandcamp\.com\/(track|album|playlist)\/([a-zA-Z0-9]+)/);
|
||||
// if (!match) break;
|
||||
// const type = match[2];
|
||||
// const id = match[3];
|
||||
|
||||
// return (
|
||||
// <iframe
|
||||
// src={`https://bandcamp.com/EmbeddedPlayer/${type.toLowerCase()}=${id}/size=large/bgcol=181a1b/linkcol=056cc4/tracklist=false/transparent=true/`}
|
||||
// seamless
|
||||
// loading="lazy"
|
||||
// style={{ border: "0", height: "42px" }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
case "Streamable": {
|
||||
const url = embed.url;
|
||||
if (!url) break;
|
||||
// extract id from url
|
||||
const match = url.match(/https:\/\/streamable\.com\/([a-zA-Z0-9]+)/);
|
||||
if (!match) break;
|
||||
const id = match[1];
|
||||
return (
|
||||
<iframe
|
||||
src={`https://streamable.com/e/${id}?quality=highest`}
|
||||
frameBorder="0"
|
||||
allowFullScreen
|
||||
seamless
|
||||
loading="lazy"
|
||||
style={{ height }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
if (embed.video) {
|
||||
const url = embed.video.url;
|
||||
return (
|
||||
<video
|
||||
className={styles.image}
|
||||
style={{ width, height }}
|
||||
src={url}
|
||||
loop={embed.type === EmbedType.GIFV}
|
||||
controls={embed.type === EmbedType.GIFV}
|
||||
autoPlay={embed.type === EmbedType.GIFV}
|
||||
muted={embed.type === EmbedType.GIFV ? true : undefined}
|
||||
/>
|
||||
);
|
||||
} else if (embed.image) {
|
||||
const url = embed.image.url;
|
||||
return (
|
||||
<img
|
||||
className={styles.image}
|
||||
src={url}
|
||||
loading="lazy"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
onClick={() => {
|
||||
console.log("preview image");
|
||||
}}
|
||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||
/>
|
||||
);
|
||||
} else if (embed.thumbnail) {
|
||||
const url = embed.thumbnail.url;
|
||||
return (
|
||||
<img
|
||||
className={styles.image}
|
||||
src={url}
|
||||
loading="lazy"
|
||||
style={{ width, height }}
|
||||
onClick={() => {
|
||||
console.log("preview image");
|
||||
}}
|
||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default EmbedMedia;
|
@ -7,6 +7,7 @@ import Markdown from "../markdown/MarkdownRenderer";
|
||||
import MessageAttachment from "./MessageAttachment";
|
||||
import MessageAuthor from "./MessageAuthor";
|
||||
import MessageBase, { MessageContent, MessageDetails, MessageInfo } from "./MessageBase";
|
||||
import MessageEmbed from "./MessageEmbed";
|
||||
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
|
||||
|
||||
interface Props {
|
||||
@ -40,9 +41,8 @@ function Message({ message, header }: Props) {
|
||||
message.attachments.map((attachment, index) => (
|
||||
<MessageAttachment key={index} attachment={attachment} />
|
||||
))}
|
||||
{/* {message.embeds?.map((embed, index) => (
|
||||
<MessageEmbed key={index} embed={embed} />
|
||||
))} */}
|
||||
{"embeds" in message &&
|
||||
message.embeds?.map((embed, index) => <MessageEmbed key={index} embed={embed} />)}
|
||||
{"files" in message && message.files?.length !== 0 && <AttachmentUploadProgress message={message} />}
|
||||
</MessageContent>
|
||||
</MessageBase>
|
||||
|
@ -1,265 +1,150 @@
|
||||
import { APIAttachment, APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { ReactNode } from "react";
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
// adapted from Revite
|
||||
// https://github.com/revoltchat/revite/blob/master/src/components/common/messaging/embed/Embed.tsx
|
||||
|
||||
import { APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import classNames from "classnames";
|
||||
import React from "react";
|
||||
import { decimalColorToHex } from "../../utils/Utils";
|
||||
import { IContextMenuItem } from "../ContextMenuItem";
|
||||
import Link from "../Link";
|
||||
import MessageAttachment from "./MessageAttachment";
|
||||
import styles from "./Embed.module.css";
|
||||
import EmbedMedia from "./EmbedMedia";
|
||||
import { MESSAGE_AREA_PADDING, MessageAreaWidthContext } from "./MessageList";
|
||||
|
||||
// TODO: move these to a constants file/configurable
|
||||
const DESCRIPTION_MAX_CHARS = 345;
|
||||
const TITLE_MAX_CHARS = 67;
|
||||
const MAX_EMBED_WIDTH = 400;
|
||||
const MAX_EMBED_HEIGHT = 640;
|
||||
const CONTAINER_PADDING = 24;
|
||||
const MAX_PREVIEW_SIZE = 150;
|
||||
const EMBEDDABLE_PROVIDERS = ["Spotify", "Bandcamp"];
|
||||
|
||||
interface EmbedProps {
|
||||
interface Props {
|
||||
embed: APIEmbed;
|
||||
contextMenuItems: IContextMenuItem[];
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: fit-content;
|
||||
background-color: var(--background-secondary);
|
||||
margin-top: 5px;
|
||||
`;
|
||||
function MessageEmbed({ embed }: Props) {
|
||||
const maxWidth = Math.min(React.useContext(MessageAreaWidthContext) - MESSAGE_AREA_PADDING, MAX_EMBED_WIDTH);
|
||||
|
||||
const Wrapper = styled.div<{ $color?: string }>`
|
||||
max-width: 430px;
|
||||
justify-self: start;
|
||||
border-left-width: 4px;
|
||||
border-left-style: solid;
|
||||
border-left-color: ${(props) => props.$color ?? "var(--background-tertiary)"};
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
function calculateSize(w: number, h: number): { width: number; height: number } {
|
||||
const limitingWidth = Math.min(maxWidth, w);
|
||||
|
||||
const EmbedWrapper = styled.div`
|
||||
max-width: 500px;
|
||||
overflow: hidden;
|
||||
padding: 8px 16px 16px 12px;
|
||||
display: grid;
|
||||
grid-template-columns: auto;
|
||||
grid-template-rows: auto;
|
||||
`;
|
||||
const limitingHeight = Math.min(MAX_EMBED_HEIGHT, h);
|
||||
|
||||
const EmbedProvider = styled.div`
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
grid-column: 1/1;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
// Calculate smallest possible WxH.
|
||||
const width = Math.min(limitingWidth, limitingHeight * (w / h));
|
||||
|
||||
const EmbedAuthor = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 1/1;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
const height = Math.min(limitingHeight, limitingWidth * (h / w));
|
||||
|
||||
const EmbedAuthorText = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
`;
|
||||
|
||||
const EmbedAuthorLink = styled.a`
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
return { width, height };
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbedTitle = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
grid-column: 1/1;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
// Determine special embed size.
|
||||
let mw, mh;
|
||||
const largeMedia =
|
||||
embed.provider?.name === "GitHub" ||
|
||||
embed.provider?.name === "Streamable" ||
|
||||
embed.type === EmbedType.Video ||
|
||||
embed.type === EmbedType.GIFV ||
|
||||
embed.type === EmbedType.Image;
|
||||
|
||||
const EmbedTitleLink = styled.a`
|
||||
color: var(--text-link);
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
if (embed.image) {
|
||||
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
|
||||
mh = embed.image?.height ?? 0;
|
||||
} else if (embed.thumbnail) {
|
||||
mw = embed.thumbnail.width ?? MAX_EMBED_WIDTH;
|
||||
mh = embed.thumbnail.height ?? 0;
|
||||
} else {
|
||||
switch (embed.provider?.name) {
|
||||
case "YouTube":
|
||||
case "Bandcamp": {
|
||||
mw = embed.video?.width ?? 1280;
|
||||
mh = embed.video?.height ?? 720;
|
||||
break;
|
||||
}
|
||||
case "Twitch":
|
||||
case "Lightspeed":
|
||||
case "Streamable": {
|
||||
mw = 1280;
|
||||
mh = 720;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
mw = MAX_EMBED_WIDTH;
|
||||
mh = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbedDescription = styled.div`
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
grid-column: 1/1;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const EmbedImage = styled.div`
|
||||
margin-top: 10px;
|
||||
grid-column: 1/1;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const EmbedImageContainer = styled.div`
|
||||
flex-flow: row nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const EmbedImageWrapper = styled.div`
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const EmbedThumbnail = styled.div`
|
||||
grid-row: 1/8;
|
||||
grid-column: 2/2;
|
||||
margin-left: 15px;
|
||||
margin-top: 10px;
|
||||
justify-self: end;
|
||||
`;
|
||||
|
||||
const EmbedFooter = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-row: auto/auto;
|
||||
grid-column: 1/1;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const EmbedFooterImage = styled.img`
|
||||
margin-right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
`;
|
||||
|
||||
const EmbedFooterText = styled.span`
|
||||
font-size: 12px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
`;
|
||||
|
||||
const YoutubeEmbed = styled.iframe`
|
||||
outline: none;
|
||||
border: none;
|
||||
margin-top: 10px;
|
||||
border-radius: 4px;
|
||||
`;
|
||||
|
||||
const WrapImageContent = ({ children }: { children: ReactNode }) => {
|
||||
const { width, height } = calculateSize(mw, mh);
|
||||
if (embed.type === EmbedType.GIFV || EMBEDDABLE_PROVIDERS.includes(embed.provider?.name ?? "")) {
|
||||
return (
|
||||
<EmbedImageContainer>
|
||||
<EmbedImageWrapper>{children}</EmbedImageWrapper>
|
||||
</EmbedImageContainer>
|
||||
<EmbedMedia
|
||||
embed={embed}
|
||||
width={height * ((embed.image?.width ?? 0) / (embed.image?.height ?? 0))}
|
||||
height={height}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const createEmbedAttachment = (embed: APIEmbed, contextMenuItems: IContextMenuItem[], isYoutubeVideo = false) => {
|
||||
const image = embed.thumbnail ?? embed.image;
|
||||
if (!image) return null;
|
||||
|
||||
const url = new URL(embed.url!);
|
||||
|
||||
const fakeAttachment: APIAttachment = {
|
||||
id: embed.url as string,
|
||||
filename: url.pathname.split("/").reverse()[0],
|
||||
size: -1,
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
proxy_url: image.proxy_url!,
|
||||
url: image.url,
|
||||
content_type: "image",
|
||||
};
|
||||
|
||||
const props = {
|
||||
contextMenuItems,
|
||||
attachment: fakeAttachment,
|
||||
};
|
||||
|
||||
if (isYoutubeVideo) return createYoutubeEmbed(embed);
|
||||
|
||||
if (embed.type === EmbedType.Link)
|
||||
return (
|
||||
<EmbedThumbnail>
|
||||
<WrapImageContent>
|
||||
<MessageAttachment {...props} maxWidth={70} />
|
||||
</WrapImageContent>
|
||||
</EmbedThumbnail>
|
||||
);
|
||||
return (
|
||||
<EmbedImage>
|
||||
<WrapImageContent>
|
||||
<MessageAttachment {...props} />
|
||||
</WrapImageContent>
|
||||
</EmbedImage>
|
||||
);
|
||||
};
|
||||
|
||||
const createYoutubeEmbed = (embed: APIEmbed) => {
|
||||
return <YoutubeEmbed width={400} height={225} src={embed.video!.url} />;
|
||||
};
|
||||
|
||||
export default function MessageEmbed({ embed, contextMenuItems }: EmbedProps) {
|
||||
const logger = useLogger("MessageEmbed");
|
||||
|
||||
// seems like the server sometimes sends thumbnails with 0 width and height, and no urls
|
||||
const isYoutubeVideo = embed.type == EmbedType.Video && embed.provider?.name == "YouTube";
|
||||
const thumbnail = createEmbedAttachment(embed, contextMenuItems, isYoutubeVideo);
|
||||
|
||||
if (embed.type == EmbedType.Image) return thumbnail;
|
||||
|
||||
const titleTrimmed = embed.title
|
||||
? embed.title?.length > TITLE_MAX_CHARS
|
||||
? embed.title.substring(0, TITLE_MAX_CHARS) + "..."
|
||||
: embed.title
|
||||
: undefined;
|
||||
|
||||
const descriptionTrimmed = embed.description
|
||||
? embed.description.length > DESCRIPTION_MAX_CHARS
|
||||
? embed.description?.substring(0, DESCRIPTION_MAX_CHARS) + "..."
|
||||
: embed.description
|
||||
: undefined;
|
||||
|
||||
let title;
|
||||
if (titleTrimmed) {
|
||||
if (embed.url)
|
||||
title = (
|
||||
<Link href={embed.url} rel="noreferrer noopener" target="_blank">
|
||||
{titleTrimmed}
|
||||
</Link>
|
||||
);
|
||||
else title = titleTrimmed;
|
||||
} else title = null;
|
||||
|
||||
let author;
|
||||
if (embed.author)
|
||||
if (embed.author.url)
|
||||
author = (
|
||||
<Link href={embed.author.url} rel="noreferrer noopener" target="_blank">
|
||||
{embed.author.name}
|
||||
</Link>
|
||||
);
|
||||
else author = <EmbedAuthorText>{embed.author.name}</EmbedAuthorText>;
|
||||
else null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Wrapper $color={embed.color ? decimalColorToHex(embed.color) : undefined}>
|
||||
<EmbedWrapper>
|
||||
{embed.provider && <EmbedProvider>{embed.provider.name}</EmbedProvider>}
|
||||
{author && <EmbedAuthor>{author}</EmbedAuthor>}
|
||||
{title && <EmbedTitle>{title}</EmbedTitle>}
|
||||
{descriptionTrimmed && !isYoutubeVideo && <EmbedDescription>{descriptionTrimmed}</EmbedDescription>}
|
||||
{thumbnail}
|
||||
{embed.footer && (
|
||||
<EmbedFooter>
|
||||
{embed.footer.icon_url && <EmbedFooterImage src={embed.footer.icon_url} />}
|
||||
<EmbedFooterText>{embed.footer.text}</EmbedFooterText>
|
||||
</EmbedFooter>
|
||||
<div
|
||||
className={classNames(styles.embed, styles.website)}
|
||||
style={{
|
||||
borderInlineStartColor: embed.color ? decimalColorToHex(embed.color) : "var(--background-tertiary)",
|
||||
maxWidth: width + CONTAINER_PADDING,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{embed.type !== EmbedType.Rich && embed.provider && (
|
||||
<span className={styles.embedProvider}>{embed.provider.name}</span>
|
||||
)}
|
||||
</EmbedWrapper>
|
||||
</Wrapper>
|
||||
</Container>
|
||||
|
||||
{embed.author && (
|
||||
<div className={styles.embedAuthor}>
|
||||
{embed.author.icon_url && (
|
||||
<img
|
||||
loading="lazy"
|
||||
className={styles.embedAuthorIcon}
|
||||
src={embed.author.icon_url}
|
||||
draggable={false}
|
||||
onError={(e) => (e.currentTarget.style.display = "none")}
|
||||
/>
|
||||
)}
|
||||
{embed.author.url ? (
|
||||
<a
|
||||
href={embed.url}
|
||||
target={"_blank"}
|
||||
className={classNames(styles.embedAuthorName, styles.embedAuthorNameLink)}
|
||||
>
|
||||
{embed.author.name}
|
||||
</a>
|
||||
) : (
|
||||
<span className={styles.embedAuthorName}>{embed.author.name}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{embed.title && (
|
||||
<>
|
||||
{embed.url ? (
|
||||
<a
|
||||
href={embed.url}
|
||||
target={"_blank"}
|
||||
className={classNames(styles.embedTitle, styles.embedTitleLink)}
|
||||
>
|
||||
{embed.title}
|
||||
</a>
|
||||
) : (
|
||||
<span className={styles.embedTitle}>{embed.title}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{embed.description && <div className={styles.embedDescription}>{embed.description}</div>}
|
||||
|
||||
{largeMedia && <EmbedMedia embed={embed} height={height} />}
|
||||
</div>
|
||||
|
||||
{!largeMedia && embed.thumbnail && <EmbedMedia embed={embed} height={100} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageEmbed;
|
||||
|
@ -3,6 +3,7 @@ import React from "react";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import PulseLoader from "react-spinners/PulseLoader";
|
||||
import styled from "styled-components";
|
||||
import useResizeObserver from "use-resize-observer";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { MessageGroup as MessageGroupType } from "../../stores/MessageStore";
|
||||
@ -12,6 +13,9 @@ import { Permissions } from "../../utils/Permissions";
|
||||
import { HorizontalDivider } from "../Divider";
|
||||
import MessageGroup from "./MessageGroup";
|
||||
|
||||
export const MessageAreaWidthContext = React.createContext(0);
|
||||
export const MESSAGE_AREA_PADDING = 82;
|
||||
|
||||
const Container = styled.div`
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
@ -37,6 +41,8 @@ function MessageList({ guild, channel }: Props) {
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
const [canView, setCanView] = React.useState(false);
|
||||
const messageGroups = channel.messages.groups;
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const { width } = useResizeObserver<HTMLDivElement>({ ref });
|
||||
|
||||
// handles the permission check
|
||||
React.useEffect(() => {
|
||||
@ -82,7 +88,8 @@ function MessageList({ guild, channel }: Props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Container id="scrollable-div">
|
||||
<MessageAreaWidthContext.Provider value={(width ?? 0) - MESSAGE_AREA_PADDING}>
|
||||
<Container id="scrollable-div" ref={ref}>
|
||||
{canView ? (
|
||||
<InfiniteScroll
|
||||
dataLength={messageGroups.length}
|
||||
@ -130,6 +137,7 @@ function MessageList({ guild, channel }: Props) {
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</MessageAreaWidthContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user