Equicord/src/equicordplugins/ghosted/Boo.tsx

205 lines
7 KiB
TypeScript

/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Channel, Message } from "@vencord/discord-types";
import { findCssClassesLazy } from "@webpack";
import { MessageStore, useEffect, UserStore, useState, useStateFromStores } from "@webpack/common";
import { cl, settings } from ".";
import { IconGhost } from "./IconGhost";
function isChannelExempted(channel: Channel): boolean {
const exemptList = settings.store.exemptedChannels
.split(",")
.map(id => id.trim())
.filter(id => id.length > 0);
const isGroupDmsExempted = settings.store.ignoreGroupDms && channel.isGroupDM();
return exemptList.includes(channel.id) || isGroupDmsExempted;
}
const countedChannels = new Set<string>();
// track channels that were manually cleared and the message ID at time of clear
const clearedChannels = new Map<string, string>();
// listeners for when a channel is cleared or un-cleared (thororen is this allowed lolz)
const clearedChannelListeners = new Set<(channelId: string) => void>();
let _booCount = 0;
const listeners = new Set<(n: number) => void>();
export function getBooCount() {
return _booCount;
}
export function setBooCount(n: number) {
_booCount = n;
for (const l of listeners) l(_booCount);
}
export function onBooCountChange(cb: (n: number) => void) {
listeners.add(cb);
return () => {
listeners.delete(cb);
};
}
export function onClearedChannelChange(cb: (channelId: string) => void) {
clearedChannelListeners.add(cb);
return () => {
clearedChannelListeners.delete(cb);
};
}
export function getGhostedChannels(): string[] {
return Array.from(countedChannels);
}
export function clearChannelFromGhost(channelId: string): void {
if (!countedChannels.has(channelId)) {
return;
}
countedChannels.delete(channelId);
setBooCount(getBooCount() - 1);
// so we can detect new messages from the other person
const lastMessage = MessageStore.getMessages(channelId)?.last();
if (lastMessage) {
clearedChannels.set(channelId, lastMessage.id);
}
// notify all listeners that this channel was cleared
for (const listener of clearedChannelListeners) {
listener(channelId);
}
}
export function isChannelCleared(channelId: string): boolean {
return clearedChannels.has(channelId);
}
const ChannelWrapperStyles = findCssClassesLazy("muted", "wrapper");
export function Boo({ channel }: { channel: Channel; }) {
const { id } = channel;
const currentUserId = useStateFromStores([UserStore], () => UserStore.getCurrentUser()?.id);
const lastMessage: Message = useStateFromStores([MessageStore], () =>
MessageStore.getMessages(id)?.last()
);
const [state, setState] = useState({
isCurrentUser: null as boolean | null,
containsQuestionMark: false,
isDataProcessed: false,
});
const [isCleared, setIsCleared] = useState(false);
const lastMessageTimestampMs = lastMessage ? new Date(lastMessage.timestamp).getTime() : 0;
const isInactive = !!lastMessage && settings.store.maxInactiveTimeMs > 0 && Number.isFinite(lastMessageTimestampMs) && Date.now() - lastMessageTimestampMs > settings.store.maxInactiveTimeMs;
useEffect(() => {
if (!lastMessage || !currentUserId) return;
const lastIsCurrentUser = lastMessage.author.id === currentUserId;
const containsQuestionMark = !lastIsCurrentUser && lastMessage.content.includes("?");
setState({
isCurrentUser: lastIsCurrentUser,
containsQuestionMark,
isDataProcessed: true,
});
}, [lastMessage, currentUserId]);
// track if this channel was manually cleared
useEffect(() => {
setIsCleared(clearedChannels.has(id));
// subscribe to cleared channel changes for instant visual updates
const unsubscribe = onClearedChannelChange(clearedChannelId => {
if (clearedChannelId === id) {
// check current state: if it's still in clearedChannels, it was cleared; otherwise un-cleared
setIsCleared(clearedChannels.has(id));
}
});
return unsubscribe;
}, [id, lastMessage?.id]);
useEffect(() => {
if (!state.isDataProcessed) return;
const isExempted = isChannelExempted(channel);
let wasManuallyCleared = clearedChannels.has(id);
// if manually cleared, check if there's a NEW message from the other person
if (wasManuallyCleared && !state.isCurrentUser) {
const clearedAtMessageId = clearedChannels.get(id);
const currentLastMessageId = lastMessage?.id;
// if it's the same message, stay cleared (don't re-ghost)
if (clearedAtMessageId === currentLastMessageId) {
return;
}
// if there's a NEW message from the OTHER person, remove from cleared state
// so it can be re-ghosted
if (currentLastMessageId !== clearedAtMessageId) {
clearedChannels.delete(id);
wasManuallyCleared = false; // update the flag since we deleted it
// notify listeners that this channel is no longer cleared (new message)
for (const listener of clearedChannelListeners) {
listener(id);
}
}
}
// if the current user responded, clear all tracking
if (state.isCurrentUser) {
if (countedChannels.has(id)) {
countedChannels.delete(id);
setBooCount(getBooCount() - 1);
}
if (clearedChannels.has(id)) {
clearedChannels.delete(id);
}
return;
}
// if exempted or bot (if setting enabled), remove from ghost tracking
if (isExempted || (settings.store.ignoreBots && lastMessage.author.bot) || isInactive) {
if (countedChannels.has(id)) {
countedChannels.delete(id);
setBooCount(getBooCount() - 1);
}
return;
}
// if manually cleared, don't add back to ghost count
if (wasManuallyCleared) {
return;
}
// normal ghosting logic: last message is from other person
if (!state.isCurrentUser) {
if (!countedChannels.has(id)) {
countedChannels.add(id);
setBooCount(getBooCount() + 1);
}
}
}, [state.isCurrentUser, state.isDataProcessed, id, lastMessage?.id, isInactive]);
if (!state.isDataProcessed || !currentUserId || !lastMessage || state.isCurrentUser || isChannelExempted(channel) || isCleared || (settings.store.ignoreBots && lastMessage.author.bot) || isInactive)
return null;
if (!settings.store.showDmIcons) return null;
return (
<div className={cl("icon", ChannelWrapperStyles.wrapper)}>
<IconGhost fill={state.containsQuestionMark ? "#ff8000" : "currentColor"} />
</div>
);
}