chore: bug fix cleanup (#4)
This commit is contained in:
@@ -103,6 +103,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background-color: var(--background-tertiary);
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
@@ -118,6 +119,33 @@
|
||||
padding: var(--spacing-3) var(--spacing-2) 0;
|
||||
}
|
||||
|
||||
.emptyState {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.emptyStateInner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-primary-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.emptyIcon {
|
||||
font-size: 42px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.emptyLabel {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {SmileySadIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as EmojiPickerActionCreators from '~/actions/EmojiPickerActionCreators';
|
||||
@@ -37,7 +38,7 @@ import {useSearchInputAutofocus} from '~/hooks/useSearchInputAutofocus';
|
||||
import {ComponentDispatch} from '~/lib/ComponentDispatch';
|
||||
import UnicodeEmojis, {EMOJI_SPRITES} from '~/lib/UnicodeEmojis';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import EmojiStore, {type Emoji} from '~/stores/EmojiStore';
|
||||
import EmojiStore, {type Emoji, normalizeEmojiSearchQuery} from '~/stores/EmojiStore';
|
||||
import {checkEmojiAvailability, shouldShowEmojiPremiumUpsell} from '~/utils/ExpressionPermissionUtils';
|
||||
import {shouldShowPremiumFeatures} from '~/utils/PremiumUtils';
|
||||
|
||||
@@ -53,14 +54,16 @@ export const EmojiPicker = observer(
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [hoveredEmoji, setHoveredEmoji] = React.useState<Emoji | null>(null);
|
||||
const [renderedEmojis, setRenderedEmojis] = React.useState<Array<Emoji>>([]);
|
||||
const [allEmojis, setAllEmojis] = React.useState<Array<Emoji>>([]);
|
||||
const [selectedRow, setSelectedRow] = React.useState(-1);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState(-1);
|
||||
const [shouldScrollOnSelection, setShouldScrollOnSelection] = React.useState(false);
|
||||
const scrollerRef = React.useRef<ScrollerHandle>(null);
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const emojiRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||
const normalizedSearchTerm = React.useMemo(() => normalizeEmojiSearchQuery(searchTerm), [searchTerm]);
|
||||
|
||||
const {i18n} = useLingui();
|
||||
const {i18n, t} = useLingui();
|
||||
const channel = channelId ? (ChannelStore.getChannel(channelId) ?? null) : null;
|
||||
const categoryRefs = React.useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
const forceUpdate = useForceUpdate();
|
||||
@@ -81,7 +84,7 @@ export const EmojiPicker = observer(
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const emojis = EmojiStore.search(channel, searchTerm).slice();
|
||||
const emojis = EmojiStore.search(channel, normalizedSearchTerm).slice();
|
||||
setRenderedEmojis(emojis);
|
||||
if (emojis.length > 0) {
|
||||
setSelectedRow(0);
|
||||
@@ -91,7 +94,12 @@ export const EmojiPicker = observer(
|
||||
setSelectedColumn(-1);
|
||||
setHoveredEmoji(null);
|
||||
}
|
||||
}, [channel, searchTerm]);
|
||||
}, [channel, normalizedSearchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const emojis = EmojiStore.search(channel, '').slice();
|
||||
setAllEmojis(emojis);
|
||||
}, [channel]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return ComponentDispatch.subscribe('EMOJI_PICKER_RERENDER', forceUpdate);
|
||||
@@ -99,10 +107,13 @@ export const EmojiPicker = observer(
|
||||
|
||||
useSearchInputAutofocus(searchInputRef);
|
||||
|
||||
const {favoriteEmojis, frequentlyUsedEmojis, customEmojisByGuildId, unicodeEmojisByCategory} =
|
||||
useEmojiCategories(renderedEmojis);
|
||||
const {favoriteEmojis, frequentlyUsedEmojis, customEmojisByGuildId, unicodeEmojisByCategory} = useEmojiCategories(
|
||||
allEmojis,
|
||||
renderedEmojis,
|
||||
);
|
||||
const showFrequentlyUsedButton = frequentlyUsedEmojis.length > 0 && !normalizedSearchTerm;
|
||||
const virtualRows = useVirtualRows(
|
||||
searchTerm,
|
||||
normalizedSearchTerm,
|
||||
renderedEmojis,
|
||||
favoriteEmojis,
|
||||
frequentlyUsedEmojis,
|
||||
@@ -110,7 +121,8 @@ export const EmojiPicker = observer(
|
||||
unicodeEmojisByCategory,
|
||||
);
|
||||
|
||||
const showPremiumUpsell = shouldShowPremiumFeatures() && shouldShowEmojiPremiumUpsell(channel);
|
||||
const showPremiumUpsell =
|
||||
shouldShowPremiumFeatures() && shouldShowEmojiPremiumUpsell(channel) && !normalizedSearchTerm;
|
||||
|
||||
const sections = React.useMemo(() => {
|
||||
const result: Array<number> = [];
|
||||
@@ -223,11 +235,12 @@ export const EmojiPicker = observer(
|
||||
className={`${styles.list} ${styles.listWrapper}`}
|
||||
fade={false}
|
||||
key="emoji-picker-scroller"
|
||||
reserveScrollbarTrack={false}
|
||||
reserveScrollbarTrack={true}
|
||||
>
|
||||
{showPremiumUpsell && <PremiumUpsellBanner />}
|
||||
{virtualRows.map((row, index) => {
|
||||
const emojiRowIndex = virtualRows.slice(0, index).filter((r) => r.type === 'emoji-row').length;
|
||||
const needsSpacingAfter = row.type === 'emoji-row' && virtualRows[index + 1]?.type === 'header';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -241,6 +254,7 @@ export const EmojiPicker = observer(
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={row.type === 'emoji-row' && needsSpacingAfter ? {marginBottom: '12px'} : undefined}
|
||||
>
|
||||
<VirtualizedRow
|
||||
row={row}
|
||||
@@ -260,6 +274,16 @@ export const EmojiPicker = observer(
|
||||
);
|
||||
})}
|
||||
</Scroller>
|
||||
{renderedEmojis.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyStateInner}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<SmileySadIcon weight="duotone" />
|
||||
</div>
|
||||
<div className={styles.emptyLabel}>{t`No emojis match your search`}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<EmojiPickerInspector hoveredEmoji={hoveredEmoji} />
|
||||
@@ -268,6 +292,7 @@ export const EmojiPicker = observer(
|
||||
customEmojisByGuildId={customEmojisByGuildId}
|
||||
unicodeEmojisByCategory={unicodeEmojisByCategory}
|
||||
handleCategoryClick={handleCategoryClick}
|
||||
showFrequentlyUsedButton={showFrequentlyUsedButton}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -280,7 +280,12 @@ export const Messages = observer(function Messages({channel}: {channel: ChannelR
|
||||
const data = payload as {channelId?: string; heightDelta?: number} | undefined;
|
||||
if (data?.channelId && data.channelId !== channel.id) return;
|
||||
|
||||
scrollManager.handleScroll();
|
||||
const heightDelta = data?.heightDelta;
|
||||
const handledLayoutShift = typeof heightDelta === 'number' ? scrollManager.applyLayoutShift(heightDelta) : false;
|
||||
|
||||
if (!handledLayoutShift) {
|
||||
scrollManager.handleScroll();
|
||||
}
|
||||
};
|
||||
|
||||
const onFocusBottommostMessage = (payload?: unknown) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ import {useForceUpdate} from '~/hooks/useForceUpdate';
|
||||
import {ComponentDispatch} from '~/lib/ComponentDispatch';
|
||||
import UnicodeEmojis, {EMOJI_SPRITES} from '~/lib/UnicodeEmojis';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import EmojiStore, {type Emoji} from '~/stores/EmojiStore';
|
||||
import EmojiStore, {type Emoji, normalizeEmojiSearchQuery} from '~/stores/EmojiStore';
|
||||
|
||||
export const MobileEmojiPicker = observer(
|
||||
({
|
||||
@@ -57,6 +57,7 @@ export const MobileEmojiPicker = observer(
|
||||
const [internalSearchTerm, setInternalSearchTerm] = React.useState('');
|
||||
const [hoveredEmoji, setHoveredEmoji] = React.useState<Emoji | null>(null);
|
||||
const [renderedEmojis, setRenderedEmojis] = React.useState<Array<Emoji>>([]);
|
||||
const [allEmojis, setAllEmojis] = React.useState<Array<Emoji>>([]);
|
||||
const scrollerRef = React.useRef<ScrollerHandle>(null);
|
||||
const emojiRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||
|
||||
@@ -67,6 +68,7 @@ export const MobileEmojiPicker = observer(
|
||||
|
||||
const searchTerm = externalSearchTerm ?? internalSearchTerm;
|
||||
const setSearchTerm = externalSetSearchTerm ?? setInternalSearchTerm;
|
||||
const normalizedSearchTerm = React.useMemo(() => normalizeEmojiSearchQuery(searchTerm), [searchTerm]);
|
||||
|
||||
const spriteSheetSizes = React.useMemo(() => {
|
||||
const nonDiversitySize = [
|
||||
@@ -83,18 +85,26 @@ export const MobileEmojiPicker = observer(
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
const emojis = EmojiStore.search(null, searchTerm).slice();
|
||||
const emojis = EmojiStore.search(channel, normalizedSearchTerm).slice();
|
||||
setRenderedEmojis(emojis);
|
||||
}, [searchTerm]);
|
||||
}, [channel, normalizedSearchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const emojis = EmojiStore.search(channel, '').slice();
|
||||
setAllEmojis(emojis);
|
||||
}, [channel]);
|
||||
|
||||
React.useEffect(() => {
|
||||
return ComponentDispatch.subscribe('EMOJI_PICKER_RERENDER', forceUpdate);
|
||||
});
|
||||
|
||||
const {customEmojisByGuildId, unicodeEmojisByCategory, favoriteEmojis, frequentlyUsedEmojis} =
|
||||
useEmojiCategories(renderedEmojis);
|
||||
const {customEmojisByGuildId, unicodeEmojisByCategory, favoriteEmojis, frequentlyUsedEmojis} = useEmojiCategories(
|
||||
allEmojis,
|
||||
renderedEmojis,
|
||||
);
|
||||
const showFrequentlyUsedButton = frequentlyUsedEmojis.length > 0 && !normalizedSearchTerm;
|
||||
const virtualRows = useVirtualRows(
|
||||
searchTerm,
|
||||
normalizedSearchTerm,
|
||||
renderedEmojis,
|
||||
favoriteEmojis,
|
||||
frequentlyUsedEmojis,
|
||||
@@ -168,6 +178,7 @@ export const MobileEmojiPicker = observer(
|
||||
unicodeEmojisByCategory={unicodeEmojisByCategory}
|
||||
handleCategoryClick={handleCategoryClick}
|
||||
horizontal={true}
|
||||
showFrequentlyUsedButton={showFrequentlyUsedButton}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,6 +61,9 @@ export const MobileStickersPicker = observer(
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [hoveredSticker, setHoveredSticker] = React.useState<GuildStickerRecord | null>(null);
|
||||
const [renderedStickers, setRenderedStickers] = React.useState<ReadonlyArray<GuildStickerRecord>>([]);
|
||||
const [allStickersForCategories, setAllStickersForCategories] = React.useState<ReadonlyArray<GuildStickerRecord>>(
|
||||
[],
|
||||
);
|
||||
const scrollerRef = React.useRef<ScrollerHandle>(null);
|
||||
const searchInputRef = React.useRef<HTMLInputElement>(null);
|
||||
const stickerRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map());
|
||||
@@ -74,13 +77,20 @@ export const MobileStickersPicker = observer(
|
||||
setRenderedStickers(stickers);
|
||||
}, [channel, searchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAllStickersForCategories(StickerStore.getAllStickers());
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
return ComponentDispatch.subscribe('STICKER_PICKER_RERENDER', forceUpdate);
|
||||
});
|
||||
|
||||
useSearchInputAutofocus(searchInputRef);
|
||||
|
||||
const {favoriteStickers, frequentlyUsedStickers, stickersByGuildId} = useStickerCategories(renderedStickers);
|
||||
const {favoriteStickers, frequentlyUsedStickers, stickersByGuildId} = useStickerCategories(
|
||||
allStickersForCategories,
|
||||
renderedStickers,
|
||||
);
|
||||
const virtualRows = useVirtualRows(
|
||||
searchTerm,
|
||||
renderedStickers,
|
||||
|
||||
@@ -55,6 +55,9 @@ export const StickersPicker = observer(
|
||||
const [searchTerm, setSearchTerm] = React.useState('');
|
||||
const [hoveredSticker, setHoveredSticker] = React.useState<GuildStickerRecord | null>(null);
|
||||
const [renderedStickers, setRenderedStickers] = React.useState<ReadonlyArray<GuildStickerRecord>>([]);
|
||||
const [allStickersForCategories, setAllStickersForCategories] = React.useState<ReadonlyArray<GuildStickerRecord>>(
|
||||
[],
|
||||
);
|
||||
const [selectedRow, setSelectedRow] = React.useState(-1);
|
||||
const [selectedColumn, setSelectedColumn] = React.useState(-1);
|
||||
const [shouldScrollOnSelection, setShouldScrollOnSelection] = React.useState(false);
|
||||
@@ -79,13 +82,20 @@ export const StickersPicker = observer(
|
||||
}
|
||||
}, [channel, searchTerm]);
|
||||
|
||||
React.useEffect(() => {
|
||||
setAllStickersForCategories(StickerStore.getAllStickers());
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
return ComponentDispatch.subscribe('STICKER_PICKER_RERENDER', forceUpdate);
|
||||
});
|
||||
|
||||
useSearchInputAutofocus(searchInputRef);
|
||||
|
||||
const {favoriteStickers, frequentlyUsedStickers, stickersByGuildId} = useStickerCategories(renderedStickers);
|
||||
const {favoriteStickers, frequentlyUsedStickers, stickersByGuildId} = useStickerCategories(
|
||||
allStickersForCategories,
|
||||
renderedStickers,
|
||||
);
|
||||
const virtualRows = useVirtualRows(
|
||||
searchTerm,
|
||||
renderedStickers,
|
||||
@@ -98,7 +108,8 @@ export const StickersPicker = observer(
|
||||
const allStickers = React.useMemo(() => StickerStore.getAllStickers(), []);
|
||||
const hasNoStickersAtAll = allStickers.length === 0;
|
||||
|
||||
const showPremiumUpsell = shouldShowPremiumFeatures() && shouldShowStickerPremiumUpsell(channel);
|
||||
const isSearching = searchTerm.trim().length > 0;
|
||||
const showPremiumUpsell = shouldShowPremiumFeatures() && shouldShowStickerPremiumUpsell(channel) && !isSearching;
|
||||
|
||||
const sections = React.useMemo(() => {
|
||||
const result: Array<number> = [];
|
||||
@@ -216,21 +227,6 @@ export const StickersPicker = observer(
|
||||
/>
|
||||
);
|
||||
|
||||
if (renderedStickers.length === 0 && searchTerm) {
|
||||
return (
|
||||
<div className={gifStyles.gifPickerContainer}>
|
||||
<ExpressionPickerHeaderPortal>{renderSearchBar()}</ExpressionPickerHeaderPortal>
|
||||
<div className={gifStyles.gifPickerMain}>
|
||||
<PickerEmptyState
|
||||
icon={SmileySadIcon}
|
||||
title={t`No Stickers Found`}
|
||||
description={t`Try a different search term`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<ExpressionPickerHeaderPortal>{renderSearchBar()}</ExpressionPickerHeaderPortal>
|
||||
@@ -242,11 +238,12 @@ export const StickersPicker = observer(
|
||||
className={`${styles.list} ${styles.listWrapper}`}
|
||||
fade={false}
|
||||
key="stickers-picker-scroller"
|
||||
reserveScrollbarTrack={false}
|
||||
reserveScrollbarTrack={true}
|
||||
>
|
||||
{showPremiumUpsell && <PremiumUpsellBanner />}
|
||||
{virtualRows.map((row, index) => {
|
||||
const stickerRowIndex = virtualRows.slice(0, index).filter((r) => r.type === 'sticker-row').length;
|
||||
const needsSpacingAfter = row.type === 'sticker-row' && virtualRows[index + 1]?.type === 'header';
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -260,6 +257,7 @@ export const StickersPicker = observer(
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
style={row.type === 'sticker-row' && needsSpacingAfter ? {marginBottom: '12px'} : undefined}
|
||||
>
|
||||
<VirtualRowWrapper
|
||||
row={row}
|
||||
@@ -278,6 +276,16 @@ export const StickersPicker = observer(
|
||||
);
|
||||
})}
|
||||
</Scroller>
|
||||
{renderedStickers.length === 0 && (
|
||||
<div className={styles.emptyState}>
|
||||
<div className={styles.emptyStateInner}>
|
||||
<div className={styles.emptyIcon}>
|
||||
<SmileySadIcon weight="duotone" />
|
||||
</div>
|
||||
<div className={styles.emptyLabel}>{t`No stickers match your search`}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<StickerPickerInspector hoveredSticker={hoveredSticker} />
|
||||
|
||||
@@ -133,6 +133,27 @@ const getOptimizedMediaURL = (
|
||||
});
|
||||
};
|
||||
|
||||
const SuppressEmbedsConfirmModal: FC<{message: MessageRecord}> = ({message}) => {
|
||||
const {t} = useLingui();
|
||||
|
||||
return (
|
||||
<ConfirmModal
|
||||
title={t`Suppress Embeds`}
|
||||
description={
|
||||
<Trans>
|
||||
Are you sure you want to suppress all link embeds on this message? This action will hide all embeds from this
|
||||
message.
|
||||
</Trans>
|
||||
}
|
||||
primaryText={t`Suppress Embeds`}
|
||||
primaryVariant="danger-primary"
|
||||
onPrimary={async () => {
|
||||
await MessageActionCreators.toggleSuppressEmbeds(message.channelId, message.id, message.flags);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const shouldRenderAsInlineThumbnail = (media?: EmbedMedia): boolean => {
|
||||
if (!isValidMedia(media)) return false;
|
||||
|
||||
@@ -697,27 +718,7 @@ export const Embed: FC<EmbedProps> = observer(({embed, message, embedIndex, onDe
|
||||
}, [message, channel]);
|
||||
|
||||
const handleSuppressEmbeds = useCallback(() => {
|
||||
ModalActionCreators.push(
|
||||
modal(() => {
|
||||
const {t} = useLingui();
|
||||
return (
|
||||
<ConfirmModal
|
||||
title={t`Suppress Embeds`}
|
||||
description={
|
||||
<Trans>
|
||||
Are you sure you want to suppress all link embeds on this message? This action will hide all embeds from
|
||||
this message.
|
||||
</Trans>
|
||||
}
|
||||
primaryText={t`Suppress Embeds`}
|
||||
primaryVariant="danger-primary"
|
||||
onPrimary={async () => {
|
||||
await MessageActionCreators.toggleSuppressEmbeds(message.channelId, message.id, message.flags);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
);
|
||||
ModalActionCreators.push(modal(() => <SuppressEmbedsConfirmModal message={message} />));
|
||||
}, [message]);
|
||||
|
||||
const showSuppressButton = !isMobile && canSuppressEmbeds() && AccessibilityStore.showSuppressEmbedsButton;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {ClockIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import styles from '~/components/channel/EmojiPicker.module.css';
|
||||
@@ -31,6 +32,7 @@ interface EmojiPickerCategoryListProps {
|
||||
customEmojisByGuildId: Map<string, Array<Emoji>>;
|
||||
unicodeEmojisByCategory: Map<string, Array<Emoji>>;
|
||||
handleCategoryClick: (category: string) => void;
|
||||
showFrequentlyUsedButton: boolean;
|
||||
horizontal?: boolean;
|
||||
}
|
||||
|
||||
@@ -39,12 +41,23 @@ export const EmojiPickerCategoryList = observer(
|
||||
customEmojisByGuildId,
|
||||
unicodeEmojisByCategory,
|
||||
handleCategoryClick,
|
||||
showFrequentlyUsedButton = false,
|
||||
horizontal = false,
|
||||
}: EmojiPickerCategoryListProps) => {
|
||||
const {i18n} = useLingui();
|
||||
if (horizontal) {
|
||||
return (
|
||||
<div className={styles.horizontalCategories}>
|
||||
{showFrequentlyUsedButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCategoryClick('frequently-used')}
|
||||
className={clsx(styles.categoryListIcon, styles.textPrimaryMuted)}
|
||||
aria-label={i18n._('Frequently Used')}
|
||||
>
|
||||
<ClockIcon className={styles.iconSize} />
|
||||
</button>
|
||||
)}
|
||||
{Array.from(customEmojisByGuildId.keys()).map((guildId) => {
|
||||
const guild = GuildStore.getGuild(guildId)!;
|
||||
return (
|
||||
@@ -81,6 +94,17 @@ export const EmojiPickerCategoryList = observer(
|
||||
<div className={styles.categoryList}>
|
||||
<div className={styles.categoryListScroll}>
|
||||
<div className={styles.listItems}>
|
||||
{showFrequentlyUsedButton && (
|
||||
<Tooltip text={i18n._('Frequently Used')} position="left">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleCategoryClick('frequently-used')}
|
||||
className={clsx(styles.categoryListIcon, styles.textPrimaryMuted)}
|
||||
>
|
||||
<ClockIcon className={styles.iconSize} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
)}
|
||||
{Array.from(customEmojisByGuildId.keys()).map((guildId) => {
|
||||
const guild = GuildStore.getGuild(guildId)!;
|
||||
return (
|
||||
|
||||
@@ -23,15 +23,15 @@ import EmojiPickerStore from '~/stores/EmojiPickerStore';
|
||||
import type {Emoji} from '~/stores/EmojiStore';
|
||||
import GuildListStore from '~/stores/GuildListStore';
|
||||
|
||||
export const useEmojiCategories = (renderedEmojis: Array<Emoji>) => {
|
||||
export const useEmojiCategories = (allEmojis: Array<Emoji>, _renderedEmojis: Array<Emoji>) => {
|
||||
const guilds = GuildListStore.guilds;
|
||||
|
||||
const favoriteEmojis = EmojiPickerStore.getFavoriteEmojis(renderedEmojis);
|
||||
const favoriteEmojis = EmojiPickerStore.getFavoriteEmojis(allEmojis);
|
||||
|
||||
const frequentlyUsedEmojis = EmojiPickerStore.getFrecentEmojis(renderedEmojis, 42);
|
||||
const frequentlyUsedEmojis = EmojiPickerStore.getFrecentEmojis(allEmojis, 42);
|
||||
|
||||
const customEmojisByGuildId = React.useMemo(() => {
|
||||
const guildEmojis = renderedEmojis.filter((emoji) => emoji.guildId != null);
|
||||
const guildEmojis = allEmojis.filter((emoji) => emoji.guildId != null);
|
||||
const guildEmojisByGuildId = new Map<string, Array<Emoji>>();
|
||||
|
||||
for (const guildEmoji of guildEmojis) {
|
||||
@@ -50,10 +50,10 @@ export const useEmojiCategories = (renderedEmojis: Array<Emoji>) => {
|
||||
}
|
||||
|
||||
return sortedGuildEmojisByGuildId;
|
||||
}, [renderedEmojis, guilds]);
|
||||
}, [allEmojis, guilds]);
|
||||
|
||||
const unicodeEmojisByCategory = React.useMemo(() => {
|
||||
const unicodeEmojis = renderedEmojis.filter((emoji) => emoji.guildId == null);
|
||||
const unicodeEmojis = allEmojis.filter((emoji) => emoji.guildId == null);
|
||||
const unicodeEmojisByCategory = new Map<string, Array<Emoji>>();
|
||||
|
||||
for (const emoji of unicodeEmojis) {
|
||||
@@ -76,7 +76,7 @@ export const useEmojiCategories = (renderedEmojis: Array<Emoji>) => {
|
||||
}
|
||||
|
||||
return sortedUnicodeEmojisByCategory;
|
||||
}, [renderedEmojis]);
|
||||
}, [allEmojis]);
|
||||
|
||||
return {
|
||||
favoriteEmojis,
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
} from '~/components/uikit/ContextMenu/ContextMenuIcons';
|
||||
import type {MenuGroupType, MenuItemType} from '~/components/uikit/MenuBottomSheet/MenuBottomSheet';
|
||||
import type {MessageRecord} from '~/records/MessageRecord';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import EmojiPickerStore from '~/stores/EmojiPickerStore';
|
||||
import type {Emoji} from '~/stores/EmojiStore';
|
||||
import EmojiStore from '~/stores/EmojiStore';
|
||||
@@ -73,7 +74,8 @@ export const useMessageActionMenuData = (
|
||||
const permissions = useMessagePermissions(message);
|
||||
const handlers = React.useMemo(() => createMessageActionHandlers(message, {onClose}), [message, onClose]);
|
||||
const isSaved = React.useMemo(() => SavedMessagesStore.isSaved(message.id), [message.id]);
|
||||
const allEmojis = React.useMemo(() => EmojiStore.search(null, ''), []);
|
||||
const channel = React.useMemo(() => ChannelStore.getChannel(message.channelId) ?? null, [message.channelId]);
|
||||
const allEmojis = React.useMemo(() => EmojiStore.search(channel, ''), [channel]);
|
||||
const quickReactionEmojis = React.useMemo(
|
||||
() => EmojiPickerStore.getQuickReactionEmojis(allEmojis, quickReactionCount),
|
||||
[allEmojis, quickReactionCount],
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
.stickerGrid {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
gap: var(--spacing-2);
|
||||
padding: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.stickerButton {
|
||||
|
||||
@@ -22,22 +22,25 @@ import type {GuildStickerRecord} from '~/records/GuildStickerRecord';
|
||||
import GuildListStore from '~/stores/GuildListStore';
|
||||
import StickerPickerStore from '~/stores/StickerPickerStore';
|
||||
|
||||
export const useStickerCategories = (renderedStickers: ReadonlyArray<GuildStickerRecord>) => {
|
||||
export const useStickerCategories = (
|
||||
allStickers: ReadonlyArray<GuildStickerRecord>,
|
||||
_renderedStickers: ReadonlyArray<GuildStickerRecord>,
|
||||
) => {
|
||||
const guilds = GuildListStore.guilds;
|
||||
const stickerPickerState = StickerPickerStore;
|
||||
|
||||
const favoriteStickers = React.useMemo(() => {
|
||||
return StickerPickerStore.getFavoriteStickers(renderedStickers);
|
||||
}, [renderedStickers, stickerPickerState.favoriteStickers]);
|
||||
return StickerPickerStore.getFavoriteStickers(allStickers);
|
||||
}, [allStickers, stickerPickerState.favoriteStickers]);
|
||||
|
||||
const frequentlyUsedStickers = React.useMemo(() => {
|
||||
return StickerPickerStore.getFrecentStickers(renderedStickers, 42);
|
||||
}, [renderedStickers, stickerPickerState.stickerUsage]);
|
||||
return StickerPickerStore.getFrecentStickers(allStickers, 42);
|
||||
}, [allStickers, stickerPickerState.stickerUsage]);
|
||||
|
||||
const stickersByGuildId = React.useMemo(() => {
|
||||
const guildStickersMap = new Map<string, Array<GuildStickerRecord>>();
|
||||
|
||||
for (const sticker of renderedStickers) {
|
||||
for (const sticker of allStickers) {
|
||||
if (!guildStickersMap.has(sticker.guildId)) {
|
||||
guildStickersMap.set(sticker.guildId, []);
|
||||
}
|
||||
@@ -53,7 +56,7 @@ export const useStickerCategories = (renderedStickers: ReadonlyArray<GuildSticke
|
||||
}
|
||||
|
||||
return sortedGuildStickersMap;
|
||||
}, [renderedStickers, guilds]);
|
||||
}, [allStickers, guilds]);
|
||||
|
||||
return {
|
||||
favoriteStickers,
|
||||
|
||||
@@ -22,7 +22,6 @@ import React from 'react';
|
||||
import * as NavigationActionCreators from '~/actions/NavigationActionCreators';
|
||||
import {FAVORITES_GUILD_ID} from '~/Constants';
|
||||
import {FavoritesWelcomeSection} from '~/components/favorites/FavoritesWelcomeSection';
|
||||
import {DndContext} from '~/components/layout/DndContext';
|
||||
import {FavoritesChannelListContent} from '~/components/layout/FavoritesChannelListContent';
|
||||
import {FavoritesGuildHeader} from '~/components/layout/FavoritesGuildHeader';
|
||||
import {GuildSidebar} from '~/components/layout/GuildSidebar';
|
||||
@@ -66,45 +65,35 @@ export const FavoritesLayout = observer(({children}: {children?: React.ReactNode
|
||||
|
||||
if (shouldRenderWelcomeScreen) {
|
||||
return (
|
||||
<DndContext>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />
|
||||
<div className={styles.guildMainContent}>
|
||||
<FavoritesWelcomeSection />
|
||||
</div>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />
|
||||
<div className={styles.guildMainContent}>
|
||||
<FavoritesWelcomeSection />
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (mobileLayout.enabled) {
|
||||
if (!channelId) {
|
||||
return (
|
||||
<DndContext>
|
||||
<GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />
|
||||
</DndContext>
|
||||
);
|
||||
return <GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
<div className={styles.guildLayoutContainer}>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildSidebar header={<FavoritesGuildHeader />} content={<FavoritesChannelListContent />} />
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -27,7 +27,6 @@ import {modal} from '~/actions/ModalActionCreators';
|
||||
import * as NagbarActionCreators from '~/actions/NagbarActionCreators';
|
||||
import * as NavigationActionCreators from '~/actions/NavigationActionCreators';
|
||||
import {ChannelTypes, GuildFeatures, Permissions} from '~/Constants';
|
||||
import {DndContext} from '~/components/layout/DndContext';
|
||||
import {GuildNavbar} from '~/components/layout/GuildNavbar';
|
||||
import {GuildNavbarSkeleton} from '~/components/layout/GuildNavbarSkeleton';
|
||||
import {Nagbar} from '~/components/layout/Nagbar';
|
||||
@@ -296,45 +295,39 @@ export const GuildLayout = observer(({children}: {children: React.ReactNode}) =>
|
||||
if (guildUnavailable || guildNotFound) {
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
{guildUnavailable ? (
|
||||
<GuildUnavailable
|
||||
icon={NetworkSlashIcon}
|
||||
title={t`Community temporarily unavailable`}
|
||||
description={t`We fluxed up! Hang tight, we're working on it.`}
|
||||
/>
|
||||
) : (
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`This is not the community you're looking for.`}
|
||||
description={t`The community you're looking for may have been deleted or you may not have access to it.`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
{guildUnavailable ? (
|
||||
<GuildUnavailable
|
||||
icon={NetworkSlashIcon}
|
||||
title={t`Community temporarily unavailable`}
|
||||
description={t`We fluxed up! Hang tight, we're working on it.`}
|
||||
/>
|
||||
) : (
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`This is not the community you're looking for.`}
|
||||
description={t`The community you're looking for may have been deleted or you may not have access to it.`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<GuildNavbar guild={guild!} />
|
||||
</DndContext>
|
||||
<GuildNavbar guild={guild!} />
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -342,21 +335,19 @@ export const GuildLayout = observer(({children}: {children: React.ReactNode}) =>
|
||||
if (guildUnavailable) {
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={NetworkSlashIcon}
|
||||
title={t`Community temporarily unavailable`}
|
||||
description={t`We fluxed up! Hang tight, we're working on it.`}
|
||||
/>
|
||||
</div>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={NetworkSlashIcon}
|
||||
title={t`Community temporarily unavailable`}
|
||||
description={t`We fluxed up! Hang tight, we're working on it.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -364,21 +355,19 @@ export const GuildLayout = observer(({children}: {children: React.ReactNode}) =>
|
||||
if (guildNotFound) {
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`This is not the community you're looking for.`}
|
||||
description={t`The community you're looking for may have been deleted or you may not have access to it.`}
|
||||
/>
|
||||
</div>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbarSkeleton />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`This is not the community you're looking for.`}
|
||||
description={t`The community you're looking for may have been deleted or you may not have access to it.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -386,36 +375,32 @@ export const GuildLayout = observer(({children}: {children: React.ReactNode}) =>
|
||||
if (channelId && !ChannelStore.getChannel(channelId) && !firstAccessibleTextChannel) {
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbar guild={guild!} />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`No accessible channels`}
|
||||
description={t`You don't have access to any channels in this community.`}
|
||||
/>
|
||||
</div>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbar guild={guild!} />
|
||||
<div className={styles.guildMainContent}>
|
||||
<GuildUnavailable
|
||||
icon={SmileySadIcon}
|
||||
title={t`No accessible channels`}
|
||||
description={t`You don't have access to any channels in this community.`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TopNagbarContext.Provider value={nagbarContextValue}>
|
||||
<DndContext>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbar guild={guild!} />
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
<div className={hasGuildNagbars ? styles.guildLayoutContainerWithNagbar : styles.guildLayoutContainer}>
|
||||
{guildNagbars}
|
||||
<div className={styles.guildLayoutContent}>
|
||||
<GuildNavbar guild={guild!} />
|
||||
<div className={styles.guildMainContent}>{children}</div>
|
||||
</div>
|
||||
</DndContext>
|
||||
</div>
|
||||
</TopNagbarContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -117,6 +117,19 @@ function storePlaybackRate(rate: number): void {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const isAbortError = (error: unknown): boolean => {
|
||||
if (!error || typeof error !== 'object') return false;
|
||||
const name = (error as {name?: unknown}).name;
|
||||
if (name === 'AbortError') return true;
|
||||
const message = (error as {message?: unknown}).message;
|
||||
return typeof message === 'string' && message.toLowerCase().includes('interrupted');
|
||||
};
|
||||
|
||||
const normalizeError = (error: unknown): Error => {
|
||||
if (error instanceof Error) return error;
|
||||
return new Error(typeof error === 'string' ? error : 'Unknown media error');
|
||||
};
|
||||
|
||||
export function useMediaPlayer(options: UseMediaPlayerOptions = {}): UseMediaPlayerReturn {
|
||||
const {
|
||||
autoPlay = false,
|
||||
@@ -324,9 +337,16 @@ export function useMediaPlayer(options: UseMediaPlayerOptions = {}): UseMediaPla
|
||||
|
||||
try {
|
||||
await media.play();
|
||||
setState((prev) => ({...prev, error: null}));
|
||||
} catch (error) {
|
||||
console.error('Play failed:', error);
|
||||
throw error;
|
||||
if (isAbortError(error)) {
|
||||
console.debug('Play interrupted before it could start:', error);
|
||||
return;
|
||||
}
|
||||
|
||||
const normalizedError = normalizeError(error);
|
||||
console.error('Play failed:', normalizedError);
|
||||
setState((prev) => ({...prev, error: normalizedError}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -82,14 +82,12 @@ const ChannelInvitesTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!(fetchStatus === 'success' && invites && invites.length === 0) && (
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button small={true} disabled={!canInvite} onClick={handleCreateInvite}>
|
||||
<Trans>Create Invite</Trans>
|
||||
</Button>
|
||||
{canManageGuild && channel?.guildId && <DisableInvitesButton guildId={channel.guildId} />}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.buttonGroup}>
|
||||
<Button small={true} disabled={!canInvite || fetchStatus === 'pending'} onClick={handleCreateInvite}>
|
||||
<Trans>Create Invite</Trans>
|
||||
</Button>
|
||||
{canManageGuild && channel?.guildId && <DisableInvitesButton guildId={channel.guildId} />}
|
||||
</div>
|
||||
|
||||
{fetchStatus === 'pending' && (
|
||||
<div className={styles.spinnerContainer}>
|
||||
|
||||
@@ -135,7 +135,7 @@ const ChannelWebhooksTab: React.FC<{channelId: string}> = observer(({channelId})
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManageWebhooks && !(fetchStatus === 'success' && (!webhooks || webhooks.length === 0)) && (
|
||||
{canManageWebhooks && (
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button onClick={handleCreateQuick} variant="primary" disabled={fetchStatus === 'pending'} small>
|
||||
<Trans>Create Webhook</Trans>
|
||||
|
||||
@@ -73,9 +73,7 @@ const GuildInvitesTab: React.FC<{guildId: string}> = observer(({guildId}) => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{canManageGuild && !(fetchStatus === 'success' && invites && invites.length === 0) && (
|
||||
<DisableInvitesButton guildId={guildId} />
|
||||
)}
|
||||
{canManageGuild && <DisableInvitesButton guildId={guildId} />}
|
||||
|
||||
{fetchStatus === 'pending' && (
|
||||
<div className={styles.spinnerContainer}>
|
||||
|
||||
@@ -112,7 +112,7 @@ const GuildWebhooksTab: React.FC<{guildId: string}> = observer(({guildId}) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{canManageWebhooks && !(fetchStatus === 'success' && sortedWebhooks.length === 0) && (
|
||||
{canManageWebhooks && (
|
||||
<div className={styles.infoBox}>
|
||||
<Trans>
|
||||
To create a webhook, open the channel's settings and use the <strong>Webhooks</strong> tab. You can still
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-3);
|
||||
border-bottom: 1px solid var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
:global(.theme-light) .header {
|
||||
|
||||
@@ -33,6 +33,7 @@ type GuildIconProps = {
|
||||
icon: string | null;
|
||||
className?: string;
|
||||
sizePx?: number;
|
||||
containerProps?: React.HTMLAttributes<HTMLElement> & {'data-jump-link-guild-icon'?: string};
|
||||
};
|
||||
|
||||
type GuildIconStyleVars = React.CSSProperties & {
|
||||
@@ -40,7 +41,14 @@ type GuildIconStyleVars = React.CSSProperties & {
|
||||
'--guild-icon-image'?: string;
|
||||
};
|
||||
|
||||
export const GuildIcon = observer(function GuildIcon({id, name, icon, className, sizePx}: GuildIconProps) {
|
||||
export const GuildIcon = observer(function GuildIcon({
|
||||
id,
|
||||
name,
|
||||
icon,
|
||||
className,
|
||||
sizePx,
|
||||
containerProps,
|
||||
}: GuildIconProps) {
|
||||
const initials = React.useMemo(() => StringUtils.getInitialsFromName(name), [name]);
|
||||
const initialsLength = React.useMemo(() => getInitialsLength(initials), [initials]);
|
||||
const [hoverRef, isHovering] = useHover();
|
||||
@@ -104,6 +112,7 @@ export const GuildIcon = observer(function GuildIcon({id, name, icon, className,
|
||||
<div
|
||||
ref={hoverRef}
|
||||
className={clsx(styles.container, className, !icon && styles.containerNoIcon)}
|
||||
{...containerProps}
|
||||
data-initials-length={initialsLength}
|
||||
style={styleVars}
|
||||
>
|
||||
|
||||
@@ -147,6 +147,13 @@ export const ManageRolesMenuItem: React.FC<ManageRolesMenuItemProps> = observer(
|
||||
}));
|
||||
}, [guild, canManageRole]);
|
||||
|
||||
const visibleRoles = React.useMemo(() => {
|
||||
if (canManageRoles) return allRoles;
|
||||
const memberRoles = currentMember?.roles;
|
||||
if (!memberRoles) return [];
|
||||
return allRoles.filter(({role}) => memberRoles.has(role.id));
|
||||
}, [allRoles, canManageRoles, currentMember]);
|
||||
|
||||
const handleToggleRole = React.useCallback(
|
||||
async (roleId: string, hasRole: boolean, canToggle: boolean) => {
|
||||
if (!canToggle) return;
|
||||
@@ -159,33 +166,42 @@ export const ManageRolesMenuItem: React.FC<ManageRolesMenuItemProps> = observer(
|
||||
[guildId, member.user.id],
|
||||
);
|
||||
|
||||
if (allRoles.length === 0) return null;
|
||||
if (visibleRoles.length === 0) return null;
|
||||
|
||||
return (
|
||||
<MenuItemSubmenu
|
||||
label={t`Roles`}
|
||||
icon={<UserListIcon className={itemStyles.icon} />}
|
||||
selectionMode="multiple"
|
||||
selectionMode={canManageRoles ? 'multiple' : 'none'}
|
||||
render={() => (
|
||||
<MenuGroup>
|
||||
{allRoles.map(({role, canManage}) => {
|
||||
const hasRole = currentMember?.roles.has(role.id) ?? false;
|
||||
const canToggle = canManageRoles && canManage;
|
||||
return (
|
||||
<MenuItemCheckbox
|
||||
key={role.id}
|
||||
checked={hasRole}
|
||||
disabled={!canToggle}
|
||||
onChange={() => handleToggleRole(role.id, hasRole, canToggle)}
|
||||
closeOnChange={false}
|
||||
>
|
||||
<div className={itemStyles.roleContainer}>
|
||||
<div className={itemStyles.roleIcon} style={{backgroundColor: ColorUtils.int2rgb(role.color)}} />
|
||||
<span className={!canToggle ? itemStyles.roleDisabled : undefined}>{role.name}</span>
|
||||
</div>
|
||||
</MenuItemCheckbox>
|
||||
);
|
||||
})}
|
||||
{canManageRoles
|
||||
? visibleRoles.map(({role, canManage}) => {
|
||||
const hasRole = currentMember?.roles.has(role.id) ?? false;
|
||||
const canToggle = canManageRoles && canManage;
|
||||
return (
|
||||
<MenuItemCheckbox
|
||||
key={role.id}
|
||||
checked={hasRole}
|
||||
disabled={!canToggle}
|
||||
onChange={() => handleToggleRole(role.id, hasRole, canToggle)}
|
||||
closeOnChange={false}
|
||||
>
|
||||
<div className={itemStyles.roleContainer}>
|
||||
<div className={itemStyles.roleIcon} style={{backgroundColor: ColorUtils.int2rgb(role.color)}} />
|
||||
<span className={!canToggle ? itemStyles.roleDisabled : undefined}>{role.name}</span>
|
||||
</div>
|
||||
</MenuItemCheckbox>
|
||||
);
|
||||
})
|
||||
: visibleRoles.map(({role}) => (
|
||||
<MenuItem key={role.id} closeOnSelect={false}>
|
||||
<div className={itemStyles.roleContainer}>
|
||||
<div className={itemStyles.roleIcon} style={{backgroundColor: ColorUtils.int2rgb(role.color)}} />
|
||||
<span className={itemStyles.roleName}>{role.name}</span>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))}
|
||||
</MenuGroup>
|
||||
)}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user