1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-21 18:02:32 +01:00

system messages (UserJoin)

This commit is contained in:
Puyodead1 2023-09-17 16:12:53 -04:00
parent 97fe9f663b
commit 94721c137b
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
9 changed files with 202 additions and 144 deletions

View File

@ -47,6 +47,7 @@
"react-secure-storage": "^1.3.0",
"react-select-search": "^4.1.6",
"react-spinners": "^0.13.8",
"react-string-replace": "^1.1.1",
"reoverlay": "^1.0.3",
"styled-components": "^5.3.10"
},

View File

@ -134,6 +134,9 @@ dependencies:
react-spinners:
specifier: ^0.13.8
version: 0.13.8(react-dom@18.2.0)(react@18.2.0)
react-string-replace:
specifier: ^1.1.1
version: 1.1.1
reoverlay:
specifier: ^1.0.3
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
@ -10729,6 +10732,11 @@ packages:
react-dom: 18.2.0(react@18.2.0)
dev: false
/react-string-replace@1.1.1:
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
engines: {node: '>=0.12.0'}
dev: false
/react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:

View File

@ -1,7 +1,7 @@
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
import dayjs from "dayjs";
import { observer } from "mobx-react-lite";
import React, { Fragment } from "react";
import reactStringReplace from "react-string-replace";
import styled from "styled-components";
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { MessageLike } from "../../stores/objects/Message";
@ -9,7 +9,10 @@ import Avatar from "../Avatar";
import { Link } from "../Link";
import { IContextMenuItem } from "./../ContextMenuItem";
import MessageAttachment from "./MessageAttachment";
import MessageBase from "./MessageBase";
import MessageEmbed from "./MessageEmbed";
import MessageTimestamp from "./MessageTimestamp";
import SystemMessage from "./SystemMessage";
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
const MessageListItem = styled.li`
@ -37,6 +40,7 @@ const MessageHeader = styled.div`
display: flex;
flex: 1;
flex-direction: row;
align-items: center;
`;
const MessageAuthor = styled.div`
@ -44,13 +48,6 @@ const MessageAuthor = styled.div`
font-weight: var(--font-weight-medium);
`;
const MessageTimestamp = styled.div`
font-size: 14px;
font-weight: var(--font-weight-regular);
margin-left: 10px;
color: var(--text-secondary);
`;
const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
font-size: 16px;
font-weight: var(--font-weight-light);
@ -60,31 +57,22 @@ const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
color: ${(props) => (props.failed ? "var(--error)" : undefined)};
`;
// converts URLs in a string to html links
const Linkify = ({ children }: { children: string }) => {
const urlPattern = /\bhttps?:\/\/\S+\b\/?/g;
const matches = children.match(urlPattern);
if (!matches) return <>{children}</>;
const MessageHeaderWrapper = styled.div`
display: flex;
flex-direction: row;
`;
const elements = [];
let lastIndex = 0;
function parseMessageContent(content?: string | null) {
if (!content) return null;
// replace links with Link components
const replacedText = reactStringReplace(content, /(https?:\/\/\S+)/g, (match, i) => (
<Link key={match + i} href={match} target="_blank" rel="noreferrer">
{match}
</Link>
));
for (const match of matches) {
const matchIndex = children.indexOf(match, lastIndex);
if (matchIndex > lastIndex) elements.push(children.substring(lastIndex, matchIndex));
elements.push(
<Link key={matchIndex} href={match} target="_blank" rel="noreferrer">
{match}
</Link>,
);
lastIndex = matchIndex + match.length;
}
if (lastIndex < children.length) elements.push(children.substring(lastIndex));
return <>{elements}</>;
};
return replacedText;
}
interface Props {
message: MessageLike;
@ -110,90 +98,99 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
},
]);
// construct the context menu options
// React.useEffect(() => {
// // if the message is queued, we don't need a context menu
// if (isSending) {
// return;
// }
// // add delete/resend option if the current user is the message author
// // if (author?.id === domain.account?.id) {
// // items.push({
// // label: failed ? 'Resend Message' : 'Delete Message',
// // onPress: () => {
// // // TODO: implement
// // console.debug(
// // failed ? 'should resend message' : 'should delete message',
// // );
// // },
// // color: theme.colors.palette.red40,
// // iconProps: {
// // name: failed ? 'reload' : 'delete',
// // },
// // });
// // }
// // setContextMenuOptions(items);
// }, [isSending, isFailed]);
// handles creating the message content based on the message type
// TODO: probably move this to a separate component
const renderMessageContent = React.useCallback(() => {
switch (message.type) {
case MessageType.Default:
return (
<MessageContent sending={isSending} failed={isFailed}>
{message.content ? <Linkify>{message.content}</Linkify> : null}
{"attachments" in message
? message.attachments.map((attachment, index) => (
<Fragment key={index}>
<MessageAttachment
key={index}
attachment={attachment}
contextMenuItems={contextMenuItems}
/>
</Fragment>
))
: null}
{"embeds" in message
? message.embeds.map((embed, index) => (
<Fragment key={index}>
<MessageEmbed key={index} embed={embed} contextMenuItems={contextMenuItems} />
</Fragment>
))
: null}
</MessageContent>
);
case MessageType.UserJoin: {
// TODO: render only the join message and timestamp, will require a bit of refactoring
const msg = message.getJoinMessage();
const split = msg.split("{author}");
return (
<MessageContent
const withMessageHeader = React.useCallback(
(children: React.ReactNode, showHeader = false) => (
<MessageHeaderWrapper>
{showHeader && (
<Avatar
key={message.author.id}
user={message.author}
size={40}
style={{
color: "var(--text-secondary)",
fontWeight: "var(--font-weight-regular)",
fontSize: "16px",
marginRight: 10,
backgroundColor: "transparent",
}}
>
{split[0]}
<Link color="var(--text)" style={{ fontWeight: "var(--font-weight-medium)" }}>
{message.author.username}
</Link>
{split[1]}
</MessageContent>
);
}
default:
return (
<div>
/>
)}
<MessageContentContainer isHeader={showHeader}>
{showHeader && (
<MessageHeader>
<MessageAuthor>{message.author.username}</MessageAuthor>
<MessageTimestamp date={message.timestamp} />
</MessageHeader>
)}
{children}
{"files" in message && message.files?.length !== 0 && (
<AttachmentUploadProgress message={message} />
)}
</MessageContentContainer>
</MessageHeaderWrapper>
),
[message, contextMenuItems],
);
const constructDefaultMessage = React.useCallback(
() =>
withMessageHeader(
<MessageContent sending={isSending} failed={isFailed}>
{message.type !== MessageType.Default && (
<div style={{ color: "var(--text-secondary)", fontSize: "12px" }}>
MessageType({MessageType[message.type]})
</div>
{message.content}
</div>
);
)}
{parseMessageContent(message.content)}
{"attachments" in message
? message.attachments.map((attachment, index) => (
<Fragment key={index}>
<MessageAttachment
key={index}
attachment={attachment}
contextMenuItems={contextMenuItems}
/>
</Fragment>
))
: null}
{"embeds" in message
? message.embeds.map((embed, index) => (
<Fragment key={index}>
<MessageEmbed key={index} embed={embed} contextMenuItems={contextMenuItems} />
</Fragment>
))
: null}
</MessageContent>,
isHeader,
),
[message, isHeader],
);
const constructJoinMessage = React.useCallback(() => {
const joinMessage = message.getJoinMessage();
return (
<SystemMessage
message={message}
iconProps={{ icon: "mdiArrowRight", size: "16px", color: "var(--success)" }}
>
{reactStringReplace(joinMessage, "{author}", (_, i) => (
<Link color="var(--text)" style={{ fontWeight: "var(--font-weight-medium)" }} key={i}>
{message.author.username}
</Link>
))}
</SystemMessage>
);
}, [message]);
// handles creating the message content based on the message type
const renderMessageContent = React.useCallback(() => {
switch (message.type) {
case MessageType.Default:
return constructDefaultMessage();
case MessageType.UserJoin:
return constructJoinMessage();
default:
return constructDefaultMessage();
}
}, [message, isSending, isFailed]);
@ -212,32 +209,7 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
});
}}
>
{isHeader && (
<Avatar
key={message.author.id}
user={message.author}
size={40}
style={{
marginRight: 10,
backgroundColor: "transparent",
}}
/>
)}
<MessageContentContainer isHeader={isHeader}>
{isHeader && (
<MessageHeader>
<MessageAuthor>{message.author.username}</MessageAuthor>
<MessageTimestamp>{dayjs(message.timestamp).calendar()}</MessageTimestamp>
</MessageHeader>
)}
{renderMessageContent()}
{"files" in message && message.files?.length !== 0 && (
<AttachmentUploadProgress message={message} />
)}
</MessageContentContainer>
<MessageBase>{renderMessageContent()}</MessageBase>
</Container>
</MessageListItem>
);

View File

@ -0,0 +1,17 @@
import styled from "styled-components";
const Container = styled.div`
display: flex;
flex-direction: row;
position: relative;
padding: 2px 12px;
&:hover {
background-color: var(--background-primary-highlight);
}
`;
function MessageBase({ children }: { children: React.ReactNode }) {
return <Container>{children}</Container>;
}
export default MessageBase;

View File

@ -0,0 +1,22 @@
import dayjs from "dayjs";
import styled from "styled-components";
import { calendarStrings } from "../../utils/i18n";
import Tooltip from "../Tooltip";
const Container = styled.div`
font-size: 14px;
font-weight: var(--font-weight-regular);
margin-left: 10px;
color: var(--text-secondary);
user-select: none;
`;
function MessageTimestamp({ date }: { date: string | number | Date | dayjs.Dayjs }) {
return (
<Tooltip title={dayjs(date).format("dddd, MMMM MM, h:mm A")} placement="top">
<Container>{dayjs(date).calendar(undefined, calendarStrings)}</Container>
</Tooltip>
);
}
export default MessageTimestamp;

View File

@ -0,0 +1,36 @@
import React from "react";
import styled from "styled-components";
import { MessageLike } from "../../stores/objects/Message";
import Icon, { IconProps } from "../Icon";
import MessageTimestamp from "./MessageTimestamp";
const Container = styled.div`
display: flex;
flex-direction: row;
`;
interface Props {
message: MessageLike;
children: React.ReactNode;
iconProps?: IconProps;
}
function SystemMessage({ message, children, iconProps }: Props) {
return (
<Container>
<div style={{ margin: "0 10px", display: "flex" }}>{iconProps && <Icon {...iconProps} />}</div>
<div
style={{
color: "var(--text-secondary)",
fontWeight: "var(--font-weight-regular)",
fontSize: "16px",
}}
>
{children}
</div>
<MessageTimestamp date={message.timestamp} />
</Container>
);
}
export default SystemMessage;

View File

@ -23,9 +23,10 @@ import { BannerContextProvider } from "./contexts/BannerContext";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
import Theme from "./contexts/Theme";
import "./index.css";
import { calendarStrings } from "./utils/i18n";
dayjs.extend(relativeTime);
dayjs.extend(calendar);
dayjs.extend(calendar, calendarStrings);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<BrowserRouter>

View File

@ -1,4 +1,4 @@
import type { APIMessage } from "@spacebarchat/spacebar-api-types/v9";
import { MessageType, type APIMessage } from "@spacebarchat/spacebar-api-types/v9";
import type { IObservableArray } from "mobx";
import { action, computed, makeObservable, observable } from "mobx";
import useLogger from "../hooks/useLogger";
@ -89,6 +89,8 @@ export default class MessageStore {
if (
lastMessage &&
lastMessage.author.id === message.author.id &&
lastMessage.type === message.type &&
message.type === MessageType.Default &&
message.timestamp.getTime() - lastMessage.timestamp.getTime() <= 10 * 60 * 1000
) {
// add to last group

View File

@ -1,8 +1,7 @@
export const calendarStrings = {
sameDay: "[Today at] h:mm A", // The same day ( Today at 2:30 AM )
nextDay: "[Tomorrow at] h:mm A", // The next day ( Tomorrow at 2:30 AM )
nextWeek: "dddd [at] h:mm A", // The next week ( Sunday at 2:30 AM )
lastDay: "[Yesterday at] h:mm A", // The day before ( Yesterday at 2:30 AM )
lastWeek: "[Last] dddd [at] h:mm A", // Last week ( Last Monday at 2:30 AM )
sameElse: "MM/DD/YYYY h:mm A", // Everything else ( 17/10/2011 )
sameDay: "[Today at] h:mm A", // The same day (Today at 2:30 AM)
nextDay: "[Tomorrow at] h:mm A", // The next day (Tomorrow at 2:30 AM)
lastDay: "[Yesterday at] h:mm A", // The day before (Yesterday at 2:30 AM)
lastWeek: "[Last] dddd [at] h:mm A", // Last week (Last Monday at 2:30 AM)
sameElse: "MM/DD/YYYY h:mm A", // Everything else (01/19/2018 2:30 AM)
};