mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
system messages (UserJoin)
This commit is contained in:
parent
97fe9f663b
commit
94721c137b
@ -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"
|
||||
},
|
||||
|
@ -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:
|
||||
|
@ -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>
|
||||
);
|
||||
|
17
src/components/messaging/MessageBase.tsx
Normal file
17
src/components/messaging/MessageBase.tsx
Normal 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;
|
22
src/components/messaging/MessageTimestamp.tsx
Normal file
22
src/components/messaging/MessageTimestamp.tsx
Normal 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;
|
36
src/components/messaging/SystemMessage.tsx
Normal file
36
src/components/messaging/SystemMessage.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user