mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
attachment upload progress component, fix queued messages, etc
This commit is contained in:
parent
cdc0405fba
commit
4b7c8c714e
@ -2,7 +2,7 @@ import styled from "styled-components";
|
||||
|
||||
interface Props {
|
||||
// variant?: "primary" | "secondary" | "danger" | "success" | "warning";
|
||||
outlined?: boolean;
|
||||
variant?: "filled" | "outlined" | "blank";
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@ -22,7 +22,7 @@ export default styled.button<Props>`
|
||||
background-color: transparent;
|
||||
|
||||
color: ${(props) => {
|
||||
if (props.outlined) return "transparent";
|
||||
if (props.variant === "outlined") return "transparent";
|
||||
// switch (props.variant) {
|
||||
// case "primary":
|
||||
// return "var(--primary)";
|
||||
@ -41,7 +41,7 @@ export default styled.button<Props>`
|
||||
}};
|
||||
|
||||
border: ${(props) => {
|
||||
if (!props.outlined) return "none";
|
||||
if (props.variant !== "outlined") return "none";
|
||||
// switch (props.variant) {
|
||||
// case "primary":
|
||||
// return "1px solid var(--primary)";
|
||||
@ -60,7 +60,7 @@ export default styled.button<Props>`
|
||||
}};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-primary-alt);
|
||||
background-color: ${(props) => (props.variant === "blank" ? "transparent" : "var(--background-secondary)")}
|
||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||
}
|
||||
`;
|
||||
|
59
src/components/messaging/AttachmentUploadProgress.tsx
Normal file
59
src/components/messaging/AttachmentUploadProgress.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components";
|
||||
import { QueuedMessage } from "../../stores/MessageQueue";
|
||||
import Icon from "../Icon";
|
||||
import IconButton from "../IconButton";
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 25%;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background-color: var(--background-secondary);
|
||||
border-color: var(--background-secondary-alt);
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const Progress = styled.progress`
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const CustomIcon = styled(Icon)`
|
||||
color: var(--text-secondary);
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
message: QueuedMessage;
|
||||
}
|
||||
|
||||
function AttachmentUploadProgress({ message }: Props) {
|
||||
console.log(message.progress);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Wrapper>
|
||||
<div>{message.files!.length === 1 ? message.files![0].name : `${message.files!.length} files`}</div>
|
||||
<Progress value={message.progress} max={100} />
|
||||
</Wrapper>
|
||||
<IconButton variant="blank">
|
||||
<CustomIcon icon="mdiClose" size="24px" />
|
||||
</IconButton>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default observer(AttachmentUploadProgress);
|
@ -1,5 +1,6 @@
|
||||
import { useModals } from "@mattjennings/react-modal-stack";
|
||||
import { APIAttachment, MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import Moment from "react-moment";
|
||||
import styled from "styled-components";
|
||||
@ -13,6 +14,7 @@ import Avatar from "../Avatar";
|
||||
import { Link } from "../Link";
|
||||
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";
|
||||
import { IContextMenuItem } from "./../ContextMenuItem";
|
||||
import AttachmentUploadProgress from "./AttachmentUploadProgress";
|
||||
|
||||
type MessageLike = MessageObject | QueuedMessage;
|
||||
|
||||
@ -181,6 +183,8 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
if (message instanceof QueuedMessage) console.log(`progress at msg`, message.progress);
|
||||
|
||||
// construct the context menu options
|
||||
// React.useEffect(() => {
|
||||
// // if the message is queued, we don't need a context menu
|
||||
@ -260,10 +264,16 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
|
||||
{message.content ?? "No Content"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{"files" in message && message.files?.length !== 0 && (
|
||||
<div>
|
||||
<AttachmentUploadProgress message={message} />
|
||||
</div>
|
||||
)}
|
||||
</MessageContentContainer>
|
||||
</Container>
|
||||
</MessageListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default Message;
|
||||
export default observer(Message);
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { QueuedMessageStatus } from "../../stores/MessageQueue";
|
||||
import { QueuedMessage, QueuedMessageStatus } from "../../stores/MessageQueue";
|
||||
import { default as MessageObject } from "../../stores/objects/Message";
|
||||
import Message from "./Message";
|
||||
|
||||
@ -8,28 +10,27 @@ const Container = styled.div`
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
messages: MessageObject[];
|
||||
messages: (MessageObject | QueuedMessage)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
const renderMessage = React.useCallback((message: MessageObject | QueuedMessage, index: number) => {
|
||||
if (message instanceof QueuedMessage) console.log(`progress at msg group`, message.progress);
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}, []);
|
||||
|
||||
return <Container>{messages.map((message, index) => renderMessage(message, index))}</Container>;
|
||||
}
|
||||
|
||||
export default MessageGroup;
|
||||
export default observer(MessageGroup);
|
||||
|
@ -111,11 +111,6 @@ function MessageInput(props: Props) {
|
||||
);
|
||||
}, []);
|
||||
|
||||
const uploadProgressCallback = React.useCallback((e: ProgressEvent) => {
|
||||
const progress = Math.round((e.loaded * 100) / e.total);
|
||||
console.log(`uploadProgressCallback`, progress);
|
||||
}, []);
|
||||
|
||||
const onKeyDown = React.useCallback(
|
||||
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
@ -133,12 +128,12 @@ function MessageInput(props: Props) {
|
||||
if (!canSend && !shouldFail) return;
|
||||
|
||||
const nonce = Snowflake.generate();
|
||||
app.queue.add({
|
||||
const msg = app.queue.add({
|
||||
id: nonce,
|
||||
author: app.account! as unknown as User,
|
||||
content,
|
||||
channel: props.channel.id,
|
||||
// attachments,
|
||||
files: attachments,
|
||||
});
|
||||
|
||||
if (shouldSend) {
|
||||
@ -153,7 +148,7 @@ function MessageInput(props: Props) {
|
||||
} else {
|
||||
body = { content, nonce };
|
||||
}
|
||||
props.channel.sendMessage(body, uploadProgressCallback).catch((error) => {
|
||||
props.channel.sendMessage(body, msg.progressCallback).catch((error) => {
|
||||
app.queue.error(nonce, error as string);
|
||||
});
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import { PulseLoader } from "react-spinners";
|
||||
import styled from "styled-components";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { QueuedMessage } from "../../stores/MessageQueue";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import Message from "../../stores/objects/Message";
|
||||
@ -34,7 +35,6 @@ interface Props {
|
||||
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);
|
||||
const [canView, setCanView] = React.useState(false);
|
||||
|
||||
@ -55,7 +55,7 @@ function MessageList({ guild, channel }: Props) {
|
||||
}
|
||||
}, [guild, channel, canView]);
|
||||
|
||||
const renderMessageGroup = React.useCallback((group: Message[], index: number) => {
|
||||
const renderMessageGroup = React.useCallback((group: (Message | QueuedMessage)[], index: number) => {
|
||||
return <MessageGroup key={index} messages={group} />;
|
||||
}, []);
|
||||
|
||||
@ -66,6 +66,7 @@ function MessageList({ guild, channel }: Props) {
|
||||
|
||||
// get last group
|
||||
const lastGroup = channel.messages.grouped[channel.messages.grouped.length - 1];
|
||||
if ("status" in lastGroup) return;
|
||||
// get first message in the group to use as before
|
||||
const before = lastGroup[0].id;
|
||||
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import type { APIAttachment, APIMessage } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import type { APIMessage } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { MessageType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { action, computed, makeAutoObservable, observable } from "mobx";
|
||||
|
||||
@ -16,19 +16,47 @@ export type QueuedMessageData = {
|
||||
channel: string;
|
||||
author: User;
|
||||
content: string;
|
||||
attachments?: File[];
|
||||
files?: File[];
|
||||
};
|
||||
|
||||
export interface QueuedMessage {
|
||||
// export interface QueuedMessage extends QueuedMessageData {
|
||||
// status: QueuedMessageStatus;
|
||||
// error?: string;
|
||||
// timestamp: Date;
|
||||
// type: MessageType;
|
||||
// }
|
||||
|
||||
export class QueuedMessage implements QueuedMessageData {
|
||||
id: string;
|
||||
status: QueuedMessageStatus;
|
||||
error?: string;
|
||||
channel: string;
|
||||
author: User;
|
||||
content: string;
|
||||
files?: File[];
|
||||
@observable progress: number;
|
||||
status: QueuedMessageStatus;
|
||||
error?: string;
|
||||
timestamp: Date;
|
||||
type: MessageType;
|
||||
attachments: APIAttachment[];
|
||||
|
||||
constructor(data: QueuedMessageData) {
|
||||
this.id = data.id;
|
||||
this.channel = data.channel;
|
||||
this.author = data.author;
|
||||
this.content = data.content;
|
||||
this.files = data.files;
|
||||
this.progress = 0;
|
||||
this.status = QueuedMessageStatus.SENDING;
|
||||
this.timestamp = new Date();
|
||||
this.type = MessageType.Default;
|
||||
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
@action
|
||||
progressCallback(e: ProgressEvent) {
|
||||
this.progress = Math.round((e.loaded / e.total) * 100);
|
||||
console.log(this.progress);
|
||||
}
|
||||
}
|
||||
|
||||
export default class MessageQueue {
|
||||
@ -42,23 +70,15 @@ export default class MessageQueue {
|
||||
|
||||
@action
|
||||
add(data: QueuedMessageData) {
|
||||
this.messages.push({
|
||||
...data,
|
||||
timestamp: new Date(),
|
||||
status: QueuedMessageStatus.SENDING,
|
||||
type: MessageType.Default,
|
||||
attachments:
|
||||
data.attachments?.map((x) => ({
|
||||
id: Snowflake.generate(),
|
||||
filename: x.name,
|
||||
size: x.size,
|
||||
url: URL.createObjectURL(x),
|
||||
proxy_url: URL.createObjectURL(x),
|
||||
height: 0,
|
||||
width: 0,
|
||||
content_type: x.type,
|
||||
})) ?? [],
|
||||
});
|
||||
// this.messages.push({
|
||||
// ...data,
|
||||
// timestamp: new Date(),
|
||||
// status: QueuedMessageStatus.SENDING,
|
||||
// type: MessageType.Default,
|
||||
// });
|
||||
const msg = new QueuedMessage(data);
|
||||
this.messages.push(msg);
|
||||
return msg;
|
||||
}
|
||||
|
||||
@action
|
||||
|
@ -4,16 +4,19 @@ import { action, computed, makeObservable, observable } from "mobx";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import Logger from "../utils/Logger";
|
||||
import AppStore from "./AppStore";
|
||||
import { QueuedMessage } from "./MessageQueue";
|
||||
import Message from "./objects/Message";
|
||||
|
||||
export default class MessageStore {
|
||||
private readonly app: AppStore;
|
||||
private readonly channelId;
|
||||
private readonly logger: Logger;
|
||||
|
||||
@observable private readonly messagesArr: IObservableArray<Message>;
|
||||
|
||||
constructor(app: AppStore) {
|
||||
constructor(app: AppStore, channelId: string) {
|
||||
this.app = app;
|
||||
this.channelId = channelId;
|
||||
this.logger = useLogger("MessageStore.ts");
|
||||
|
||||
this.messagesArr = observable.array([]);
|
||||
@ -37,8 +40,8 @@ export default class MessageStore {
|
||||
|
||||
@computed
|
||||
get grouped() {
|
||||
// sort messages from the same author and within 7 minutes of each other into groups sorted by time, oldest messages at the end of the list and newest at the start, each groups messages should be sorted from oldest to newest
|
||||
const sortedGroups = this.messagesArr
|
||||
const messages = [...this.messagesArr, ...this.app.queue.get(this.channelId)];
|
||||
const sortedGroups = messages
|
||||
.slice()
|
||||
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
||||
.reduce((groups, message) => {
|
||||
@ -56,7 +59,7 @@ export default class MessageStore {
|
||||
groups.push([message]);
|
||||
}
|
||||
return groups;
|
||||
}, [] as Message[][])
|
||||
}, [] as (Message | QueuedMessage)[][])
|
||||
.map((group) => group.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()))
|
||||
.reverse();
|
||||
|
||||
|
@ -58,8 +58,6 @@ export default class Channel {
|
||||
constructor(app: AppStore, channel: APIChannel) {
|
||||
this.app = app;
|
||||
|
||||
this.messages = new MessageStore(app);
|
||||
|
||||
this.id = channel.id;
|
||||
this.createdAt = new Date(channel.created_at);
|
||||
this.name = channel.name;
|
||||
@ -88,6 +86,8 @@ export default class Channel {
|
||||
this.flags = channel.flags;
|
||||
this.defaultThreadRateLimitPerUser = channel.default_thread_rate_limit_per_user;
|
||||
|
||||
this.messages = new MessageStore(app, this.id);
|
||||
|
||||
if (channel.messages) {
|
||||
this.messages.addAll(channel.messages);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user