diff --git a/packages/discord-types/src/common/Channel.d.ts b/packages/discord-types/src/common/Channel.d.ts index 21f2dc10e..5813ee00b 100644 --- a/packages/discord-types/src/common/Channel.d.ts +++ b/packages/discord-types/src/common/Channel.d.ts @@ -332,6 +332,8 @@ export class Channel extends DiscordRecord { isMultiUserDM(): boolean; /** Whether the channel is marked as NSFW. */ isNSFW(): boolean; + /** Whether the channel details are obfuscated */ + isObfuscated(): boolean; /** * Checks if the given user is the channel owner. * @param userId User ID to check. diff --git a/packages/discord-types/src/stores/BasicGuildStore.d.ts b/packages/discord-types/src/stores/BasicGuildStore.d.ts new file mode 100644 index 000000000..0523472b5 --- /dev/null +++ b/packages/discord-types/src/stores/BasicGuildStore.d.ts @@ -0,0 +1,13 @@ +import { FluxStore, Guild } from ".."; + +export interface BasicGuild extends Pick { + discovery_splash: Guild["discoverySplash"]; + home_header: Guild["homeHeader"]; +} + +export class BasicGuildStore extends FluxStore { + getGuild(guildId: string): BasicGuild; + getGuildOrStatus(guildId: string): BasicGuild | { type: "loading"; } | { type: "failed"; } | undefined; + isGuildFetching(guildId: string): boolean; + getVersion(): number; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index 0212f37df..7608f75f8 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -9,6 +9,7 @@ export * from "./ApplicationStreamPreviewStore"; export * from "./AuthenticationStore"; export * from "./AuthorizedAppsStore"; export * from "./AuthSessionsStore"; +export * from "./BasicGuildStore"; export * from "./CallStore"; export * from "./ChannelMemberStore"; export * from "./ChannelRTCStore"; diff --git a/src/equicordplugins/betterForwards/components.tsx b/src/equicordplugins/betterForwards/components.tsx index 5c1b3c539..95e6b969f 100644 --- a/src/equicordplugins/betterForwards/components.tsx +++ b/src/equicordplugins/betterForwards/components.tsx @@ -8,11 +8,12 @@ import { BaseText } from "@components/BaseText"; import { Flex, FlexProps } from "@components/Flex"; import { RightArrow } from "@components/Icons"; import { iconsModule } from "@equicordplugins/_core/concatenatedModules"; -import { MessageAttachment } from "@vencord/discord-types"; -import { ChannelType } from "@vencord/discord-types/enums"; +import { getGuildAcronym, getIntlMessage } from "@utils/discord"; +import { getUserAvatarUrl } from "@utils/misc"; +import { BasicGuild, Guild, MessageAttachment } from "@vencord/discord-types"; import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack"; -import { ChannelStore, DateUtils, GuildStore, IconUtils, match, NavigationRouter, Popout, React, SelectedGuildStore, SnowflakeUtils, useMemo, useRef, UserStore, useStateFromStores } from "@webpack/common"; -import { PropsWithChildren } from "react"; +import { BasicGuildStore, ChannelActionCreators, ChannelStore, DateUtils, GuildStore, IconUtils, Popout, React, RelationshipStore, SelectedGuildStore, SnowflakeUtils, useEffect, useMemo, useRef, UserStore, useStateFromStores } from "@webpack/common"; +import { PropsWithChildren, ReactNode } from "react"; import { cl, ForwardOptionsContext, ForwardOptionsState } from "."; @@ -20,25 +21,40 @@ type AttachmentType = "IMAGE" | "VIDEO" | "CLIP" | "AUDIO" | "VISUAL_PLACEHOLDER const tagClasses = findCssClassesLazy("tagList", "tagGroup", "tag"); const ServerProfileComponent = findComponentByCodeLazy("{guildProfile:", "GUILD_PROFILE"); -const getAttachmentType: (attachment: MessageAttachment, inlineAttachmentMedia?: boolean) => AttachmentType = findByCodeLazy('"PLAINTEXT_PREVIEW":"OTHER"'); +const getAttachmentType = findByCodeLazy('"PLAINTEXT_PREVIEW":"OTHER"'); +const formatChannelName = findByCodeLazy("#{intl::NO_ACCESS}", "isObfuscated()"); +const getChannelIcon = findByCodeLazy("textFocused:", "isGameInvitesChannel()"); +const navigateTo = findByCodeLazy('getConfig({location:"channel_mention"})'); +const fetchBasicGuild = findByCodeLazy('type:"BASIC_GUILD_FETCH_SUCCESS"'); export function GuildName({ guildId }: { guildId: string; }) { - const guild = useStateFromStores( - [GuildStore, SelectedGuildStore], - () => { - const current = SelectedGuildStore.getGuildId(); - return current !== guildId ? GuildStore.getGuild(guildId) : null; - }, + const currentGuildId = useStateFromStores([SelectedGuildStore], () => SelectedGuildStore.getGuildId(), []); + const guild: Guild | BasicGuild | undefined = useStateFromStores( + [GuildStore, BasicGuildStore], + () => GuildStore.getGuild(guildId) ?? BasicGuildStore.getGuild(guildId), [guildId] ); + const icon = useMemo(() => { - if (!guild?.icon) return null; - return IconUtils.getGuildIconURL({ id: guildId, icon: guild.icon, canAnimate: true, size: 16 }); - }, [guildId, guild?.icon]); + if (!guild) return null; + + return guild.icon ? ( + {`Server + ) : ( +
{getGuildAcronym(guild)}
+ ); + }, [guild]); + const guildDivRef = useRef(null); + useEffect(() => void fetchBasicGuild(guildId), [guildId]); + return ( - guild && ( + currentGuildId !== guildId && ( } @@ -46,11 +62,11 @@ export function GuildName({ guildId }: { guildId: string; }) { > {popoutProps => (
- {icon && {`Server} + {icon} {guild ? guild.name : "View server"} - +
)}
@@ -59,45 +75,61 @@ export function GuildName({ guildId }: { guildId: string; }) { } export function ChannelName({ guildId, channelId, messageId }: { guildId?: string; channelId: string; messageId: string; }) { - const name = useStateFromStores( - [ChannelStore, UserStore], - () => { - const channel = ChannelStore.getChannel(channelId); - if (!channel) return null; - - return match(channel.type) - .with(ChannelType.DM, () => { - const user = UserStore.getUser(channel.recipients[0]); - return `@${user.globalName || user.username}`; - }) - .with(ChannelType.GROUP_DM, () => { - if (channel.name) return channel.name; - const users = channel.recipients.map(r => UserStore.getUser(r)); - return users.map(u => u.globalName || u.username).join(", "); - }) - .with( - ChannelType.ANNOUNCEMENT_THREAD, - ChannelType.PRIVATE_THREAD, - ChannelType.PUBLIC_THREAD, - () => channel.name - ) - .otherwise(() => `#${channel.name}`); - }, - [channelId] + const channel = useStateFromStores([ChannelStore], () => ChannelStore.getChannel(channelId), [channelId]); + const name: ReactNode = useStateFromStores( + [UserStore, RelationshipStore], + () => + channel ? ( + formatChannelName(channel, UserStore, RelationshipStore, false, false) + ) : ( + {guildId ? getIntlMessage("UNKNOWN_CHANNEL").toLowerCase() : getIntlMessage("UNKNOWN_USER")} + ), + [channel, guildId] ); + const icon = useMemo(() => { + if (channel?.isDM()) { + const [user] = channel.recipients.map(UserStore.getUser).filter(Boolean); + if (user) + return ( + {`DM + ); + } + + if (channel?.isGroupDM()) { + return ( + {`Group + ); + } + + const Icon = (channel && getChannelIcon(channel)) ?? (guildId ? iconsModule.TextIcon : iconsModule.AtIcon); + return ; + }, [channel, guildId, name]); + return ( - name && ( -
NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}/${messageId}`)} - > - - {name} - - -
- ) +
navigateTo(guildId ?? "@me", channelId, messageId)} + onMouseEnter={() => ChannelActionCreators.preload(guildId ?? "@me", channelId)} + > + {icon} + + {name} + + +
); } @@ -118,18 +150,19 @@ export function Timestamp({ snowflake }: { snowflake: string; }) { export function ForwardPicker() { const state = React.useContext(ForwardOptionsContext); + const { message } = state; - if (state.message.embeds.length + state.message.attachments.length === 0) return null; + if (!message || message.embeds.length + message.attachments.length === 0) return null; return ( - {state.message.attachments.length > 0 && } - {state.message.embeds.length > 0 && } + {message.attachments.length > 0 && } + {message.embeds.length > 0 && } ); } -export function EmbedPicker({ message, opts, setOpts, hasOpts, defaultOpts }: ForwardOptionsState) { +export function EmbedPicker({ message, opts, setOpts, hasOpts, defaultOpts }: Required) { const embeds = useMemo(() => { let id = 0; return message.embeds.map(({ rawTitle, rawDescription, image, images = image ? [image] : [], video }, i) => { @@ -158,7 +191,7 @@ export function EmbedPicker({ message, opts, setOpts, hasOpts, defaultOpts }: Fo const { EmbedIcon, ImageIcon } = iconsModule; - return embeds.map(({ title, subEmbeds }) => ( + return embeds?.map(({ title, subEmbeds }) => ( setOpts(prev => ({ ...prev, onlyEmbedIndices: data }))} + onChange={onlyEmbedIndices => setOpts(prev => ({ ...prev, onlyEmbedIndices }))} disabled={!hasOpts} > {Icon && } @@ -189,7 +222,7 @@ export function EmbedPicker({ message, opts, setOpts, hasOpts, defaultOpts }: Fo )); } -export function AttachmentPicker({ message, opts, setOpts, hasOpts, defaultOpts }: ForwardOptionsState) { +export function AttachmentPicker({ message, opts, setOpts, hasOpts, defaultOpts }: Required) { return ( {message.attachments.map(attachment => ( @@ -197,7 +230,7 @@ export function AttachmentPicker({ message, opts, setOpts, hasOpts, defaultOpts key={attachment.id} id={attachment.id} source={hasOpts ? opts.onlyAttachmentIds ?? [] : defaultOpts.onlyAttachmentIds} - onChange={data => setOpts(prev => ({ ...prev, onlyAttachmentIds: data }))} + onChange={onlyAttachmentIds => setOpts(prev => ({ ...prev, onlyAttachmentIds }))} disabled={!hasOpts} > @@ -219,7 +252,7 @@ function Tag({ id, children, source, onChange, disabled }: { id: T; source: T
onChange(selected ? source.filter(x => x !== id) : [...source, id])} style={{ textWrap: "wrap", opacity: disabled ? .5 : undefined }} inert={disabled} @@ -229,7 +262,7 @@ function Tag({ id, children, source, onChange, disabled }: { id: T; source: T ); } -const icons: Partial> = { +const attachmentIcons: Partial> = { IMAGE: "Image", VIDEO: "Video", CLIP: "Clips", @@ -240,7 +273,7 @@ const icons: Partial> = { function AttachmentIcon({ attachment }: { attachment: MessageAttachment; }) { const Icon = useMemo(() => { const type = getAttachmentType(attachment, true); - return iconsModule[(icons[type] ?? "ImageFile") + "Icon"]; + return iconsModule[(attachmentIcons[type] ?? "ImageFile") + "Icon"]; }, [attachment]); return Icon && ; diff --git a/src/equicordplugins/betterForwards/index.tsx b/src/equicordplugins/betterForwards/index.tsx index 8b23798a9..693a355a7 100644 --- a/src/equicordplugins/betterForwards/index.tsx +++ b/src/equicordplugins/betterForwards/index.tsx @@ -34,16 +34,15 @@ export interface ForwardOptionsState { setOpts: Dispatch>; defaultOpts: Required; hasOpts: boolean; - message: Message; + message?: Message; } export const ForwardOptionsContext = proxyLazyWebpack(() => - React.createContext({ + React.createContext({ opts: {}, - setOpts: () => {}, + setOpts: () => { }, defaultOpts: { onlyAttachmentIds: [], onlyEmbedIndices: [] }, hasOpts: false, - message: {} as Message }) ); @@ -159,7 +158,7 @@ export default definePlugin({ } ], - async sendForward(additionalMessage: string | null, channels: { id: string; type: string }[], message: Message, options: ForwardOptions) { + async sendForward(additionalMessage: string | null, channels: { id: string; type: string; }[], message: Message, options: ForwardOptions) { const contentMessage = message.messageSnapshots[0]?.message ?? message; const newLine = `\n${settings.store.forwardPreface} `; @@ -199,7 +198,7 @@ export default definePlugin({ ignore = !!event?.shiftKey; }, - renderForwardFooter({ message }: { message: Message }) { + renderForwardFooter({ message }: { message: Message; }) { if (!message.messageReference) return null; const { guild_id, channel_id, message_id } = message.messageReference; @@ -215,8 +214,9 @@ export default definePlugin({ ); }, - useProps(props: { message: Message; forwardOptions?: ForwardOptions }) { - const message = props.message.messageSnapshots[0]?.message ?? props.message; + useProps(props: { message?: Message; forwardOptions?: ForwardOptions; }) { + // Message is undefined in certain forward contexts (e.g. forwarding an item from the shop) + const message = props.message?.messageSnapshots[0]?.message ?? props.message; const [opts, setOpts] = useState(() => { if (!props.forwardOptions || !props.forwardOptions.onlyEmbedIndices) @@ -226,18 +226,18 @@ export default definePlugin({ const embedsIds = new Set(props.forwardOptions.onlyEmbedIndices as number[]); // Discord incorrectly assumes that embed indices directly map to whole embeds, this is an attempt to fix that - const onlyEmbedIndices = message.embeds + const onlyEmbedIndices = message?.embeds .flatMap((e, i) => e.images?.map(() => ({ id: id++, eId: i })) ?? { id: id++, eId: i }) .filter(({ eId }) => embedsIds.has(eId)) - .map(({ id }) => id); + .map(({ id }) => id) ?? []; return { ...props.forwardOptions, onlyEmbedIndices }; }); const defaultOpts = useMemo( () => ({ - onlyAttachmentIds: message.attachments.map(a => a.id), - onlyEmbedIndices: message.embeds.flatMap(e => e.images ?? [{}]).map((_, i) => i) + onlyAttachmentIds: message?.attachments.map(a => a.id) ?? [], + onlyEmbedIndices: message?.embeds.flatMap(e => e.images ?? [{}]).map((_, i) => i) ?? [] }), [message] ); @@ -272,7 +272,7 @@ export default definePlugin({ {children} - {message.embeds.length + message.attachments.length > 0 && ( + {message && message.embeds.length + message.attachments.length > 0 && ( setOpts(!hasOpts ? defaultOpts : {})} size={20}> Forward everything diff --git a/src/equicordplugins/betterForwards/style.css b/src/equicordplugins/betterForwards/style.css index beb364cc6..fa88d6009 100644 --- a/src/equicordplugins/betterForwards/style.css +++ b/src/equicordplugins/betterForwards/style.css @@ -8,10 +8,32 @@ color: var(--text-muted); } - .vc-betterforwards-guild-icon { - margin-block: 0; - border-radius: 4px; + .vc-betterforwards-guild-icon, + .vc-betterforwards-user-icon, + .vc-betterforwards-guild-acronym { + margin-right: 2px; width: 16px; + height: 16px; + flex-shrink: 0; + } + + .vc-betterforwards-guild-icon { + border-radius: 4px; + } + + .vc-betterforwards-user-icon { + border-radius: 9999px; + } + + .vc-betterforwards-guild-acronym { + line-height: 16px; + text-align: center; + overflow: hidden; + font-size: 9.6px; + border-radius: 4px; + white-space: nowrap; + background-color: var(--background-base-lower); + color: var(--text-default); } .vc-betterforwards-footer-element { @@ -39,6 +61,12 @@ padding: 1px 0 1px 2px; border-radius: 4px; vertical-align: middle; + color: var(--text-muted); + + svg, + img { + flex-shrink: 0; + } } } @@ -48,6 +76,26 @@ text-overflow: ellipsis; } -[class^="forwardPreview"] > [class^="contentWrapper"] { - overflow: hidden; -} \ No newline at end of file +[class^="forwardPreview"] { + height: fit-content; + + > [class^="contentWrapper"] { + max-height: 250px; + overflow-y: auto; + scrollbar-width: thin; + + > [class^="forwardPreviewMessage"] { + flex-shrink: 0; + } + + [class^="tagGroup"] > [class^="tag"] { + text-wrap: nowrap !important; + min-width: 0; + + .vc-text-base { + text-overflow: ellipsis; + overflow: hidden; + } + } + } +} diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index 4ea285fa6..27fb209d7 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -107,6 +107,7 @@ export let ReferencedMessageStore: t.ReferencedMessageStore; export let SessionsStore: t.SessionsStore; export let GuildAvailabilityStore: t.GuildAvailabilityStore; export let UserGuildJoinRequestStore: t.UserGuildJoinRequestStore; +export let BasicGuildStore: t.BasicGuildStore; /** * @see jsdoc of {@link t.useStateFromStores} @@ -179,14 +180,13 @@ waitForStore("AuthSessionsStore", m => AuthSessionsStore = m); waitForStore("ClientThemesBackgroundStore", m => ClientThemesBackgroundStore = m); waitForStore("ConnectedAccountsStore", m => ConnectedAccountsStore = m); waitForStore("ChannelMemberStore", m => ChannelMemberStore = m); -// comeback waitForStore("ThreadMemberListStore", m => ThreadMemberListStore = m); waitForStore("CollapsedVoiceChannelStore", m => CollapsedVoiceChannelStore = m); waitForStore("ReferencedMessageStore", m => ReferencedMessageStore = m); -// comeback waitForStore("SessionsStore", m => SessionsStore = m); waitForStore("GuildAvailabilityStore", m => GuildAvailabilityStore = m); waitForStore("UserGuildJoinRequestStore", m => UserGuildJoinRequestStore = m); +waitForStore("BasicGuildStore", m => BasicGuildStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly causes all webpack commons to be imported, which can easily cause circular dependencies.