fix: various fixes to sentry-reported errors and more
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user