fix: various fixes to sentry-reported errors and more

This commit is contained in:
Hampus Kraft
2026-02-18 15:38:51 +00:00
parent 302c0d2a0c
commit 0517a966a3
357 changed files with 25420 additions and 16281 deletions

View File

@@ -191,7 +191,7 @@ export const Messages = observer(function Messages({channel, onBottomBarVisibili
canAutoAck,
});
useEffect(() => {
useLayoutEffect(() => {
const node = messagesWrapperRef.current;
if (node) {
node.style.setProperty('--message-group-spacing', `${state.messageGroupSpacing}px`);

View File

@@ -36,7 +36,7 @@
--search-scope-badge-hover-color: var(--text-primary);
--search-scope-badge-border-color: var(--background-modifier-accent);
--search-input-text-color: var(--text-primary);
--search-input-placeholder-color: var(--text-primary-muted);
--search-input-placeholder-color: var(--text-tertiary);
--search-clear-button-color: var(--text-tertiary);
--search-clear-button-hover-color: var(--text-primary);
--search-clear-button-hover-background: var(--background-modifier-hover);

View File

@@ -150,9 +150,10 @@ export const EmojiListItem: React.FC<{
guildId: string;
emoji: GuildEmojiWithUser;
layout: 'list' | 'grid';
canModify: boolean;
onRename: (emojiId: string, newName: string) => void;
onRemove: (emojiId: string) => void;
}> = observer(({guildId, emoji, layout, onRename, onRemove}) => {
}> = observer(({guildId, emoji, layout, canModify, onRename, onRemove}) => {
const {t} = useLingui();
const avatarUrl = emoji.user ? AvatarUtils.getUserAvatarURL(emoji.user, false) : null;
const gridNameButtonRef = useRef<HTMLButtonElement | null>(null);
@@ -222,38 +223,44 @@ export const EmojiListItem: React.FC<{
</div>
<div className={styles.gridName}>
<Popout
position="bottom"
offsetMainAxis={8}
offsetCrossAxis={0}
returnFocusRef={gridNameButtonRef}
render={({onClose}) => (
<EmojiRenamePopoutContent initialName={emoji.name} onSave={handleSave} onClose={onClose} />
)}
>
<button
type="button"
ref={gridNameButtonRef}
className={styles.gridNameButton}
aria-label={t`Rename :${emoji.name}:`}
{canModify ? (
<Popout
position="bottom"
offsetMainAxis={8}
offsetCrossAxis={0}
returnFocusRef={gridNameButtonRef}
render={({onClose}) => (
<EmojiRenamePopoutContent initialName={emoji.name} onSave={handleSave} onClose={onClose} />
)}
>
<span className={styles.gridNameText}>:{emoji.name}:</span>
</button>
</Popout>
<button
type="button"
ref={gridNameButtonRef}
className={styles.gridNameButton}
aria-label={t`Rename :${emoji.name}:`}
>
<span className={styles.gridNameText}>:{emoji.name}:</span>
</button>
</Popout>
) : (
<span className={styles.gridNameText}>:{emoji.name}:</span>
)}
</div>
</div>
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button
type="button"
onClick={handleDelete}
className={clsx(styles.deleteButton, styles.deleteButtonFloating)}
>
<XIcon className={styles.deleteIcon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
{canModify && (
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button
type="button"
onClick={handleDelete}
className={clsx(styles.deleteButton, styles.deleteButtonFloating)}
>
<XIcon className={styles.deleteIcon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
)}
</div>
);
}
@@ -266,17 +273,21 @@ export const EmojiListItem: React.FC<{
</div>
<div className={styles.listName}>
<InlineEdit
value={emoji.name}
onSave={handleSave}
prefix=":"
suffix=":"
maxLength={32}
width="100%"
className={styles.nameInlineEdit}
inputClassName={styles.nameInlineEditInput}
buttonClassName={styles.nameInlineEditButton}
/>
{canModify ? (
<InlineEdit
value={emoji.name}
onSave={handleSave}
prefix=":"
suffix=":"
maxLength={32}
width="100%"
className={styles.nameInlineEdit}
inputClassName={styles.nameInlineEditInput}
buttonClassName={styles.nameInlineEditButton}
/>
) : (
<span className={styles.nameInlineEdit}>:{emoji.name}:</span>
)}
</div>
<div className={styles.listUploader}>
@@ -293,13 +304,15 @@ export const EmojiListItem: React.FC<{
</div>
</div>
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleDelete} className={styles.deleteButton}>
<XIcon className={styles.deleteIcon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
{canModify && (
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleDelete} className={styles.deleteButton}>
<XIcon className={styles.deleteIcon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
)}
</div>
);
});

View File

@@ -37,7 +37,7 @@
}
.input::placeholder {
color: var(--text-primary-muted);
color: var(--text-tertiary);
}
.input.minHeight {
@@ -209,7 +209,7 @@
}
.textarea::placeholder {
color: var(--text-primary-muted);
color: var(--text-tertiary);
}
.textareaActions {

View File

@@ -121,14 +121,28 @@ const UserAreaInner = observer(
return;
}
const height = voiceConnectionRef.current?.getBoundingClientRect().height ?? 0;
if (height > 0) {
root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`);
} else {
const element = voiceConnectionRef.current;
if (!element) {
root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE);
return;
}
const updateHeight = () => {
const height = element.getBoundingClientRect().height;
if (height > 0) {
root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`);
} else {
root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE);
}
};
updateHeight();
const observer = new ResizeObserver(updateHeight);
observer.observe(element);
return () => {
observer.disconnect();
root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE);
};
}, [hasVoiceConnection]);
@@ -164,13 +178,13 @@ const UserAreaInner = observer(
return (
<div className={wrapperClassName}>
{hasVoiceConnection && (
<>
<div ref={voiceConnectionRef}>
<div className={styles.separator} aria-hidden />
<div ref={voiceConnectionRef} className={styles.voiceConnectionWrapper}>
<div className={styles.voiceConnectionWrapper}>
<VoiceConnectionStatus />
</div>
<div className={styles.separator} aria-hidden />
</>
</div>
)}
{!hasVoiceConnection && <div className={styles.separator} aria-hidden />}
<div className={styles.userAreaContainer}>

View File

@@ -93,7 +93,10 @@ export const AddConnectionModal = observer(({defaultType}: AddConnectionModalPro
const onSubmitInitiate = useCallback(
async (data: InitiateFormInputs) => {
const identifier = data.identifier.trim();
let identifier = data.identifier.trim();
if (type === ConnectionTypes.BLUESKY) {
identifier = identifier.replace(/^https?:\/\/bsky\.app\/profile\//i, '').replace(/^@/, '');
}
if (UserConnectionStore.hasConnectionByTypeAndName(type, identifier)) {
initiateForm.setError('identifier', {type: 'validate', message: t`You already have this connection.`});
return;

View File

@@ -56,8 +56,11 @@ export const GuildSettingsModal: React.FC<GuildSettingsModalProps> = observer(
if (!guild) return guildSettingsTabs;
return guildSettingsTabs.filter((tab) => {
if (tab.permission && !PermissionStore.can(tab.permission, {guildId})) {
return false;
if (tab.permission) {
const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission];
if (!perms.some((p) => PermissionStore.can(p, {guildId}))) {
return false;
}
}
if (tab.requireFeature && !guild.features.has(tab.requireFeature)) {
return false;

View File

@@ -192,9 +192,6 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => {
};
const getExpirationText = () => {
if (maxAge === '0') {
return <Trans>never expires</Trans>;
}
const option = maxAgeOptions.find((opt) => opt.value === maxAge);
if (option) {
switch (option.value) {
@@ -310,9 +307,16 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => {
onInputClick={(e) => e.currentTarget.select()}
inputProps={{placeholder: t`Invite link`}}
>
{isUsingVanityUrl ? (
{isUsingVanityUrl || maxAge === '0' ? (
<p className={styles.expirationText}>
<Trans>This invite link never expires.</Trans>
<Trans>This invite link never expires.</Trans>{' '}
{!isUsingVanityUrl && (
<FocusRing offset={-2}>
<button type="button" onClick={() => setShowAdvanced(true)} className={styles.editLink}>
<Trans>Edit invite link</Trans>
</button>
</FocusRing>
)}
</p>
) : (
<p className={styles.expirationText}>

View File

@@ -87,7 +87,7 @@
margin: 0;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-line-clamp: 4;
-webkit-box-orient: vertical;
flex: 1;
}

View File

@@ -19,11 +19,14 @@
import type {DiscoveryGuild} from '@app/actions/DiscoveryActionCreators';
import * as DiscoveryActionCreators from '@app/actions/DiscoveryActionCreators';
import * as ModalActionCreators from '@app/actions/ModalActionCreators';
import * as NavigationActionCreators from '@app/actions/NavigationActionCreators';
import * as ToastActionCreators from '@app/actions/ToastActionCreators';
import {GuildBadge} from '@app/components/guild/GuildBadge';
import styles from '@app/components/modals/discovery/DiscoveryGuildCard.module.css';
import {GuildIcon} from '@app/components/popouts/GuildIcon';
import {Button} from '@app/components/uikit/button/Button';
import DiscoveryStore from '@app/stores/DiscoveryStore';
import GuildStore from '@app/stores/GuildStore';
import {getApiErrorMessage} from '@app/utils/ApiErrorUtils';
import {getCurrentLocale} from '@app/utils/LocaleUtils';
@@ -51,6 +54,9 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}:
setJoining(true);
try {
await DiscoveryActionCreators.joinGuild(guild.id);
DiscoveryStore.reset();
ModalActionCreators.pop();
NavigationActionCreators.selectGuild(guild.id);
} catch (error) {
setJoining(false);
const message = getApiErrorMessage(error) ?? t`Failed to join this community. Please try again.`;

View File

@@ -121,22 +121,22 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
application.status === DiscoveryApplicationStatus.REJECTED ||
application.status === DiscoveryApplicationStatus.REMOVED);
const formValues = useMemo(
() =>
hasActiveApplication && application
? {description: application.description, category_type: application.category_type}
: undefined,
[hasActiveApplication, application],
);
const form = useForm<FormInputs>({
defaultValues: {
description: hasActiveApplication ? application.description : '',
category_type: hasActiveApplication ? application.category_type : 0,
description: '',
category_type: 0,
},
values: formValues,
});
useEffect(() => {
if (hasActiveApplication && application) {
form.reset({
description: application.description,
category_type: application.category_type,
});
}
}, [application, hasActiveApplication, form]);
const setApplicationFromResponse = useCallback((response: DiscoveryApplicationResponse) => {
setStatus((prev) => (prev ? {...prev, application: response} : prev));
}, []);
@@ -291,8 +291,10 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
<div className={styles.fieldLabel}>
<Trans>Description</Trans>
</div>
<Textarea
{...form.register('description', {
<Controller
name="description"
control={form.control}
rules={{
required: t`A description is required.`,
minLength: {
value: DISCOVERY_DESCRIPTION_MIN_LENGTH,
@@ -302,15 +304,24 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {
value: DISCOVERY_DESCRIPTION_MAX_LENGTH,
message: t`Description must be no more than ${DISCOVERY_DESCRIPTION_MAX_LENGTH} characters.`,
},
})}
error={form.formState.errors.description?.message}
label=""
placeholder={t`Describe what your community is about...`}
minRows={3}
maxRows={6}
maxLength={DISCOVERY_DESCRIPTION_MAX_LENGTH}
showCharacterCount
disabled={!eligible && canApply}
}}
render={({field, fieldState}) => (
<Textarea
name={field.name}
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
ref={field.ref}
error={fieldState.error?.message}
label=""
placeholder={t`Describe what your community is about...`}
minRows={3}
maxRows={6}
maxLength={DISCOVERY_DESCRIPTION_MAX_LENGTH}
showCharacterCount
disabled={!eligible && canApply}
/>
)}
/>
</div>

View File

@@ -33,11 +33,14 @@ import {Logger} from '@app/lib/Logger';
import EmojiStickerLayoutStore from '@app/stores/EmojiStickerLayoutStore';
import {seedGuildEmojiCache, subscribeToGuildEmojiUpdates} from '@app/stores/GuildExpressionTabCache';
import GuildStore from '@app/stores/GuildStore';
import PermissionStore from '@app/stores/PermissionStore';
import UserStore from '@app/stores/UserStore';
import {getApiErrorCode, getApiErrorErrors} from '@app/utils/ApiErrorUtils';
import {openFilePicker} from '@app/utils/FilePickerUtils';
import * as ImageCropUtils from '@app/utils/ImageCropUtils';
import {GlobalLimits} from '@app/utils/limits/GlobalLimits';
import {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import type {GuildEmojiWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import {sortBySnowflakeDesc} from '@fluxer/snowflake/src/SnowflakeUtils';
import {Trans, useLingui} from '@lingui/react/macro';
@@ -62,6 +65,10 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
const layout = layoutStore.getEmojiLayout();
const guild = GuildStore.getGuild(guildId);
const canCreateExpressions = PermissionStore.can(Permissions.CREATE_EXPRESSIONS, {guildId});
const canManageExpressions = PermissionStore.can(Permissions.MANAGE_EXPRESSIONS, {guildId});
const currentUserId = UserStore.currentUserId;
const setEmojisWithCache = useCallback(
(updater: React.SetStateAction<ReadonlyArray<GuildEmojiWithUser>>) => {
setEmojis((prev) => {
@@ -132,6 +139,15 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
return 50;
}, [guild]);
const canModifyEmoji = useCallback(
(emoji: GuildEmojiWithUser): boolean => {
if (canManageExpressions) return true;
if (canCreateExpressions && emoji.user?.id === currentUserId) return true;
return false;
},
[canManageExpressions, canCreateExpressions, currentUserId],
);
const handleEmojiDelete = useCallback(
async (emojiId: string) => {
try {
@@ -322,45 +338,50 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
</div>
)}
<UploadSlotInfo
title={<Trans>Emoji Slots</Trans>}
currentCount={staticEmojis.length}
maxCount={maxStaticEmojis}
uploadButtonText={<Trans>Upload Emoji</Trans>}
onUploadClick={async () => {
const files = await openFilePicker({
multiple: true,
accept: '.jpg,.jpeg,.png,.apng,.gif,.webp,.avif,image/*',
});
if (files.length > 0) {
void handleFileSelect(files);
}
}}
description={
<Trans>
Emoji names must be at least 2 characters long and can only contain alphanumeric characters and underscores.
Allowed file types: JPEG, PNG, WebP, GIF. We compress images to 128x128 pixels. Maximum size:{' '}
{Math.round(GlobalLimits.getEmojiMaxSize() / 1024)} KB per emoji.
</Trans>
}
additionalSlots={
<>
<span>
{canCreateExpressions && (
<>
<UploadSlotInfo
title={<Trans>Emoji Slots</Trans>}
currentCount={staticEmojis.length}
maxCount={maxStaticEmojis}
uploadButtonText={<Trans>Upload Emoji</Trans>}
onUploadClick={async () => {
const files = await openFilePicker({
multiple: true,
accept: '.jpg,.jpeg,.png,.apng,.gif,.webp,.avif,image/*',
});
if (files.length > 0) {
void handleFileSelect(files);
}
}}
description={
<Trans>
Static: {staticEmojis.length} / {maxStaticEmojis === Number.POSITIVE_INFINITY ? '' : maxStaticEmojis}
Emoji names must be at least 2 characters long and can only contain alphanumeric characters and
underscores. Allowed file types: JPEG, PNG, WebP, GIF. We compress images to 128x128 pixels. Maximum
size: {Math.round(GlobalLimits.getEmojiMaxSize() / 1024)} KB per emoji.
</Trans>
</span>
<span>
<Trans>
Animated: {animatedEmojis.length} /{' '}
{maxAnimatedEmojis === Number.POSITIVE_INFINITY ? '' : maxAnimatedEmojis}
</Trans>
</span>
</>
}
/>
}
additionalSlots={
<>
<span>
<Trans>
Static: {staticEmojis.length} /{' '}
{maxStaticEmojis === Number.POSITIVE_INFINITY ? '' : maxStaticEmojis}
</Trans>
</span>
<span>
<Trans>
Animated: {animatedEmojis.length} /{' '}
{maxAnimatedEmojis === Number.POSITIVE_INFINITY ? '' : maxAnimatedEmojis}
</Trans>
</span>
</>
}
/>
<UploadDropZone onDrop={handleDrop} description={<Trans>Drag and drop emoji files here</Trans>} />
<UploadDropZone onDrop={handleDrop} description={<Trans>Drag and drop emoji files here</Trans>} />
</>
)}
{searchQuery && filteredEmojis.length === 0 && (
<div className={styles.notice}>
@@ -391,6 +412,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
guildId={guildId}
emoji={emoji}
layout={layout}
canModify={canModifyEmoji(emoji)}
onRename={handleEmojiRename}
onRemove={handleEmojiDelete}
/>
@@ -411,6 +433,7 @@ const GuildEmojiTab: React.FC<{guildId: string}> = observer(function GuildEmojiT
guildId={guildId}
emoji={emoji}
layout={layout}
canModify={canModifyEmoji(emoji)}
onRename={handleEmojiRename}
onRemove={handleEmojiDelete}
/>

View File

@@ -32,8 +32,11 @@ import {Logger} from '@app/lib/Logger';
import EmojiStickerLayoutStore from '@app/stores/EmojiStickerLayoutStore';
import {seedGuildStickerCache, subscribeToGuildStickerUpdates} from '@app/stores/GuildExpressionTabCache';
import GuildStore from '@app/stores/GuildStore';
import PermissionStore from '@app/stores/PermissionStore';
import UserStore from '@app/stores/UserStore';
import {openFilePicker} from '@app/utils/FilePickerUtils';
import {GlobalLimits} from '@app/utils/limits/GlobalLimits';
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
import type {GuildStickerWithUser} from '@fluxer/schema/src/domains/guild/GuildEmojiSchemas';
import {sortBySnowflakeDesc} from '@fluxer/snowflake/src/SnowflakeUtils';
import {Trans, useLingui} from '@lingui/react/macro';
@@ -54,6 +57,11 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
const layoutStore = EmojiStickerLayoutStore;
const viewMode = layoutStore.getStickerViewMode();
const guild = GuildStore.getGuild(guildId);
const canCreateExpressions = PermissionStore.can(Permissions.CREATE_EXPRESSIONS, {guildId});
const canManageExpressions = PermissionStore.can(Permissions.MANAGE_EXPRESSIONS, {guildId});
const currentUserId = UserStore.currentUserId;
const setStickersWithCache = useCallback(
(updater: React.SetStateAction<ReadonlyArray<GuildStickerWithUser>>) => {
setStickers((prev) => {
@@ -120,6 +128,15 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
});
}, [stickers, searchQuery]);
const canModifySticker = useCallback(
(sticker: GuildStickerWithUser): boolean => {
if (canManageExpressions) return true;
if (canCreateExpressions && sticker.user?.id === currentUserId) return true;
return false;
},
[canManageExpressions, canCreateExpressions, currentUserId],
);
const maxStickers = guild?.maxStickers ?? 50;
return (
@@ -154,25 +171,29 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
</div>
</div>
<UploadSlotInfo
title={<Trans>Sticker Slots</Trans>}
currentCount={stickers.length}
maxCount={maxStickers}
uploadButtonText={<Trans>Upload Sticker</Trans>}
onUploadClick={handleAddSticker}
description={
<Trans>
Stickers must be exactly 320x320 pixels and no larger than{' '}
{Math.round(GlobalLimits.getStickerMaxSize() / 1024)} KB, but we automatically resize and compress images
for you. Allowed file types: JPEG, PNG, WebP, GIF.
</Trans>
}
/>
<UploadDropZone
onDrop={handleDrop}
description={<Trans>Drag and drop a sticker file here (one at a time)</Trans>}
acceptMultiple={false}
/>
{canCreateExpressions && (
<>
<UploadSlotInfo
title={<Trans>Sticker Slots</Trans>}
currentCount={stickers.length}
maxCount={maxStickers}
uploadButtonText={<Trans>Upload Sticker</Trans>}
onUploadClick={handleAddSticker}
description={
<Trans>
Stickers must be exactly 320x320 pixels and no larger than{' '}
{Math.round(GlobalLimits.getStickerMaxSize() / 1024)} KB, but we automatically resize and compress
images for you. Allowed file types: JPEG, PNG, WebP, GIF.
</Trans>
}
/>
<UploadDropZone
onDrop={handleDrop}
description={<Trans>Drag and drop a sticker file here (one at a time)</Trans>}
acceptMultiple={false}
/>
</>
)}
{fetchStatus === 'pending' && (
<div className={styles.spinnerContainer}>
@@ -192,7 +213,13 @@ const GuildStickersTab: React.FC<{guildId: string}> = observer(function GuildSti
{fetchStatus === 'success' && filteredStickers.length > 0 && (
<div className={clsx(styles.stickerGrid, viewMode === 'compact' ? styles.compactGrid : styles.cozyGrid)}>
{filteredStickers.map((sticker) => (
<StickerGridItem key={sticker.id} guildId={guildId} sticker={sticker} onUpdate={fetchStickers} />
<StickerGridItem
key={sticker.id}
guildId={guildId}
sticker={sticker}
canModify={canModifySticker(sticker)}
onUpdate={fetchStickers}
/>
))}
</div>
)}

View File

@@ -72,7 +72,7 @@ export interface GuildSettingsTab {
icon: Icon;
iconWeight?: IconWeight;
component: React.ComponentType<{guildId: string}>;
permission?: bigint;
permission?: bigint | ReadonlyArray<bigint>;
requireFeature?: string;
}
@@ -83,7 +83,7 @@ interface GuildSettingsTabDescriptor {
icon: Icon;
iconWeight?: IconWeight;
component: React.ComponentType<{guildId: string}>;
permission?: bigint;
permission?: bigint | ReadonlyArray<bigint>;
requireFeature?: string;
}
@@ -110,7 +110,7 @@ const GUILD_SETTINGS_TABS_DESCRIPTORS: Array<GuildSettingsTabDescriptor> = [
label: msg`Custom Emoji`,
icon: SmileyIcon,
component: GuildEmojiTab,
permission: Permissions.MANAGE_EXPRESSIONS,
permission: [Permissions.CREATE_EXPRESSIONS, Permissions.MANAGE_EXPRESSIONS],
},
{
type: 'stickers',
@@ -118,7 +118,7 @@ const GUILD_SETTINGS_TABS_DESCRIPTORS: Array<GuildSettingsTabDescriptor> = [
label: msg`Custom Stickers`,
icon: StickerIcon,
component: GuildStickersTab,
permission: Permissions.MANAGE_EXPRESSIONS,
permission: [Permissions.CREATE_EXPRESSIONS, Permissions.MANAGE_EXPRESSIONS],
},
{
type: 'moderation',

View File

@@ -38,10 +38,16 @@ import {observer} from 'mobx-react-lite';
interface StickerGridItemProps {
guildId: string;
sticker: GuildStickerWithUser;
canModify: boolean;
onUpdate: () => void;
}
export const StickerGridItem = observer(function StickerGridItem({guildId, sticker, onUpdate}: StickerGridItemProps) {
export const StickerGridItem = observer(function StickerGridItem({
guildId,
sticker,
canModify,
onUpdate,
}: StickerGridItemProps) {
const {t} = useLingui();
const {shouldAnimate} = useStickerAnimation();
@@ -107,23 +113,25 @@ export const StickerGridItem = observer(function StickerGridItem({guildId, stick
)}
</div>
<div className={styles.actions}>
<Tooltip text={t`Edit`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleEdit} className={styles.actionButton}>
<PencilIcon className={styles.icon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
{canModify && (
<div className={styles.actions}>
<Tooltip text={t`Edit`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleEdit} className={styles.actionButton}>
<PencilIcon className={styles.icon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleDelete} className={clsx(styles.actionButton, styles.deleteButton)}>
<XIcon className={styles.icon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
</div>
<Tooltip text={t`Delete`}>
<FocusRing offset={-2}>
<button type="button" onClick={handleDelete} className={clsx(styles.actionButton, styles.deleteButton)}>
<XIcon className={styles.icon} weight="bold" />
</button>
</FocusRing>
</Tooltip>
</div>
)}
</div>
);
});

View File

@@ -228,8 +228,11 @@ export function useGuildMenuData(guild: GuildRecord, options: UseGuildMenuDataOp
const availableSettingsTabs = useMemo(() => {
const allTabs = getGuildSettingsTabs(i18n);
return allTabs.filter((tab) => {
if (tab.permission && !PermissionStore.can(tab.permission, {guildId: guild.id})) {
return false;
if (tab.permission) {
const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission];
if (!perms.some((p) => PermissionStore.can(p, {guildId: guild.id}))) {
return false;
}
}
if (tab.requireFeature && !guild.features.has(tab.requireFeature)) {
return false;

View File

@@ -311,8 +311,11 @@ export const CommunitySettingsMenuItem: React.FC<GuildMenuItemProps> = observer(
const accessibleTabs = useMemo(() => {
const guildTabs = getGuildSettingsTabs(i18n);
return guildTabs.filter((tab) => {
if (tab.permission && !PermissionStore.can(tab.permission, {guildId: guild.id})) {
return false;
if (tab.permission) {
const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission];
if (!perms.some((p) => PermissionStore.can(p, {guildId: guild.id}))) {
return false;
}
}
if (tab.requireFeature && !guild.features.has(tab.requireFeature)) {
return false;