// SPDX-License-Identifier: AGPL-3.0-or-later import Authentication from '@app/features/auth/state/Authentication'; import styles from '@app/features/channel/components/ChannelSourcePreview.module.css'; import type {Channel} from '@app/features/channel/models/Channel'; import Channels from '@app/features/channel/state/Channels'; import * as ChannelUtils from '@app/features/channel/utils/ChannelUtils'; import {getGroupDMAccentColor} from '@app/features/channel/utils/GroupDMColorUtils'; import {GuildIcon} from '@app/features/guild/components/popouts/GuildIcon'; import Guilds from '@app/features/guild/state/Guilds'; import {DIRECT_MESSAGE_DESCRIPTOR, PERSONAL_NOTES_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {Link} from '@app/features/platform/components/router/RouterReact'; import {remFromPx} from '@app/features/theme/layout/RemFromPx'; import {MentionBadge} from '@app/features/ui/components/MentionBadge'; import FocusRing from '@app/features/ui/focus_ring/FocusRing'; import Users from '@app/features/user/state/Users'; import * as AvatarUtils from '@app/features/user/utils/AvatarUtils'; import {MEDIA_PROXY_ICON_SIZE_DEFAULT} from '@fluxer/constants/src/MediaProxyAssetSizes'; import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes'; import {msg, plural} from '@lingui/core/macro'; import {useLingui} from '@lingui/react/macro'; import {CaretRightIcon, HashIcon, NotePencilIcon, UserIcon, UsersIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import type React from 'react'; import {useMemo} from 'react'; const CHANNEL_DESCRIPTOR = msg({ message: 'Channel', comment: 'Short label in the channel source preview. Keep it concise.', }); const GROUP_DESCRIPTOR = msg({ message: 'Group', comment: 'Short label in the channel source preview. Keep it concise.', }); const JUMP_TO_DESCRIPTOR = msg({ message: 'Jump to {displayName}', comment: 'Short label in the channel source preview. Keep it concise. Preserve {displayName}; it is inserted by code.', }); type SubtextTone = 'muted' | 'primary'; interface ChannelAvatarFallbackLabels { channel: string; directMessage: string; group: string; personalNotes: string; } interface ChannelSourcePreviewProps { channel: Channel; onClick?: () => void; linkTo?: string; onContextMenu?: (event: React.MouseEvent) => void; mentionCount?: number; avatarSize?: MediaProxyImageSize; variant?: 'default' | 'inline'; showChannelIcon?: boolean; className?: string; } function getChannelDisplayName(channel: Channel): string { if (channel.isPrivate()) { return ChannelUtils.getDMDisplayName(channel); } return channel.name?.trim() || ChannelUtils.getName(channel); } function getGroupDmMemberCount(channel: Channel): number { const currentUserId = Authentication.currentUserId; if (!currentUserId) return channel.recipientIds.length; const memberIds = new Set(channel.recipientIds); memberIds.add(currentUserId); return memberIds.size; } function renderAvatarImage(props: {url: string; label: string; size: number}): React.ReactNode { const {url, label, size} = props; return (
); } function renderAvatarFallback(props: { label: string; size: number; background?: string; content: React.ReactNode; }): React.ReactNode { const {label, size, background, content} = props; return (
{content}
); } function renderChannelAvatar( channel: Channel, size: MediaProxyImageSize, fallbackLabels: ChannelAvatarFallbackLabels, ): React.ReactNode { if (channel.guildId) { const guild = Guilds.getGuild(channel.guildId); if (!guild) { return renderAvatarFallback({ label: fallbackLabels.channel, size, content: ( ), }); } return ( ); } if (channel.isDM()) { const recipientId = channel.getRecipientId(); const recipient = recipientId ? Users.getUser(recipientId) : null; if (recipient) { const url = AvatarUtils.getUserAvatarURL({id: recipient.id, avatar: recipient.avatar}, false); return renderAvatarImage({url, label: recipient.tag, size}); } return renderAvatarFallback({ label: fallbackLabels.directMessage, size, content: ( ), }); } if (channel.isGroupDM()) { const iconUrl = AvatarUtils.getChannelIconURL( {id: channel.id, icon: channel.icon}, MEDIA_PROXY_ICON_SIZE_DEFAULT, true, ); if (iconUrl) { return renderAvatarImage({url: iconUrl, label: fallbackLabels.group, size}); } const accentColor = getGroupDMAccentColor(channel.id); return renderAvatarFallback({ label: fallbackLabels.group, size, background: accentColor, content: ( ), }); } if (channel.isPersonalNotes()) { return renderAvatarFallback({ label: fallbackLabels.personalNotes, size, content: ( ), }); } return renderAvatarFallback({ label: fallbackLabels.channel, size, content: ( ), }); } export const ChannelSourcePreview = observer(function ChannelSourcePreview({ channel, onClick, linkTo, onContextMenu, mentionCount, avatarSize, variant = 'default', showChannelIcon = true, className, }: ChannelSourcePreviewProps) { const {i18n} = useLingui(); const guild = channel.guildId ? (Guilds.getGuild(channel.guildId) ?? null) : null; const category = channel.parentId ? (Channels.getChannel(channel.parentId) ?? null) : null; const isGroupDm = channel.isGroupDM(); const isUserDm = channel.isDM(); const channelDisplayName = getChannelDisplayName(channel); const displayName = isUserDm ? `@${channelDisplayName}` : channelDisplayName; const resolvedAvatarSize: MediaProxyImageSize = avatarSize ?? (variant === 'inline' ? 24 : 32); const avatarFallbackLabels = useMemo( () => ({ channel: i18n._(CHANNEL_DESCRIPTOR), directMessage: i18n._(DIRECT_MESSAGE_DESCRIPTOR), group: i18n._(GROUP_DESCRIPTOR), personalNotes: i18n._(PERSONAL_NOTES_DESCRIPTOR), }), [i18n.locale], ); const {subtext, subtextTone} = useMemo(() => { if (isGroupDm) { const memberCount = getGroupDmMemberCount(channel); const label = plural( {count: memberCount}, { one: '# member', other: '# members', }, ); return {subtext: label, subtextTone: 'primary' as SubtextTone}; } if (guild) { if (category?.name) { return { subtext: ( {guild.name} {category.name} ), subtextTone: 'muted' as SubtextTone, }; } return {subtext: guild.name, subtextTone: 'muted' as SubtextTone}; } return {subtext: null, subtextTone: 'muted' as SubtextTone}; }, [category?.name, channel, guild, isGroupDm, i18n]); const nameContent = ( {displayName} ); const nameAriaLabel = i18n._(JUMP_TO_DESCRIPTOR, {displayName}); const renderNameAction = () => { if (linkTo) { return ( {nameContent} ); } if (onClick) { return ( ); } return nameContent; }; const containerInteractionProps = onContextMenu ? {onContextMenu} : {}; return (
{renderChannelAvatar(channel, resolvedAvatarSize, avatarFallbackLabels)}
{showChannelIcon && channel.guildId ? ChannelUtils.getIcon(channel, {className: styles.channelIcon, weight: 'bold'}) : null} {renderNameAction()} {mentionCount != null && mentionCount > 0 ? ( ) : null}
{subtext ? (
{subtext}
) : null}
); });