mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 10:22:30 +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-secure-storage": "^1.3.0",
|
||||||
"react-select-search": "^4.1.6",
|
"react-select-search": "^4.1.6",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
|
"react-string-replace": "^1.1.1",
|
||||||
"reoverlay": "^1.0.3",
|
"reoverlay": "^1.0.3",
|
||||||
"styled-components": "^5.3.10"
|
"styled-components": "^5.3.10"
|
||||||
},
|
},
|
||||||
|
@ -134,6 +134,9 @@ dependencies:
|
|||||||
react-spinners:
|
react-spinners:
|
||||||
specifier: ^0.13.8
|
specifier: ^0.13.8
|
||||||
version: 0.13.8(react-dom@18.2.0)(react@18.2.0)
|
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:
|
reoverlay:
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(react-dom@18.2.0)(react@18.2.0)
|
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)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
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):
|
/react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import dayjs from "dayjs";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
import { MessageLike } from "../../stores/objects/Message";
|
import { MessageLike } from "../../stores/objects/Message";
|
||||||
@ -9,7 +9,10 @@ import Avatar from "../Avatar";
|
|||||||
import { Link } from "../Link";
|
import { Link } from "../Link";
|
||||||
import { IContextMenuItem } from "./../ContextMenuItem";
|
import { IContextMenuItem } from "./../ContextMenuItem";
|
||||||
import MessageAttachment from "./MessageAttachment";
|
import MessageAttachment from "./MessageAttachment";
|
||||||
|
import MessageBase from "./MessageBase";
|
||||||
import MessageEmbed from "./MessageEmbed";
|
import MessageEmbed from "./MessageEmbed";
|
||||||
|
import MessageTimestamp from "./MessageTimestamp";
|
||||||
|
import SystemMessage from "./SystemMessage";
|
||||||
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
|
import AttachmentUploadProgress from "./attachments/AttachmentUploadProgress";
|
||||||
|
|
||||||
const MessageListItem = styled.li`
|
const MessageListItem = styled.li`
|
||||||
@ -37,6 +40,7 @@ const MessageHeader = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const MessageAuthor = styled.div`
|
const MessageAuthor = styled.div`
|
||||||
@ -44,13 +48,6 @@ const MessageAuthor = styled.div`
|
|||||||
font-weight: var(--font-weight-medium);
|
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 }>`
|
const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-light);
|
font-weight: var(--font-weight-light);
|
||||||
@ -60,32 +57,23 @@ const MessageContent = styled.div<{ sending?: boolean; failed?: boolean }>`
|
|||||||
color: ${(props) => (props.failed ? "var(--error)" : undefined)};
|
color: ${(props) => (props.failed ? "var(--error)" : undefined)};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// converts URLs in a string to html links
|
const MessageHeaderWrapper = styled.div`
|
||||||
const Linkify = ({ children }: { children: string }) => {
|
display: flex;
|
||||||
const urlPattern = /\bhttps?:\/\/\S+\b\/?/g;
|
flex-direction: row;
|
||||||
const matches = children.match(urlPattern);
|
`;
|
||||||
if (!matches) return <>{children}</>;
|
|
||||||
|
|
||||||
const elements = [];
|
function parseMessageContent(content?: string | null) {
|
||||||
let lastIndex = 0;
|
if (!content) return null;
|
||||||
|
// replace links with Link components
|
||||||
for (const match of matches) {
|
const replacedText = reactStringReplace(content, /(https?:\/\/\S+)/g, (match, i) => (
|
||||||
const matchIndex = children.indexOf(match, lastIndex);
|
<Link key={match + i} href={match} target="_blank" rel="noreferrer">
|
||||||
if (matchIndex > lastIndex) elements.push(children.substring(lastIndex, matchIndex));
|
|
||||||
|
|
||||||
elements.push(
|
|
||||||
<Link key={matchIndex} href={match} target="_blank" rel="noreferrer">
|
|
||||||
{match}
|
{match}
|
||||||
</Link>,
|
</Link>
|
||||||
);
|
));
|
||||||
lastIndex = matchIndex + match.length;
|
|
||||||
|
return replacedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastIndex < children.length) elements.push(children.substring(lastIndex));
|
|
||||||
|
|
||||||
return <>{elements}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: MessageLike;
|
message: MessageLike;
|
||||||
isHeader?: boolean;
|
isHeader?: boolean;
|
||||||
@ -110,41 +98,50 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// construct the context menu options
|
const withMessageHeader = React.useCallback(
|
||||||
// React.useEffect(() => {
|
(children: React.ReactNode, showHeader = false) => (
|
||||||
// // if the message is queued, we don't need a context menu
|
<MessageHeaderWrapper>
|
||||||
// if (isSending) {
|
{showHeader && (
|
||||||
// return;
|
<Avatar
|
||||||
// }
|
key={message.author.id}
|
||||||
|
user={message.author}
|
||||||
|
size={40}
|
||||||
|
style={{
|
||||||
|
marginRight: 10,
|
||||||
|
backgroundColor: "transparent",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
// // add delete/resend option if the current user is the message author
|
<MessageContentContainer isHeader={showHeader}>
|
||||||
// // if (author?.id === domain.account?.id) {
|
{showHeader && (
|
||||||
// // items.push({
|
<MessageHeader>
|
||||||
// // label: failed ? 'Resend Message' : 'Delete Message',
|
<MessageAuthor>{message.author.username}</MessageAuthor>
|
||||||
// // onPress: () => {
|
<MessageTimestamp date={message.timestamp} />
|
||||||
// // // TODO: implement
|
</MessageHeader>
|
||||||
// // console.debug(
|
)}
|
||||||
// // failed ? 'should resend message' : 'should delete message',
|
|
||||||
// // );
|
|
||||||
// // },
|
|
||||||
// // color: theme.colors.palette.red40,
|
|
||||||
// // iconProps: {
|
|
||||||
// // name: failed ? 'reload' : 'delete',
|
|
||||||
// // },
|
|
||||||
// // });
|
|
||||||
// // }
|
|
||||||
|
|
||||||
// // setContextMenuOptions(items);
|
{children}
|
||||||
// }, [isSending, isFailed]);
|
|
||||||
|
|
||||||
// handles creating the message content based on the message type
|
{"files" in message && message.files?.length !== 0 && (
|
||||||
// TODO: probably move this to a separate component
|
<AttachmentUploadProgress message={message} />
|
||||||
const renderMessageContent = React.useCallback(() => {
|
)}
|
||||||
switch (message.type) {
|
</MessageContentContainer>
|
||||||
case MessageType.Default:
|
</MessageHeaderWrapper>
|
||||||
return (
|
),
|
||||||
|
[message, contextMenuItems],
|
||||||
|
);
|
||||||
|
|
||||||
|
const constructDefaultMessage = React.useCallback(
|
||||||
|
() =>
|
||||||
|
withMessageHeader(
|
||||||
<MessageContent sending={isSending} failed={isFailed}>
|
<MessageContent sending={isSending} failed={isFailed}>
|
||||||
{message.content ? <Linkify>{message.content}</Linkify> : null}
|
{message.type !== MessageType.Default && (
|
||||||
|
<div style={{ color: "var(--text-secondary)", fontSize: "12px" }}>
|
||||||
|
MessageType({MessageType[message.type]})
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{parseMessageContent(message.content)}
|
||||||
{"attachments" in message
|
{"attachments" in message
|
||||||
? message.attachments.map((attachment, index) => (
|
? message.attachments.map((attachment, index) => (
|
||||||
<Fragment key={index}>
|
<Fragment key={index}>
|
||||||
@ -163,37 +160,37 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
</MessageContent>
|
</MessageContent>,
|
||||||
|
isHeader,
|
||||||
|
),
|
||||||
|
[message, isHeader],
|
||||||
);
|
);
|
||||||
case MessageType.UserJoin: {
|
|
||||||
// TODO: render only the join message and timestamp, will require a bit of refactoring
|
const constructJoinMessage = React.useCallback(() => {
|
||||||
const msg = message.getJoinMessage();
|
const joinMessage = message.getJoinMessage();
|
||||||
const split = msg.split("{author}");
|
|
||||||
return (
|
return (
|
||||||
<MessageContent
|
<SystemMessage
|
||||||
style={{
|
message={message}
|
||||||
color: "var(--text-secondary)",
|
iconProps={{ icon: "mdiArrowRight", size: "16px", color: "var(--success)" }}
|
||||||
fontWeight: "var(--font-weight-regular)",
|
|
||||||
fontSize: "16px",
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{split[0]}
|
{reactStringReplace(joinMessage, "{author}", (_, i) => (
|
||||||
<Link color="var(--text)" style={{ fontWeight: "var(--font-weight-medium)" }}>
|
<Link color="var(--text)" style={{ fontWeight: "var(--font-weight-medium)" }} key={i}>
|
||||||
{message.author.username}
|
{message.author.username}
|
||||||
</Link>
|
</Link>
|
||||||
{split[1]}
|
))}
|
||||||
</MessageContent>
|
</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:
|
default:
|
||||||
return (
|
return constructDefaultMessage();
|
||||||
<div>
|
|
||||||
<div style={{ color: "var(--text-secondary)", fontSize: "12px" }}>
|
|
||||||
MessageType({MessageType[message.type]})
|
|
||||||
</div>
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [message, isSending, isFailed]);
|
}, [message, isSending, isFailed]);
|
||||||
|
|
||||||
@ -212,32 +209,7 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isHeader && (
|
<MessageBase>{renderMessageContent()}</MessageBase>
|
||||||
<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>
|
|
||||||
</Container>
|
</Container>
|
||||||
</MessageListItem>
|
</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 { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
|
||||||
import Theme from "./contexts/Theme";
|
import Theme from "./contexts/Theme";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { calendarStrings } from "./utils/i18n";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
dayjs.extend(calendar);
|
dayjs.extend(calendar, calendarStrings);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<BrowserRouter>
|
<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 type { IObservableArray } from "mobx";
|
||||||
import { action, computed, makeObservable, observable } from "mobx";
|
import { action, computed, makeObservable, observable } from "mobx";
|
||||||
import useLogger from "../hooks/useLogger";
|
import useLogger from "../hooks/useLogger";
|
||||||
@ -89,6 +89,8 @@ export default class MessageStore {
|
|||||||
if (
|
if (
|
||||||
lastMessage &&
|
lastMessage &&
|
||||||
lastMessage.author.id === message.author.id &&
|
lastMessage.author.id === message.author.id &&
|
||||||
|
lastMessage.type === message.type &&
|
||||||
|
message.type === MessageType.Default &&
|
||||||
message.timestamp.getTime() - lastMessage.timestamp.getTime() <= 10 * 60 * 1000
|
message.timestamp.getTime() - lastMessage.timestamp.getTime() <= 10 * 60 * 1000
|
||||||
) {
|
) {
|
||||||
// add to last group
|
// add to last group
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
export const calendarStrings = {
|
export const calendarStrings = {
|
||||||
sameDay: "[Today at] h:mm A", // The same day (Today at 2:30 AM)
|
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)
|
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)
|
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)
|
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 )
|
sameElse: "MM/DD/YYYY h:mm A", // Everything else (01/19/2018 2:30 AM)
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user