mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 10:22:30 +01:00
rework message listing
This commit is contained in:
parent
351f3eca92
commit
662a49024b
@ -1,127 +0,0 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { useParams } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { QueuedMessageStatus } from "../stores/MessageQueue";
|
||||
import ChatHeader from "./ChatHeader";
|
||||
import Message from "./Message";
|
||||
import MessageInput from "./MessageInput";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 100%;
|
||||
background-color: var(--background-primary-alt);
|
||||
`;
|
||||
|
||||
const MessageListWrapper = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Spacer = styled.div`
|
||||
height: 30px;
|
||||
width: 1px;
|
||||
`;
|
||||
|
||||
function Chat() {
|
||||
const app = useAppStore();
|
||||
const logger = useLogger("Chat");
|
||||
const { guildId, channelId } = useParams<{
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
}>();
|
||||
const guild = app.guilds.get(guildId!);
|
||||
const channel = guild?.channels.get(channelId!);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (guild && channel) {
|
||||
channel.getMessages(app, true);
|
||||
}
|
||||
}, [guild, channel]);
|
||||
|
||||
if (!guild || !channel) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatHeader channel={channel} />
|
||||
<span>{!guild ? "Unknown Guild" : "Unknown Channel"}</span>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const messages = [...(channel.messages.messages ?? []), ...(channel ? app.queue.get(channel.id) ?? [] : [])];
|
||||
|
||||
const fetchMore = async () => {
|
||||
if (!channel.messages.count) {
|
||||
return;
|
||||
}
|
||||
// get first message in the list to use as before
|
||||
const before = channel.messages.messages[0].id;
|
||||
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
||||
await channel.getMessages(app, false, 50, before);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChatHeader channel={channel} />
|
||||
<Container>
|
||||
<MessageListWrapper>
|
||||
<MessageListWrapper id="scrollable-div">
|
||||
<InfiniteScroll
|
||||
dataLength={messages.length}
|
||||
next={fetchMore}
|
||||
inverse={true}
|
||||
// TODO: change this to false when we have a fetch that returns less than 50 messages
|
||||
hasMore={true}
|
||||
loader={<h4>Loading...</h4>}
|
||||
scrollableTarget="scrollable-div"
|
||||
>
|
||||
{messages.map((message, index, arr) => {
|
||||
// calculate max ms between messages to determine if they should be grouped (if from same author). 7 minutes
|
||||
const maxTimeDifference = 1000 * 60 * 7;
|
||||
|
||||
const isHeader =
|
||||
// always show header for first message
|
||||
index === 0 ||
|
||||
// show header if author is different from previous message
|
||||
message.author.id !== arr[index - 1].author.id ||
|
||||
// show header if time difference is greater than maxTimeDifference
|
||||
message.timestamp.getTime() - arr[index - 1].timestamp.getTime() >
|
||||
maxTimeDifference;
|
||||
|
||||
return (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
isHeader={isHeader}
|
||||
isSending={
|
||||
"status" in message && message.status === QueuedMessageStatus.SENDING
|
||||
}
|
||||
isFailed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Spacer />
|
||||
</InfiniteScroll>
|
||||
</MessageListWrapper>
|
||||
</MessageListWrapper>
|
||||
<MessageInput channel={channel} />
|
||||
</Container>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Chat);
|
@ -1,5 +1,17 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
export const Divider = styled.span`
|
||||
export const HorizontalDivider = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
z-index: 1;
|
||||
height: 0;
|
||||
border-top: thin solid var(--text-disabled);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
export const TextDivider = styled.span`
|
||||
padding: 0 4px;
|
||||
`;
|
||||
|
@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import Moment from "react-moment";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||
import { QueuedMessage } from "../stores/MessageQueue";
|
||||
import { default as MessageObject } from "../stores/objects/Message";
|
||||
import { calendarStrings } from "../utils/i18n";
|
||||
import Avatar from "./Avatar";
|
||||
import { IContextMenuItem } from "./ContextMenuItem";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { QueuedMessage } from "../../stores/MessageQueue";
|
||||
import { default as MessageObject } from "../../stores/objects/Message";
|
||||
import { calendarStrings } from "../../utils/i18n";
|
||||
import Avatar from "../Avatar";
|
||||
import { IContextMenuItem } from "./../ContextMenuItem";
|
||||
|
||||
type MessageLike = MessageObject | QueuedMessage;
|
||||
|
||||
@ -17,7 +17,6 @@ const Container = styled.div<{ isHeader?: boolean }>`
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
padding: ${(props) => (props.isHeader ? "4" : "2")}px 12px;
|
||||
margin-top: ${(props) => (props.isHeader ? "20px" : undefined)};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-primary-highlight);
|
||||
@ -60,6 +59,9 @@ interface Props {
|
||||
isFailed?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for rendering a single message
|
||||
*/
|
||||
function Message({ message, isHeader, isSending, isFailed }: Props) {
|
||||
const contextMenu = React.useContext(ContextMenuContext);
|
||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
35
src/components/messaging/MessageGroup.tsx
Normal file
35
src/components/messaging/MessageGroup.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import styled from "styled-components";
|
||||
import { QueuedMessageStatus } from "../../stores/MessageQueue";
|
||||
import { default as MessageObject } from "../../stores/objects/Message";
|
||||
import Message from "./Message";
|
||||
|
||||
const Container = styled.div`
|
||||
margin-top: 20px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
messages: MessageObject[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that handles rendering a group of messages from the same author
|
||||
*/
|
||||
function MessageGroup({ messages }: Props) {
|
||||
return (
|
||||
<Container>
|
||||
{messages.map((message, index) => {
|
||||
return (
|
||||
<Message
|
||||
key={message.id}
|
||||
message={message}
|
||||
isHeader={index === 0}
|
||||
isSending={"status" in message && message.status === QueuedMessageStatus.SENDING}
|
||||
isFailed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageGroup;
|
@ -1,14 +1,14 @@
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { BaseEditor, Descendant, Node, createEditor } from "slate";
|
||||
import { HistoryEditor, withHistory } from "slate-history";
|
||||
import { Editable, ReactEditor, Slate, withReact } from "slate-react";
|
||||
import User from "../stores/objects/User";
|
||||
import Snowflake from "../utils/Snowflake";
|
||||
import User from "../../stores/objects/User";
|
||||
import Snowflake from "../../utils/Snowflake";
|
||||
|
||||
type CustomElement = { type: "paragraph"; children: CustomText[] };
|
||||
type CustomText = { text: string; bold?: true };
|
||||
@ -50,6 +50,9 @@ interface Props {
|
||||
channel?: Channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for sending messages
|
||||
*/
|
||||
function MessageInput(props: Props) {
|
||||
const app = useAppStore();
|
||||
const logger = useLogger("MessageInput");
|
95
src/components/messaging/MessageList.tsx
Normal file
95
src/components/messaging/MessageList.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import { PulseLoader } from "react-spinners";
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import { HorizontalDivider } from "../Divider";
|
||||
import MessageGroup from "./MessageGroup";
|
||||
|
||||
const Container = styled.div`
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
`;
|
||||
|
||||
const EndMessageContainer = styled.div`
|
||||
margin: 16px 16px 0 16px;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
guild: Guild;
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main component for rendering the messages list of a channel
|
||||
*/
|
||||
function MessageList({ guild, channel }: Props) {
|
||||
const app = useAppStore();
|
||||
const logger = useLogger("MessageList.tsx");
|
||||
// const messages = [...(channel.messages.messages ?? []), ...(channel ? app.queue.get(channel.id) ?? [] : [])];
|
||||
const [hasMore, setHasMore] = React.useState(true);
|
||||
|
||||
// handles the initial fetch of channel messages
|
||||
React.useEffect(() => {
|
||||
if (guild && channel && channel.messages.count === 0) {
|
||||
channel.getMessages(app, true).then((r) => {
|
||||
if (r < 50) {
|
||||
setHasMore(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [guild, channel]);
|
||||
|
||||
const fetchMore = async () => {
|
||||
if (!channel.messages.count) {
|
||||
return;
|
||||
}
|
||||
// get first message in the list to use as before
|
||||
const before = channel.messages.grouped[0][0].id;
|
||||
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
||||
channel.getMessages(app, false, 50, before).then((r) => {
|
||||
if (r < 50) {
|
||||
setHasMore(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Container id="scrollable-div">
|
||||
<InfiniteScroll
|
||||
dataLength={channel.messages.grouped.length}
|
||||
next={fetchMore}
|
||||
style={{ display: "flex", flexDirection: "column-reverse" }} // to put endMessage and loader to the top.
|
||||
hasMore={hasMore}
|
||||
loader={
|
||||
<PulseLoader
|
||||
style={{ display: "flex", justifyContent: "center", alignContent: "center" }}
|
||||
color="var(--primary)"
|
||||
/>
|
||||
}
|
||||
scrollableTarget="scrollable-div"
|
||||
endMessage={
|
||||
<EndMessageContainer>
|
||||
<h1 style={{ fontWeight: 700, margin: "8px 0" }}>Welcome to #{channel.name}!</h1>
|
||||
<p style={{ color: "var(--text-secondary)" }}>
|
||||
This is the start of the #{channel.name} channel.
|
||||
</p>
|
||||
<HorizontalDivider />
|
||||
</EndMessageContainer>
|
||||
}
|
||||
>
|
||||
{channel.messages.grouped.map((group, index) => {
|
||||
return <MessageGroup key={index} messages={group} />;
|
||||
})}
|
||||
</InfiniteScroll>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(MessageList);
|
64
src/components/messaging/Messages.tsx
Normal file
64
src/components/messaging/Messages.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useParams } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import MessageInput from "./MessageInput";
|
||||
import MessageList from "./MessageList";
|
||||
import MessagesHeader from "./MessagesHeader";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 100%;
|
||||
background-color: var(--background-primary-alt);
|
||||
`;
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const Spacer = styled.div`
|
||||
margin-bottom: 30px;
|
||||
`;
|
||||
|
||||
/**
|
||||
* Main component for rendering channel messages
|
||||
*/
|
||||
function Messages() {
|
||||
const app = useAppStore();
|
||||
const logger = useLogger("Messages");
|
||||
|
||||
const { guildId, channelId } = useParams<{
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
}>();
|
||||
const guild = app.guilds.get(guildId!);
|
||||
const channel = guild?.channels.get(channelId!);
|
||||
|
||||
if (!guild || !channel) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<MessagesHeader channel={channel} />
|
||||
<span>{!guild ? "Unknown Guild" : "Unknown Channel"}</span>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<MessagesHeader channel={channel} />
|
||||
<Container>
|
||||
<MessageList guild={guild} channel={channel} />
|
||||
<Spacer />
|
||||
<MessageInput channel={channel} />
|
||||
</Container>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(Messages);
|
@ -1,8 +1,8 @@
|
||||
import * as Icons from "@mdi/js";
|
||||
import styled from "styled-components";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Icon from "./Icon";
|
||||
import Tooltip from "./Tooltip";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Icon from "../Icon";
|
||||
import Tooltip from "../Tooltip";
|
||||
|
||||
const IconButton = styled.button`
|
||||
margin: 0;
|
||||
@ -112,7 +112,10 @@ function ActionItem({ icon, active, ariaLabel, tooltip }: ActionItemProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatHeader({ channel }: Props) {
|
||||
/**
|
||||
* Top header for channel messages section
|
||||
*/
|
||||
function MessagesHeader({ channel }: Props) {
|
||||
return (
|
||||
<Container>
|
||||
<Wrapper>
|
||||
@ -137,4 +140,4 @@ function ChatHeader({ channel }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ChatHeader;
|
||||
export default MessagesHeader;
|
@ -8,7 +8,7 @@ import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents";
|
||||
import { Divider } from "../Divider";
|
||||
import { TextDivider } from "../Divider";
|
||||
import Icon from "../Icon";
|
||||
import AddServerModal from "./AddServerModal";
|
||||
import {
|
||||
@ -212,7 +212,7 @@ function CreateServerModal() {
|
||||
{errors.name && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.name.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
|
@ -7,7 +7,7 @@ import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
|
||||
import { Divider } from "../Divider";
|
||||
import { TextDivider } from "../Divider";
|
||||
import Icon from "../Icon";
|
||||
import AddServerModal from "./AddServerModal";
|
||||
import {
|
||||
@ -130,7 +130,7 @@ function JoinServerModal() {
|
||||
{errors.code && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.code.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -145,6 +145,7 @@ function JoinServerModal() {
|
||||
error={!!errors.code}
|
||||
disabled={isLoading}
|
||||
autoFocus
|
||||
minLength={6}
|
||||
/>
|
||||
</InviteInputContainer>
|
||||
</form>
|
||||
|
@ -7,6 +7,8 @@ h6,
|
||||
p,
|
||||
span {
|
||||
color: var(--text);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
SubmitButton,
|
||||
Wrapper,
|
||||
} from "../components/AuthComponents";
|
||||
import { Divider } from "../components/Divider";
|
||||
import { TextDivider } from "../components/Divider";
|
||||
import HCaptcha, { HeaderContainer } from "../components/HCaptcha";
|
||||
import ForgotPasswordModal from "../components/modals/ForgotPasswordModal";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
@ -244,7 +244,7 @@ function LoginPage() {
|
||||
{isCheckingInstance != false && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
Checking
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -252,7 +252,7 @@ function LoginPage() {
|
||||
{errors.instance && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.instance.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -279,7 +279,7 @@ function LoginPage() {
|
||||
{errors.login && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.login.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -303,7 +303,7 @@ function LoginPage() {
|
||||
{errors.password && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.password.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
|
@ -22,7 +22,7 @@ import {
|
||||
Wrapper,
|
||||
} from "../components/AuthComponents";
|
||||
import DOBInput from "../components/DOBInput";
|
||||
import { Divider } from "../components/Divider";
|
||||
import { TextDivider } from "../components/Divider";
|
||||
import HCaptcha from "../components/HCaptcha";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { AUTH_NO_BRANDING, useAppStore } from "../stores/AppStore";
|
||||
@ -180,7 +180,7 @@ function RegistrationPage() {
|
||||
{errors.email && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.email.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -204,7 +204,7 @@ function RegistrationPage() {
|
||||
{errors.username && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.username.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -226,7 +226,7 @@ function RegistrationPage() {
|
||||
{errors.password && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.password.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
@ -249,7 +249,7 @@ function RegistrationPage() {
|
||||
{errors.date_of_birth && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.date_of_birth.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
|
@ -1,11 +1,11 @@
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import ChannelSidebar from "../../components/ChannelSidebar";
|
||||
import Chat from "../../components/Chat";
|
||||
import Container from "../../components/Container";
|
||||
import ContextMenu from "../../components/ContextMenu";
|
||||
import GuildSidebar from "../../components/GuildSidebar";
|
||||
import MemberList from "../../components/MemberList";
|
||||
import Messages from "../../components/messaging/Messages";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
|
||||
const Wrapper = styled(Container)`
|
||||
@ -17,7 +17,7 @@ function Test() {
|
||||
return (
|
||||
<>
|
||||
<ChannelSidebar />
|
||||
<Chat />
|
||||
<Messages />
|
||||
<MemberList />
|
||||
</>
|
||||
);
|
||||
|
@ -19,7 +19,7 @@ import {
|
||||
SubmitButton,
|
||||
Wrapper,
|
||||
} from "../../components/AuthComponents";
|
||||
import { Divider } from "../../components/Divider";
|
||||
import { TextDivider } from "../../components/Divider";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import {
|
||||
@ -108,7 +108,7 @@ function MFA(props: IAPILoginResponseMFARequired) {
|
||||
{errors.code && (
|
||||
<InputErrorText>
|
||||
<>
|
||||
<Divider>-</Divider>
|
||||
<TextDivider>-</TextDivider>
|
||||
{errors.code.message}
|
||||
</>
|
||||
</InputErrorText>
|
||||
|
@ -1,16 +1,20 @@
|
||||
import 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";
|
||||
import Logger from "../utils/Logger";
|
||||
import AppStore from "./AppStore";
|
||||
import Message from "./objects/Message";
|
||||
|
||||
export default class MessageStore {
|
||||
private readonly app: AppStore;
|
||||
private readonly logger: Logger;
|
||||
|
||||
@observable private readonly messagesArr: IObservableArray<Message>;
|
||||
|
||||
constructor(app: AppStore) {
|
||||
this.app = app;
|
||||
this.logger = useLogger("MessageStore.ts");
|
||||
|
||||
this.messagesArr = observable.array([]);
|
||||
|
||||
@ -32,11 +36,32 @@ export default class MessageStore {
|
||||
}
|
||||
|
||||
@computed
|
||||
get messages() {
|
||||
return this.messagesArr
|
||||
.slice()
|
||||
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
||||
.filter((x) => x);
|
||||
get grouped() {
|
||||
const groupedMessages: Message[][] = [];
|
||||
let lastAuthorId: string | undefined = undefined;
|
||||
let lastTimestamp: Date | undefined = undefined;
|
||||
let lastGroup: Message[] | undefined = undefined;
|
||||
for (const message of this.messagesArr) {
|
||||
if (
|
||||
lastAuthorId !== message.author.id ||
|
||||
!lastTimestamp ||
|
||||
message.timestamp.getTime() - lastTimestamp.getTime() > 1000 * 60 * 7
|
||||
) {
|
||||
// start a new group
|
||||
lastAuthorId = message.author.id;
|
||||
lastTimestamp = message.timestamp;
|
||||
lastGroup = [];
|
||||
groupedMessages.push(lastGroup);
|
||||
}
|
||||
if (!lastGroup) {
|
||||
// this should never happen
|
||||
this.logger.error("lastGroup is undefined");
|
||||
continue;
|
||||
}
|
||||
lastGroup.push(message);
|
||||
}
|
||||
|
||||
return groupedMessages;
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
|
@ -144,58 +144,61 @@ export default class Channel {
|
||||
}
|
||||
|
||||
@action
|
||||
async getMessages(
|
||||
getMessages(
|
||||
app: AppStore,
|
||||
isInitial: boolean,
|
||||
limit?: number,
|
||||
before?: SnowflakeType,
|
||||
after?: SnowflakeType,
|
||||
around?: SnowflakeType,
|
||||
) {
|
||||
if (isInitial && this.hasFetchedMessages) {
|
||||
return;
|
||||
}
|
||||
): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isInitial && this.hasFetchedMessages) {
|
||||
return;
|
||||
}
|
||||
|
||||
let opts: RESTGetAPIChannelMessagesQuery = {
|
||||
limit: limit || 50,
|
||||
};
|
||||
let opts: RESTGetAPIChannelMessagesQuery = {
|
||||
limit: limit || 50,
|
||||
};
|
||||
|
||||
if (before) {
|
||||
opts = { ...opts, before };
|
||||
}
|
||||
if (after) {
|
||||
opts = { ...opts, after };
|
||||
}
|
||||
if (around) {
|
||||
opts = { ...opts, around };
|
||||
}
|
||||
if (before) {
|
||||
opts = { ...opts, before };
|
||||
}
|
||||
if (after) {
|
||||
opts = { ...opts, after };
|
||||
}
|
||||
if (around) {
|
||||
opts = { ...opts, around };
|
||||
}
|
||||
|
||||
this.hasFetchedMessages = true;
|
||||
this.logger.info(`Fetching messags for ${this.id}`);
|
||||
app.rest
|
||||
.get<RESTGetAPIChannelMessagesResult | APIError>(Routes.channelMessages(this.id), opts)
|
||||
.then((res) => {
|
||||
if ("code" in res) {
|
||||
this.logger.error(res);
|
||||
return;
|
||||
}
|
||||
this.messages.addAll(
|
||||
res.filter((x) => !this.messages.has(x.id)).reverse(),
|
||||
// .sort((a, b) => {
|
||||
// const aTimestamp = new Date(a.timestamp as unknown as string);
|
||||
// const bTimestamp = new Date(b.timestamp as unknown as string);
|
||||
// return aTimestamp.getTime() - bTimestamp.getTime();
|
||||
// })
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error(err);
|
||||
});
|
||||
this.hasFetchedMessages = true;
|
||||
this.logger.info(`Fetching messags for ${this.id}`);
|
||||
app.rest
|
||||
.get<RESTGetAPIChannelMessagesResult | APIError>(Routes.channelMessages(this.id), opts)
|
||||
.then((res) => {
|
||||
if ("code" in res) {
|
||||
this.logger.error(res);
|
||||
return;
|
||||
}
|
||||
this.messages.addAll(
|
||||
res.filter((x) => !this.messages.has(x.id)),
|
||||
// .sort((a, b) => {
|
||||
// const aTimestamp = new Date(a.timestamp as unknown as string);
|
||||
// const bTimestamp = new Date(b.timestamp as unknown as string);
|
||||
// return aTimestamp.getTime() - bTimestamp.getTime();
|
||||
// })
|
||||
);
|
||||
resolve(res.length);
|
||||
})
|
||||
.catch((err) => {
|
||||
this.logger.error(err);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async sendMessage(data: RESTPostAPIChannelMessageJSONBody) {
|
||||
// TODO: handle errors, highlight message as failed
|
||||
return this.app.rest.post<RESTPostAPIChannelMessageJSONBody, RESTPostAPIChannelMessageResult>(
|
||||
Routes.channelMessages(this.id),
|
||||
data,
|
||||
|
Loading…
Reference in New Issue
Block a user