mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
new message list and WIP read states
This commit is contained in:
parent
e20967d0f2
commit
a8c91ca7a5
@ -48,6 +48,7 @@
|
|||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-advanced-cropper": "^0.19.6",
|
"react-advanced-cropper": "^0.19.6",
|
||||||
"react-colorful": "^5.6.1",
|
"react-colorful": "^5.6.1",
|
||||||
|
"react-content-loader": "^7.0.2",
|
||||||
"react-device-detect": "^2.2.3",
|
"react-device-detect": "^2.2.3",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-fps-stats": "^0.3.1",
|
"react-fps-stats": "^0.3.1",
|
||||||
|
@ -137,6 +137,9 @@ dependencies:
|
|||||||
react-colorful:
|
react-colorful:
|
||||||
specifier: ^5.6.1
|
specifier: ^5.6.1
|
||||||
version: 5.6.1(react-dom@18.2.0)(react@18.2.0)
|
version: 5.6.1(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-content-loader:
|
||||||
|
specifier: ^7.0.2
|
||||||
|
version: 7.0.2(react@18.2.0)
|
||||||
react-device-detect:
|
react-device-detect:
|
||||||
specifier: ^2.2.3
|
specifier: ^2.2.3
|
||||||
version: 2.2.3(react-dom@18.2.0)(react@18.2.0)
|
version: 2.2.3(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -12956,6 +12959,15 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-content-loader@7.0.2(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-773S98JTyC8VB2nu7LXUhpHx8tZMieGxMcx3qTe7IkohT6Br7d9AXnIXs/wQ6IhlUdKQcw6JLKk1QKigYCWDRA==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.0.0'
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-dev-utils@12.0.1(eslint@8.57.0)(typescript@5.4.5)(webpack@5.90.3):
|
/react-dev-utils@12.0.1(eslint@8.57.0)(typescript@5.4.5)(webpack@5.90.3):
|
||||||
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
|
resolution: {integrity: sha512-84Ivxmr17KjUupyqzFode6xKhjwuEJDROWKJy/BthkL7Wn6NJ8h4WE6k/exAv6ImS+0oZLRRW5j/aINMHyeGeQ==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { useContext, useEffect } from "react";
|
import React, { useContext, useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
@ -7,6 +8,7 @@ import { useAppStore } from "../../stores/AppStore";
|
|||||||
import Channel from "../../stores/objects/Channel";
|
import Channel from "../../stores/objects/Channel";
|
||||||
import { Permissions } from "../../utils/Permissions";
|
import { Permissions } from "../../utils/Permissions";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
|
import SidebarPill from "../SidebarPill";
|
||||||
import Floating from "../floating/Floating";
|
import Floating from "../floating/Floating";
|
||||||
import FloatingTrigger from "../floating/FloatingTrigger";
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<SidebarPill type={channel.hasUnread() ? "unread" : "none"} />
|
||||||
{channel.channelIcon && !isCategory && (
|
{channel.channelIcon && !isCategory && (
|
||||||
<Icon
|
<Icon
|
||||||
icon={channel.channelIcon}
|
icon={channel.channelIcon}
|
||||||
@ -163,4 +166,4 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ChannelListItem;
|
export default observer(ChannelListItem);
|
||||||
|
@ -5,7 +5,7 @@ export type PillType = "none" | "unread" | "hover" | "active";
|
|||||||
|
|
||||||
const Wrapper = styled(Container)`
|
const Wrapper = styled(Container)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
// top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
|
27
src/components/SkeletonLoader.tsx
Normal file
27
src/components/SkeletonLoader.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
function SkeletonLoader() {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
role="img"
|
||||||
|
width="560"
|
||||||
|
height="66"
|
||||||
|
aria-labelledby="loading-aria"
|
||||||
|
viewBox="0 0 560 66"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
fill="#121212"
|
||||||
|
>
|
||||||
|
<title id="loading-aria">Loading...</title>
|
||||||
|
<rect x="0" y="0" width="100%" height="100%" clip-path="url(#clip-path)"></rect>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip-path">
|
||||||
|
<circle cx="32" cy="18" r="16" />
|
||||||
|
<rect x="60" y="10" rx="4" ry="4" width="90" height="7" />
|
||||||
|
<rect x="160" y="10" rx="4" ry="4" width="110" height="7" />
|
||||||
|
<rect x="60" y="31" rx="3" ry="3" width="400" height="6" />
|
||||||
|
<rect x="60" y="42" rx="3" ry="3" width="270" height="6" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SkeletonLoader;
|
@ -54,9 +54,12 @@ interface Props2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChatContent({ channel, guild }: Props2) {
|
function ChatContent({ channel, guild }: Props2) {
|
||||||
|
const app = useAppStore();
|
||||||
|
const readstate = app.readStateStore.get(channel.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<MessageList guild={guild} channel={channel} />
|
<MessageList guild={guild} channel={channel} before={readstate?.lastMessageId} />
|
||||||
<MessageInput channel={channel} guild={guild} />
|
<MessageInput channel={channel} guild={guild} />
|
||||||
<TypingIndicator channel={channel} />
|
<TypingIndicator channel={channel} />
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -70,7 +70,7 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
|||||||
height = newHeight;
|
height = newHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Original size: ${originalWidth}x${originalHeight} - Scaled size: ${width}x${height}`);
|
// console.log(`Original size: ${originalWidth}x${originalHeight} - Scaled size: ${width}x${height}`);
|
||||||
|
|
||||||
switch (embed.provider?.name) {
|
switch (embed.provider?.name) {
|
||||||
case "YouTube": {
|
case "YouTube": {
|
||||||
|
@ -18,7 +18,7 @@ function MessageGroup({ group }: Props) {
|
|||||||
<>
|
<>
|
||||||
{messages.map((message, index) => {
|
{messages.map((message, index) => {
|
||||||
if (message.type === MessageType.Default || message.type === MessageType.Reply) {
|
if (message.type === MessageType.Default || message.type === MessageType.Reply) {
|
||||||
return <Message key={message.id} message={message} header={index === messages.length - 1} />;
|
return <Message key={message.id} message={message} header={index === 0} />;
|
||||||
} else return <SystemMessage key={message.id} message={message} />;
|
} else return <SystemMessage key={message.id} message={message} />;
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { useEffect, useRef, useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import { PulseLoader } from "react-spinners";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import useResizeObserver from "use-resize-observer";
|
import useResizeObserver from "use-resize-observer";
|
||||||
import { VList, VListHandle } from "virtua";
|
import { VList, VListHandle } from "virtua";
|
||||||
@ -11,6 +10,7 @@ import Channel from "../../stores/objects/Channel";
|
|||||||
import Guild from "../../stores/objects/Guild";
|
import Guild from "../../stores/objects/Guild";
|
||||||
import { Permissions } from "../../utils/Permissions";
|
import { Permissions } from "../../utils/Permissions";
|
||||||
import { HorizontalDivider } from "../Divider";
|
import { HorizontalDivider } from "../Divider";
|
||||||
|
import SkeletonLoader from "../SkeletonLoader.tsx";
|
||||||
import MessageGroup from "./MessageGroup";
|
import MessageGroup from "./MessageGroup";
|
||||||
|
|
||||||
export const MessageAreaWidthContext = React.createContext(0);
|
export const MessageAreaWidthContext = React.createContext(0);
|
||||||
@ -43,18 +43,35 @@ function MessageList({ guild, channel, before }: Props) {
|
|||||||
const logger = useLogger("MessageList.tsx");
|
const logger = useLogger("MessageList.tsx");
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [canView, setCanView] = useState(false);
|
const [canView, setCanView] = useState(false);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(true);
|
||||||
const messageGroups = channel.messages.groups;
|
const messageGroups = channel.messages.groups;
|
||||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
const { width } = useResizeObserver<HTMLDivElement>({ ref: wrapperRef });
|
const { width } = useResizeObserver<HTMLDivElement>({ ref: wrapperRef });
|
||||||
const ref = useRef<VListHandle>(null);
|
const ref = useRef<VListHandle>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ref.current?.scrollToIndex(
|
// if (before) {
|
||||||
messageGroups.reduce((p, c) => p + c.messages.length, 0) +
|
// // find message group containing the message
|
||||||
1 /* +1 to account for the spacer that adds some margin to the bottom */,
|
// const group = messageGroups.find((x) => x.messages.some((y) => y.id === before));
|
||||||
);
|
// const index = group
|
||||||
}, [messageGroups]);
|
// ? messageGroups.indexOf(group) +
|
||||||
|
// (hasMore
|
||||||
|
// ? 10
|
||||||
|
// : 0) /** +10 to account for the "skeleton" divs used to add some padding for scrolling */
|
||||||
|
// : -1;
|
||||||
|
// if (index !== -1) {
|
||||||
|
// ref.current?.scrollToIndex(index);
|
||||||
|
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this is for switching to a channel with cached messages, it ensures that we correctly set hasMore to false if there are messages but its less than 50
|
||||||
|
if (channel.messages.count > 0 && channel.messages.count < 50) setHasMore(false);
|
||||||
|
|
||||||
|
const hasSkeleton = hasMore || loading;
|
||||||
|
ref.current?.scrollToIndex(channel.messages.count + (hasSkeleton ? 30 : 0));
|
||||||
|
}, [messageGroups, hasMore, loading]);
|
||||||
|
|
||||||
const fetchMore = React.useCallback(() => {
|
const fetchMore = React.useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -76,11 +93,9 @@ function MessageList({ guild, channel, before }: Props) {
|
|||||||
const before = lastGroup.messages[0].id;
|
const before = lastGroup.messages[0].id;
|
||||||
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
||||||
channel.getMessages(app, false, 50, before).then((r) => {
|
channel.getMessages(app, false, 50, before).then((r) => {
|
||||||
if (r < 50) {
|
if (r < 50) setHasMore(false);
|
||||||
setHasMore(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
});
|
||||||
}, [channel, messageGroups]);
|
}, [channel, messageGroups]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -94,15 +109,19 @@ function MessageList({ guild, channel, before }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (guild && channel && channel.messages.count === 0) {
|
if (guild && channel && channel.messages.count === 0) {
|
||||||
channel.getMessages(app, true, 50, before).then((r) => {
|
logger.debug(`Fetching 50 messages for channel ${channel.id}`);
|
||||||
if (r < 50) {
|
channel.getMessages(app, true).then((r) => {
|
||||||
setHasMore(false);
|
if (r < 50) setHasMore(false);
|
||||||
}
|
|
||||||
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
} else if (channel.messages.count !== 0) {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
logger.debug("MessageList unmounted");
|
logger.debug("MessageList unmounted");
|
||||||
|
setLoading(true);
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setCanView(false);
|
setCanView(false);
|
||||||
};
|
};
|
||||||
@ -126,7 +145,12 @@ function MessageList({ guild, channel, before }: Props) {
|
|||||||
}}
|
}}
|
||||||
reverse
|
reverse
|
||||||
>
|
>
|
||||||
{((messageGroups.length === 0 && !loading) || (!loading && !hasMore)) && (
|
{Array.from({
|
||||||
|
length: hasMore || loading ? 30 : 0,
|
||||||
|
}).map((_, i) => (
|
||||||
|
<SkeletonLoader />
|
||||||
|
))}
|
||||||
|
{!loading && !hasMore && (
|
||||||
<EndMessageContainer>
|
<EndMessageContainer>
|
||||||
<h1 style={{ fontWeight: 700, margin: "8px 0" }}>Welcome to #{channel.name}!</h1>
|
<h1 style={{ fontWeight: 700, margin: "8px 0" }}>Welcome to #{channel.name}!</h1>
|
||||||
<p style={{ color: "var(--text-secondary)" }}>
|
<p style={{ color: "var(--text-secondary)" }}>
|
||||||
@ -135,17 +159,6 @@ function MessageList({ guild, channel, before }: Props) {
|
|||||||
<HorizontalDivider />
|
<HorizontalDivider />
|
||||||
</EndMessageContainer>
|
</EndMessageContainer>
|
||||||
)}
|
)}
|
||||||
{loading && (
|
|
||||||
<PulseLoader
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignContent: "center",
|
|
||||||
margin: 30,
|
|
||||||
}}
|
|
||||||
color="var(--primary)"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{messageGroups.map((group, index) => renderGroup(group))}
|
{messageGroups.map((group, index) => renderGroup(group))}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
</VList>
|
</VList>
|
||||||
|
@ -13,6 +13,7 @@ import GuildStore from "./GuildStore";
|
|||||||
import MessageQueue from "./MessageQueue";
|
import MessageQueue from "./MessageQueue";
|
||||||
import PresenceStore from "./PresenceStore";
|
import PresenceStore from "./PresenceStore";
|
||||||
import PrivateChannelStore from "./PrivateChannelStore";
|
import PrivateChannelStore from "./PrivateChannelStore";
|
||||||
|
import ReadStateStore from "./ReadStateStore";
|
||||||
import RoleStore from "./RoleStore";
|
import RoleStore from "./RoleStore";
|
||||||
import ThemeStore from "./ThemeStore";
|
import ThemeStore from "./ThemeStore";
|
||||||
import UpdaterStore from "./UpdaterStore";
|
import UpdaterStore from "./UpdaterStore";
|
||||||
@ -47,6 +48,7 @@ export default class AppStore {
|
|||||||
@observable rest = new REST(this);
|
@observable rest = new REST(this);
|
||||||
@observable experiments = new ExperimentsStore();
|
@observable experiments = new ExperimentsStore();
|
||||||
@observable presences = new PresenceStore(this);
|
@observable presences = new PresenceStore(this);
|
||||||
|
@observable readStateStore = new ReadStateStore(this);
|
||||||
@observable queue = new MessageQueue(this);
|
@observable queue = new MessageQueue(this);
|
||||||
@observable updaterStore: UpdaterStore | null = null;
|
@observable updaterStore: UpdaterStore | null = null;
|
||||||
|
|
||||||
|
@ -469,7 +469,7 @@ export default class GatewayConnectionStore {
|
|||||||
*/
|
*/
|
||||||
private onReady = (data: GatewayReadyDispatchData) => {
|
private onReady = (data: GatewayReadyDispatchData) => {
|
||||||
this.logger.info(`[Ready] took ${Date.now() - this.connectionStartTime!}ms`);
|
this.logger.info(`[Ready] took ${Date.now() - this.connectionStartTime!}ms`);
|
||||||
const { session_id, guilds, users, user, private_channels, sessions } = data;
|
const { session_id, guilds, users, user, private_channels, sessions, read_state } = data;
|
||||||
this.sessionId = session_id;
|
this.sessionId = session_id;
|
||||||
this.session = (sessions as GatewaySession[]).find((x) => x.session_id === session_id);
|
this.session = (sessions as GatewaySession[]).find((x) => x.session_id === session_id);
|
||||||
|
|
||||||
@ -480,8 +480,9 @@ export default class GatewayConnectionStore {
|
|||||||
if (users) {
|
if (users) {
|
||||||
this.app.users.addAll(users);
|
this.app.users.addAll(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: store relationships
|
// TODO: store relationships
|
||||||
// TODO: store readstates
|
this.app.readStateStore.addAll(read_state.entries);
|
||||||
this.app.privateChannels.addAll(private_channels);
|
this.app.privateChannels.addAll(private_channels);
|
||||||
|
|
||||||
if (data.merged_members) {
|
if (data.merged_members) {
|
||||||
|
@ -86,7 +86,7 @@ export default class MessageStore {
|
|||||||
|
|
||||||
const sortedGroups = sortedMessages
|
const sortedGroups = sortedMessages
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
|
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
|
||||||
.reduce((groups, message) => {
|
.reduce((groups, message) => {
|
||||||
const lastGroup = groups[groups.length - 1];
|
const lastGroup = groups[groups.length - 1];
|
||||||
const lastMessage = lastGroup?.messages[lastGroup.messages.length - 1];
|
const lastMessage = lastGroup?.messages[lastGroup.messages.length - 1];
|
||||||
@ -95,7 +95,7 @@ export default class MessageStore {
|
|||||||
lastMessage.author.id === message.author.id &&
|
lastMessage.author.id === message.author.id &&
|
||||||
lastMessage.type === message.type &&
|
lastMessage.type === message.type &&
|
||||||
message.type === MessageType.Default &&
|
message.type === MessageType.Default &&
|
||||||
message.timestamp.getTime() - lastMessage.timestamp.getTime() <= 10 * 60 * 1000
|
lastMessage.timestamp.getTime() - message.timestamp.getTime() <= 10 * 60 * 1000
|
||||||
) {
|
) {
|
||||||
// add to last group
|
// add to last group
|
||||||
lastGroup.messages.unshift(message);
|
lastGroup.messages.unshift(message);
|
||||||
@ -107,7 +107,8 @@ export default class MessageStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return groups;
|
return groups;
|
||||||
}, [] as MessageGroup[]);
|
}, [] as MessageGroup[])
|
||||||
|
.reverse();
|
||||||
|
|
||||||
return sortedGroups;
|
return sortedGroups;
|
||||||
}
|
}
|
||||||
|
61
src/stores/ReadStateStore.ts
Normal file
61
src/stores/ReadStateStore.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import type { APIReadState } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { ObservableMap, action, computed, observable } from "mobx";
|
||||||
|
import AppStore from "./AppStore";
|
||||||
|
import ReadState from "./objects/ReadState";
|
||||||
|
|
||||||
|
export default class ReadStateStore {
|
||||||
|
private readonly app: AppStore;
|
||||||
|
@observable readonly readstates = new ObservableMap<string, ReadState>();
|
||||||
|
|
||||||
|
constructor(app: AppStore) {
|
||||||
|
this.app = app;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
add(readstate: APIReadState) {
|
||||||
|
this.readstates.set(readstate.id, new ReadState(this.app, readstate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
update(readstate: APIReadState) {
|
||||||
|
const existing = this.readstates.get(readstate.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.update(readstate);
|
||||||
|
} else {
|
||||||
|
this.add(readstate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
addAll(readstates: APIReadState[]) {
|
||||||
|
readstates.forEach((readstate) => this.add(readstate));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a channels readstate
|
||||||
|
* @param id channel id
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
get(id: string) {
|
||||||
|
return this.readstates.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get all() {
|
||||||
|
return Array.from(this.readstates.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
remove(id: string) {
|
||||||
|
this.readstates.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get count() {
|
||||||
|
return this.readstates.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(id: string) {
|
||||||
|
return this.readstates.has(id);
|
||||||
|
}
|
||||||
|
}
|
@ -310,4 +310,14 @@ export default class Channel {
|
|||||||
|
|
||||||
return listId;
|
return listId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasUnread() {
|
||||||
|
const readState = this.app.readStateStore.get(this.id);
|
||||||
|
if (!readState) {
|
||||||
|
// this.logger.warn(`Failed to find readstate for channel ${this.id}`); // this just causes unnecessary spam
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return readState.lastMessageId !== this.lastMessageId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
src/stores/objects/ReadState.ts
Normal file
26
src/stores/objects/ReadState.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { APIReadState } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { action, observable } from "mobx";
|
||||||
|
import AppStore from "../AppStore";
|
||||||
|
|
||||||
|
export default class ReadState {
|
||||||
|
private readonly app: AppStore;
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
@observable lastMessageId: string;
|
||||||
|
@observable lastPinTimestamp: string | null;
|
||||||
|
@observable mentionCount: number | null;
|
||||||
|
|
||||||
|
constructor(app: AppStore, data: APIReadState) {
|
||||||
|
this.app = app;
|
||||||
|
|
||||||
|
this.id = data.id;
|
||||||
|
this.lastMessageId = data.last_message_id;
|
||||||
|
this.lastPinTimestamp = data.last_pin_timestamp;
|
||||||
|
this.mentionCount = data.mention_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
update(role: APIReadState) {
|
||||||
|
Object.assign(this, role);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user