initial commit
This commit is contained in:
256
fluxer_app/src/components/shared/ChannelPinsContent.tsx
Normal file
256
fluxer_app/src/components/shared/ChannelPinsContent.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<MessageRecord | null>(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<HTMLDivElement>) => {
|
||||
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(() => (
|
||||
<ConfirmModal
|
||||
title={t`Unpin Message`}
|
||||
description={t`Do you want to send this pin back in time?`}
|
||||
message={message}
|
||||
primaryText={t`Unpin it`}
|
||||
onPrimary={() => ChannelPinActionCreators.unpin(message.channelId, message.id)}
|
||||
/>
|
||||
)),
|
||||
);
|
||||
}
|
||||
setMenuOpen(false);
|
||||
};
|
||||
|
||||
const renderUnpinButton = (message: MessageRecord) => {
|
||||
if (!canUnpin) return null;
|
||||
return (
|
||||
<FocusRing offset={-2}>
|
||||
<button
|
||||
type="button"
|
||||
className={previewStyles.actionIconButton}
|
||||
onClick={(event) => handleUnpin(message, event)}
|
||||
>
|
||||
<XIcon weight="regular" className={previewStyles.actionIcon} />
|
||||
</button>
|
||||
</FocusRing>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className={previewStyles.emptyState}>
|
||||
<div className={previewStyles.emptyStateContent}>
|
||||
<SparkleIcon className={previewStyles.emptyStateIcon} />
|
||||
<div className={previewStyles.emptyStateTextContainer}>
|
||||
<h3 className={previewStyles.emptyStateTitle}>{t`No Pinned Messages`}</h3>
|
||||
<p
|
||||
className={previewStyles.emptyStateDescription}
|
||||
>{t`Whenever someone pins a message, it'll appear here.`}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Scroller
|
||||
className={clsx(previewStyles.scroller, mobileLayout.enabled && previewStyles.scrollerMobile)}
|
||||
key="channel-pins-scroller"
|
||||
onScroll={handleScroll}
|
||||
reserveScrollbarTrack={false}
|
||||
>
|
||||
{mobileLayout.enabled && <div className={previewStyles.topSpacer} />}
|
||||
{pinnedPins.slice().map(({message}) => {
|
||||
const cardClasses = clsx(previewStyles.previewCard, mobileLayout.enabled && previewStyles.previewCardMobile);
|
||||
|
||||
if (mobileLayout.enabled) {
|
||||
return (
|
||||
<LongPressable
|
||||
key={message.id}
|
||||
className={cardClasses}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleTap(message)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
handleTap(message);
|
||||
}
|
||||
}}
|
||||
onLongPress={() => {
|
||||
if (!canUnpin) return;
|
||||
setSelectedMessage(message);
|
||||
setMenuOpen(true);
|
||||
}}
|
||||
>
|
||||
<Message
|
||||
message={message}
|
||||
channel={ChannelStore.getChannel(message.channelId)!}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
/>
|
||||
</LongPressable>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={message.id} className={cardClasses} role="button" tabIndex={-1}>
|
||||
<Message
|
||||
message={message}
|
||||
channel={ChannelStore.getChannel(message.channelId)!}
|
||||
previewContext={MessagePreviewContext.LIST_POPOUT}
|
||||
/>
|
||||
<div className={previewStyles.actionButtons}>
|
||||
<FocusRing offset={-2}>
|
||||
<button
|
||||
type="button"
|
||||
className={previewStyles.actionButton}
|
||||
onClick={() => handleJump(message.channelId, message.id)}
|
||||
>
|
||||
{t`Jump`}
|
||||
</button>
|
||||
</FocusRing>
|
||||
{renderUnpinButton(message)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isLoading && (
|
||||
<div className={previewStyles.loadingState}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasMore && (
|
||||
<div className={previewStyles.endState}>
|
||||
<div className={previewStyles.endStateContent}>
|
||||
<FlagCheckeredIcon className={previewStyles.endStateIcon} />
|
||||
<div className={previewStyles.endStateTextContainer}>
|
||||
<h3 className={previewStyles.endStateTitle}>{t`You've reached the end`}</h3>
|
||||
<p className={previewStyles.endStateDescription}>{endStateDescription}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Scroller>
|
||||
|
||||
{mobileLayout.enabled && selectedMessage && (
|
||||
<MenuBottomSheet
|
||||
isOpen={menuOpen}
|
||||
onClose={() => {
|
||||
setMenuOpen(false);
|
||||
setSelectedMessage(null);
|
||||
}}
|
||||
groups={[
|
||||
{
|
||||
items: [
|
||||
{
|
||||
icon: <PushPinSlashIcon className={previewStyles.menuIcon} />,
|
||||
label: t`Unpin Message`,
|
||||
onClick: () => handleUnpin(selectedMessage),
|
||||
danger: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--background-modifier-accent);
|
||||
background-color: var(--background-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image {
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
width: 100%;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px dashed var(--background-modifier-accent);
|
||||
background-color: var(--background-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary-muted);
|
||||
text-align: center;
|
||||
min-height: 80px;
|
||||
}
|
||||
91
fluxer_app/src/components/shared/ImagePreviewField.tsx
Normal file
91
fluxer_app/src/components/shared/ImagePreviewField.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import styles from './ImagePreviewField.module.css';
|
||||
|
||||
export interface ImagePreviewFieldProps {
|
||||
imageUrl: string | null | undefined;
|
||||
showPlaceholder: boolean;
|
||||
placeholderText: React.ReactNode;
|
||||
altText: string;
|
||||
aspectRatio?: string | number;
|
||||
className?: string;
|
||||
objectFit?: 'cover' | 'contain';
|
||||
}
|
||||
|
||||
export const ImagePreviewField: React.FC<ImagePreviewFieldProps> = ({
|
||||
imageUrl,
|
||||
showPlaceholder,
|
||||
placeholderText,
|
||||
altText,
|
||||
aspectRatio,
|
||||
className,
|
||||
objectFit = 'cover',
|
||||
}) => {
|
||||
const innerContainerStyle: React.CSSProperties = aspectRatio
|
||||
? {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
paddingBottom: `${(1 / Number(aspectRatio)) * 100}%`,
|
||||
}
|
||||
: {};
|
||||
|
||||
const imageContainerStyle: React.CSSProperties = aspectRatio
|
||||
? {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}
|
||||
: {};
|
||||
|
||||
const imageStyle: React.CSSProperties = {
|
||||
objectFit,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
};
|
||||
|
||||
if (showPlaceholder || !imageUrl) {
|
||||
return (
|
||||
<div className={`${styles.placeholder} ${className ?? ''}`} style={aspectRatio ? innerContainerStyle : {}}>
|
||||
{aspectRatio ? (
|
||||
<div style={imageContainerStyle}>
|
||||
<span>{placeholderText}</span>
|
||||
</div>
|
||||
) : (
|
||||
<span>{placeholderText}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${styles.preview} ${className ?? ''}`} style={aspectRatio ? innerContainerStyle : {}}>
|
||||
{aspectRatio ? (
|
||||
<div style={imageContainerStyle}>
|
||||
<img src={imageUrl} alt={altText} className={styles.image} style={imageStyle} />
|
||||
</div>
|
||||
) : (
|
||||
<img src={imageUrl} alt={altText} className={styles.image} style={imageStyle} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.channelHeader {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.channelHeader:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.channelHeaderCompact {
|
||||
margin-top: 0.5rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.channelIcon {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.channelIconAvatar {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.channelIconAvatarImage {
|
||||
height: 1.25rem;
|
||||
width: 1.25rem;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.channelNameButton {
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-family: inherit;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.channelNameButton:hover,
|
||||
.channelNameButton:focus-visible {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.channelNameButton:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.channelNameText {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.channelNamePrimary {
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.channelScopeRow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.channelScopeGuildIcon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
--guild-icon-size: 0.75rem;
|
||||
}
|
||||
|
||||
.channelScopeGuildName {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.channelScopeChevron {
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.channelScopeChannelInfo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.channelScopeChannelIcon {
|
||||
height: 0.75rem;
|
||||
width: 0.75rem;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.channelScopeChannelName {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.focusRingTight {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {CaretRightIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import type React from 'react';
|
||||
import {GroupDMAvatar} from '~/components/common/GroupDMAvatar';
|
||||
import {GuildIcon} from '~/components/popouts/GuildIcon';
|
||||
import {Avatar} from '~/components/uikit/Avatar';
|
||||
import FocusRing from '~/components/uikit/FocusRing/FocusRing';
|
||||
import type {ChannelRecord} from '~/records/ChannelRecord';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import * as ChannelUtils from '~/utils/ChannelUtils';
|
||||
import styles from './MessageContextPrefix.module.css';
|
||||
|
||||
const getChannelDisplayName = (channel: ChannelRecord): string => {
|
||||
if (channel.isPrivate()) {
|
||||
return ChannelUtils.getDMDisplayName(channel);
|
||||
}
|
||||
return channel.name?.trim() || ChannelUtils.getName(channel);
|
||||
};
|
||||
|
||||
const renderChannelIcon = (channel: ChannelRecord): React.ReactNode => {
|
||||
if (channel.isPersonalNotes()) {
|
||||
return ChannelUtils.getIcon(channel, {className: styles.channelIcon});
|
||||
}
|
||||
|
||||
if (channel.isDM()) {
|
||||
const recipientId = channel.recipientIds[0];
|
||||
const recipient = recipientId ? UserStore.getUser(recipientId) : null;
|
||||
|
||||
if (recipient) {
|
||||
return (
|
||||
<div className={styles.channelIconAvatar}>
|
||||
<Avatar user={recipient} size={20} status={null} className={styles.channelIconAvatarImage} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return ChannelUtils.getIcon(channel, {className: styles.channelIcon});
|
||||
}
|
||||
|
||||
if (channel.isGroupDM()) {
|
||||
return (
|
||||
<div className={styles.channelIconAvatar}>
|
||||
<GroupDMAvatar channel={channel} size={20} disableStatusIndicator />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return ChannelUtils.getIcon(channel, {className: styles.channelIcon});
|
||||
};
|
||||
|
||||
export interface MessageContextPrefixProps {
|
||||
channel: ChannelRecord;
|
||||
showGuildMeta?: boolean;
|
||||
compact?: boolean;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
export const MessageContextPrefix = observer(
|
||||
({channel, showGuildMeta = false, compact = false, onClick}: MessageContextPrefixProps) => {
|
||||
const guild = channel.guildId ? (GuildStore.getGuild(channel.guildId) ?? null) : null;
|
||||
const effectiveShowGuildMeta = Boolean(showGuildMeta && guild);
|
||||
|
||||
const channelDisplayName = getChannelDisplayName(channel);
|
||||
|
||||
return (
|
||||
<div className={[styles.channelHeader, compact && styles.channelHeaderCompact].filter(Boolean).join(' ')}>
|
||||
{!effectiveShowGuildMeta && renderChannelIcon(channel)}
|
||||
|
||||
<FocusRing offset={-2} ringClassName={styles.focusRingTight}>
|
||||
<button type="button" className={styles.channelNameButton} onClick={onClick}>
|
||||
{effectiveShowGuildMeta ? (
|
||||
<span className={styles.channelScopeRow}>
|
||||
<GuildIcon
|
||||
id={guild!.id}
|
||||
name={guild!.name}
|
||||
icon={guild!.icon}
|
||||
className={styles.channelScopeGuildIcon}
|
||||
sizePx={12}
|
||||
/>
|
||||
<span className={styles.channelScopeGuildName}>{guild!.name}</span>
|
||||
<CaretRightIcon className={styles.channelScopeChevron} size={12} weight="bold" />
|
||||
<span className={styles.channelScopeChannelInfo}>
|
||||
{ChannelUtils.getIcon(channel, {className: styles.channelScopeChannelIcon})}
|
||||
<span className={styles.channelScopeChannelName}>{channelDisplayName}</span>
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className={styles.channelNameText}>
|
||||
<span className={styles.channelNamePrimary}>{channelDisplayName}</span>
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</FocusRing>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
239
fluxer_app/src/components/shared/MessagePreview.module.css
Normal file
239
fluxer_app/src/components/shared/MessagePreview.module.css
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.previewCard {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--background-header-secondary);
|
||||
background-color: var(--background-secondary);
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.previewCardMobile {
|
||||
margin-bottom: 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 14px;
|
||||
border: none;
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.actionButtons {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.previewCard:hover .actionButtons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
padding: 0 8px;
|
||||
font-size: 11px;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
color: var(--text-primary-muted);
|
||||
background-color: var(--background-primary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.actionButton:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.actionIconButton {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-primary-muted);
|
||||
background-color: var(--background-primary);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.actionIconButton:hover {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.actionIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.scrollerMobile {
|
||||
padding: 0 16px 16px;
|
||||
}
|
||||
|
||||
.topSpacer {
|
||||
height: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.emptyStateContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.emptyStateIcon {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.emptyStateTextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.emptyStateTitle {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.emptyStateDescription {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.endState {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.endStateContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.endStateIcon {
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.endStateTextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.endStateTitle {
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
line-height: 28px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.endStateDescription {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.lostMessageInner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.lostMessageIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.lostMessageText {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-warning);
|
||||
}
|
||||
|
||||
.loadingState {
|
||||
display: flex;
|
||||
height: 80px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.loadingText {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
color: var(--text-primary-muted);
|
||||
}
|
||||
|
||||
.menuIcon {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
48
fluxer_app/src/components/shared/SavedMessageMissingCard.tsx
Normal file
48
fluxer_app/src/components/shared/SavedMessageMissingCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {WarningCircleIcon} from '@phosphor-icons/react';
|
||||
import previewStyles from '~/components/shared/MessagePreview.module.css';
|
||||
import FocusRing from '~/components/uikit/FocusRing/FocusRing';
|
||||
|
||||
interface SavedMessageMissingCardProps {
|
||||
entryId: string;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
export const SavedMessageMissingCard = ({entryId, onRemove}: SavedMessageMissingCardProps) => {
|
||||
const {t} = useLingui();
|
||||
return (
|
||||
<div key={`lost-${entryId}`} className={previewStyles.previewCard}>
|
||||
<div className={previewStyles.lostMessageInner}>
|
||||
<WarningCircleIcon className={previewStyles.lostMessageIcon} weight="duotone" />
|
||||
<p className={previewStyles.lostMessageText}>{t`You lost access to this saved message. Remove?`}</p>
|
||||
</div>
|
||||
|
||||
<div className={previewStyles.actionButtons}>
|
||||
<FocusRing offset={-2}>
|
||||
<button type="button" className={previewStyles.actionButton} onClick={onRemove}>
|
||||
{t`Remove`}
|
||||
</button>
|
||||
</FocusRing>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user