/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {useLingui} from '@lingui/react/macro'; import {FlagCheckeredIcon, PushPinSlashIcon, SparkleIcon, XIcon} from '@phosphor-icons/react'; import {clsx} from 'clsx'; import {observer} from 'mobx-react-lite'; import React from 'react'; import * as ChannelPinActionCreators from '~/actions/ChannelPinsActionCreators'; import * as ModalActionCreators from '~/actions/ModalActionCreators'; import {modal} from '~/actions/ModalActionCreators'; import {ChannelTypes, MessagePreviewContext, Permissions} from '~/Constants'; import {Message} from '~/components/channel/Message'; import {LongPressable} from '~/components/LongPressable'; import {ConfirmModal} from '~/components/modals/ConfirmModal'; import FocusRing from '~/components/uikit/FocusRing/FocusRing'; import {MenuBottomSheet} from '~/components/uikit/MenuBottomSheet/MenuBottomSheet'; import {Scroller} from '~/components/uikit/Scroller'; import {Spinner} from '~/components/uikit/Spinner'; import type {ChannelRecord} from '~/records/ChannelRecord'; import type {MessageRecord} from '~/records/MessageRecord'; import ChannelPinsStore from '~/stores/ChannelPinsStore'; import ChannelStore from '~/stores/ChannelStore'; import MobileLayoutStore from '~/stores/MobileLayoutStore'; import PermissionStore from '~/stores/PermissionStore'; import ReadStateStore from '~/stores/ReadStateStore'; import {goToMessage} from '~/utils/MessageNavigator'; import previewStyles from './MessagePreview.module.css'; interface ChannelPinsContentProps { channel: ChannelRecord; onJump?: () => void; } export const ChannelPinsContent = observer(({channel, onJump}: ChannelPinsContentProps) => { const {t} = useLingui(); const pinnedPins = ChannelPinsStore.getPins(channel.id); const fetched = ChannelPinsStore.isFetched(channel.id); const hasMore = ChannelPinsStore.getHasMore(channel.id); const isLoading = ChannelPinsStore.getIsLoading(channel.id); const isDMChannel = channel.type === ChannelTypes.DM || channel.type === ChannelTypes.GROUP_DM; const canUnpin = isDMChannel || PermissionStore.can(Permissions.MANAGE_MESSAGES, channel); const mobileLayout = MobileLayoutStore; const [menuOpen, setMenuOpen] = React.useState(false); const [selectedMessage, setSelectedMessage] = React.useState(null); React.useEffect(() => { if (!fetched && !isLoading) { ChannelPinActionCreators.fetch(channel.id); } }, [fetched, isLoading, channel.id]); React.useEffect(() => { ReadStateStore.ackPins(channel.id); }, [channel.id]); const handleScroll = React.useCallback( (event: React.UIEvent) => { const target = event.currentTarget; const scrollPercentage = (target.scrollTop + target.offsetHeight) / target.scrollHeight; if (scrollPercentage > 0.8 && hasMore && !isLoading) { ChannelPinActionCreators.loadMore(channel.id); } }, [channel.id, hasMore, isLoading], ); const handleUnpin = (message: MessageRecord, event?: React.MouseEvent) => { if (event?.shiftKey) { ChannelPinActionCreators.unpin(message.channelId, message.id); } else { ModalActionCreators.push( modal(() => ( ChannelPinActionCreators.unpin(message.channelId, message.id)} /> )), ); } setMenuOpen(false); }; const renderUnpinButton = (message: MessageRecord) => { if (!canUnpin) return null; return ( ); }; const handleJump = (channelId: string, messageId: string) => { goToMessage(channelId, messageId); onJump?.(); }; const handleTap = (message: MessageRecord) => { if (mobileLayout.enabled) { handleJump(message.channelId, message.id); } }; const endStateDescription = channel.guildId ? t`Members with the "Pin Messages" permission can pin messages for everyone to see.` : t`You can pin messages in this conversation for everyone to see.`; if (pinnedPins.length === 0) { return (

{t`No Pinned Messages`}

{t`Whenever someone pins a message, it'll appear here.`}

); } return ( <> {mobileLayout.enabled &&
} {pinnedPins.slice().map(({message}) => { const cardClasses = clsx(previewStyles.previewCard, mobileLayout.enabled && previewStyles.previewCardMobile); if (mobileLayout.enabled) { return ( handleTap(message)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleTap(message); } }} onLongPress={() => { if (!canUnpin) return; setSelectedMessage(message); setMenuOpen(true); }} > ); } return (
{renderUnpinButton(message)}
); })} {isLoading && (
)} {!hasMore && (

{t`You've reached the end`}

{endStateDescription}

)} {mobileLayout.enabled && selectedMessage && ( { setMenuOpen(false); setSelectedMessage(null); }} groups={[ { items: [ { icon: , label: t`Unpin Message`, onClick: () => handleUnpin(selectedMessage), danger: true, }, ], }, ]} /> )} ); });