[skip ci] feat: prepare for public release
This commit is contained in:
@@ -23,8 +23,11 @@ import {PhoneIcon, VideoCameraIcon} from '@phosphor-icons/react';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as CallActionCreators from '~/actions/CallActionCreators';
|
||||
import * as ToastActionCreators from '~/actions/ToastActionCreators';
|
||||
import {ChannelTypes} from '~/Constants';
|
||||
import type {ChannelRecord} from '~/records/ChannelRecord';
|
||||
import CallStateStore from '~/stores/CallStateStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import MediaEngineStore from '~/stores/voice/MediaEngineFacade';
|
||||
import * as CallUtils from '~/utils/CallUtils';
|
||||
import {ChannelHeaderIcon} from './ChannelHeaderIcon';
|
||||
@@ -38,9 +41,20 @@ const VoiceCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
const hasActiveCall = CallStateStore.hasActiveCall(channel.id);
|
||||
const participants = call ? CallStateStore.getParticipants(channel.id) : [];
|
||||
const participantCount = participants.length;
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const isUnclaimed = !(currentUser?.isClaimed() ?? false);
|
||||
const is1to1 = channel.type === ChannelTypes.DM;
|
||||
const blocked = isUnclaimed && is1to1;
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
async (event: React.MouseEvent) => {
|
||||
if (blocked) {
|
||||
ToastActionCreators.createToast({
|
||||
type: 'error',
|
||||
children: t`Claim your account to start or join 1:1 calls.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isInCall) {
|
||||
void CallActionCreators.leaveCall(channel.id);
|
||||
} else if (hasActiveCall) {
|
||||
@@ -50,7 +64,7 @@ const VoiceCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
await CallUtils.checkAndStartCall(channel.id, silent);
|
||||
}
|
||||
},
|
||||
[channel.id, isInCall, hasActiveCall],
|
||||
[channel.id, isInCall, hasActiveCall, blocked],
|
||||
);
|
||||
|
||||
let label: string;
|
||||
@@ -67,7 +81,13 @@ const VoiceCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
: t`Join Voice Call (${participantCount} participants)`;
|
||||
}
|
||||
} else {
|
||||
label = isInCall ? t`Leave Voice Call` : hasActiveCall ? t`Join Voice Call` : t`Start Voice Call`;
|
||||
label = blocked
|
||||
? t`Claim your account to call`
|
||||
: isInCall
|
||||
? t`Leave Voice Call`
|
||||
: hasActiveCall
|
||||
? t`Join Voice Call`
|
||||
: t`Start Voice Call`;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -76,6 +96,7 @@ const VoiceCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
label={label}
|
||||
isSelected={isInCall}
|
||||
onClick={handleClick}
|
||||
disabled={blocked}
|
||||
keybindAction="start_pm_call"
|
||||
/>
|
||||
);
|
||||
@@ -90,9 +111,20 @@ const VideoCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
const hasActiveCall = CallStateStore.hasActiveCall(channel.id);
|
||||
const participants = call ? CallStateStore.getParticipants(channel.id) : [];
|
||||
const participantCount = participants.length;
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
const isUnclaimed = !(currentUser?.isClaimed() ?? false);
|
||||
const is1to1 = channel.type === ChannelTypes.DM;
|
||||
const blocked = isUnclaimed && is1to1;
|
||||
|
||||
const handleClick = React.useCallback(
|
||||
async (event: React.MouseEvent) => {
|
||||
if (blocked) {
|
||||
ToastActionCreators.createToast({
|
||||
type: 'error',
|
||||
children: t`Claim your account to start or join 1:1 calls.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isInCall) {
|
||||
void CallActionCreators.leaveCall(channel.id);
|
||||
} else if (hasActiveCall) {
|
||||
@@ -102,7 +134,7 @@ const VideoCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
await CallUtils.checkAndStartCall(channel.id, silent);
|
||||
}
|
||||
},
|
||||
[channel.id, isInCall, hasActiveCall],
|
||||
[channel.id, isInCall, hasActiveCall, blocked],
|
||||
);
|
||||
|
||||
let label: string;
|
||||
@@ -119,10 +151,24 @@ const VideoCallButton = observer(({channel}: {channel: ChannelRecord}) => {
|
||||
: t`Join Video Call (${participantCount} participants)`;
|
||||
}
|
||||
} else {
|
||||
label = isInCall ? t`Leave Video Call` : hasActiveCall ? t`Join Video Call` : t`Start Video Call`;
|
||||
label = blocked
|
||||
? t`Claim your account to call`
|
||||
: isInCall
|
||||
? t`Leave Video Call`
|
||||
: hasActiveCall
|
||||
? t`Join Video Call`
|
||||
: t`Start Video Call`;
|
||||
}
|
||||
|
||||
return <ChannelHeaderIcon icon={VideoCameraIcon} label={label} isSelected={isInCall} onClick={handleClick} />;
|
||||
return (
|
||||
<ChannelHeaderIcon
|
||||
icon={VideoCameraIcon}
|
||||
label={label}
|
||||
isSelected={isInCall}
|
||||
onClick={handleClick}
|
||||
disabled={blocked}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const CallButtons = {
|
||||
|
||||
@@ -33,7 +33,7 @@ import type {UserRecord} from '~/records/UserRecord';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import MemberSidebarStore from '~/stores/MemberSidebarStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import type {GroupDMMemberGroup, MemberGroup} from '~/utils/MemberListUtils';
|
||||
import type {GroupDMMemberGroup} from '~/utils/MemberListUtils';
|
||||
import * as MemberListUtils from '~/utils/MemberListUtils';
|
||||
import * as NicknameUtils from '~/utils/NicknameUtils';
|
||||
import styles from './ChannelMembers.module.css';
|
||||
@@ -65,35 +65,6 @@ const SkeletonMemberItem = ({index}: {index: number}) => {
|
||||
);
|
||||
};
|
||||
|
||||
const _MemberListGroup = observer(
|
||||
({guild, group, channelId}: {guild: GuildRecord; group: MemberGroup; channelId: string}) => (
|
||||
<div className={styles.groupContainer}>
|
||||
<div className={styles.groupHeader}>
|
||||
{group.displayName} — {group.count}
|
||||
</div>
|
||||
<div className={styles.membersList}>
|
||||
{group.members.map((member: GuildMemberRecord) => {
|
||||
const user = member.user;
|
||||
const userId = user.id;
|
||||
return (
|
||||
<MemberListItem
|
||||
key={userId}
|
||||
user={user}
|
||||
channelId={channelId}
|
||||
guildId={guild.id}
|
||||
isOwner={guild.isOwner(userId)}
|
||||
roleColor={member.getColorString?.() ?? undefined}
|
||||
displayName={NicknameUtils.getNickname(user, guild.id)}
|
||||
disableBackdrop={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.groupSpacer} />
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
interface GroupDMMemberListGroupProps {
|
||||
group: GroupDMMemberGroup;
|
||||
channelId: string;
|
||||
|
||||
@@ -40,9 +40,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import * as ContextMenuActionCreators from '~/actions/ContextMenuActionCreators';
|
||||
import {MessagePreviewContext} from '~/Constants';
|
||||
import {Message, type MessageBehaviorOverrides} from '~/components/channel/Message';
|
||||
import {GroupDMAvatar} from '~/components/common/GroupDMAvatar';
|
||||
import {MessageContextPrefix} from '~/components/shared/MessageContextPrefix/MessageContextPrefix';
|
||||
import {Avatar} from '~/components/uikit/Avatar';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import {ContextMenuCloseProvider} from '~/components/uikit/ContextMenu/ContextMenu';
|
||||
import {MenuGroup} from '~/components/uikit/ContextMenu/MenuGroup';
|
||||
@@ -58,9 +56,7 @@ import ChannelSearchStore, {getChannelSearchContextId} from '~/stores/ChannelSea
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import GuildNSFWAgreeStore from '~/stores/GuildNSFWAgreeStore';
|
||||
import GuildStore from '~/stores/GuildStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {applyChannelSearchHighlight, clearChannelSearchHighlight} from '~/utils/ChannelSearchHighlight';
|
||||
import * as ChannelUtils from '~/utils/ChannelUtils';
|
||||
import {goToMessage} from '~/utils/MessageNavigator';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
import {tokenizeSearchQuery} from '~/utils/SearchQueryTokenizer';
|
||||
@@ -78,14 +74,6 @@ import type {SearchMachineState} from './SearchResultsUtils';
|
||||
import {areSegmentsEqual} from './SearchResultsUtils';
|
||||
import {DEFAULT_SCOPE_VALUE, getScopeOptionsForChannel} from './searchScopeOptions';
|
||||
|
||||
const getChannelDisplayName = (channel: ChannelRecord): string => {
|
||||
if (channel.isPrivate()) {
|
||||
return ChannelUtils.getDMDisplayName(channel);
|
||||
}
|
||||
|
||||
return channel.name?.trim() || ChannelUtils.getName(channel);
|
||||
};
|
||||
|
||||
const getChannelGuild = (channel: ChannelRecord): GuildRecord | null => {
|
||||
if (!channel.guildId) {
|
||||
return null;
|
||||
@@ -101,37 +89,6 @@ const getChannelPath = (channel: ChannelRecord): string => {
|
||||
return Routes.dmChannel(channel.id);
|
||||
};
|
||||
|
||||
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});
|
||||
};
|
||||
|
||||
interface ChannelSearchResultsProps {
|
||||
channel: ChannelRecord;
|
||||
searchQuery: string;
|
||||
@@ -753,7 +710,6 @@ export const ChannelSearchResults = observer(
|
||||
}
|
||||
|
||||
const channelGuild = getChannelGuild(messageChannel);
|
||||
const _channelDisplayName = getChannelDisplayName(messageChannel);
|
||||
const showGuildMeta = shouldShowGuildMetaForScope(
|
||||
channelGuild,
|
||||
(activeScope ?? DEFAULT_SCOPE_VALUE) as MessageSearchScope,
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '~/components/embeds/EmbedCard/EmbedCard';
|
||||
import cardStyles from '~/components/embeds/EmbedCard/EmbedCard.module.css';
|
||||
import {useEmbedSkeletonOverride} from '~/components/embeds/EmbedCard/useEmbedSkeletonOverride';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import i18n from '~/i18n';
|
||||
import {ComponentDispatch} from '~/lib/ComponentDispatch';
|
||||
@@ -48,6 +49,7 @@ export const GiftEmbed = observer(function GiftEmbed({code}: GiftEmbedProps) {
|
||||
const giftState = GiftStore.gifts.get(code) ?? null;
|
||||
const gift = giftState?.data;
|
||||
const creator = UserStore.getUser(gift?.created_by?.id ?? '');
|
||||
const isUnclaimed = !(UserStore.currentUser?.isClaimed() ?? false);
|
||||
const shouldForceSkeleton = useEmbedSkeletonOverride();
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -76,6 +78,10 @@ export const GiftEmbed = observer(function GiftEmbed({code}: GiftEmbedProps) {
|
||||
const durationText = getGiftDurationText(i18n, gift);
|
||||
|
||||
const handleRedeem = async () => {
|
||||
if (isUnclaimed) {
|
||||
openClaimAccountModal({force: true});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await GiftActionCreators.redeem(i18n, code);
|
||||
} catch (error) {
|
||||
@@ -87,17 +93,22 @@ export const GiftEmbed = observer(function GiftEmbed({code}: GiftEmbedProps) {
|
||||
<span className={styles.subRow}>{t`From ${creator.username}#${creator.discriminator}`}</span>
|
||||
) : undefined;
|
||||
|
||||
const helpText = gift.redeemed ? t`Already redeemed` : t`Click to claim your gift!`;
|
||||
const helpText = gift.redeemed
|
||||
? t`Already redeemed`
|
||||
: isUnclaimed
|
||||
? t`Claim your account to redeem this gift.`
|
||||
: t`Click to claim your gift!`;
|
||||
|
||||
const footer = gift.redeemed ? (
|
||||
<Button variant="primary" matchSkeletonHeight disabled>
|
||||
{t`Gift Claimed`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" matchSkeletonHeight onClick={handleRedeem}>
|
||||
{t`Claim Gift`}
|
||||
</Button>
|
||||
);
|
||||
const footer =
|
||||
gift.redeemed && !isUnclaimed ? (
|
||||
<Button variant="primary" matchSkeletonHeight disabled>
|
||||
{t`Gift Claimed`}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="primary" matchSkeletonHeight onClick={handleRedeem} disabled={gift.redeemed || isUnclaimed}>
|
||||
{gift.redeemed ? t`Gift Claimed` : isUnclaimed ? t`Claim Account to Redeem` : t`Claim Gift`}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmbedCard
|
||||
|
||||
@@ -108,7 +108,6 @@ const MessageReactionItem = observer(
|
||||
|
||||
const emojiName = getEmojiName(reaction.emoji);
|
||||
const emojiUrl = useEmojiURL({emoji: reaction.emoji, isHovering});
|
||||
const _emojiIdentifier = reaction.emoji.id ?? reaction.emoji.name;
|
||||
const isUnicodeEmoji = reaction.emoji.id == null;
|
||||
|
||||
const variants = {
|
||||
|
||||
@@ -280,9 +280,7 @@ 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;
|
||||
|
||||
if (scrollManager.isPinned()) {
|
||||
scrollManager.handleScroll();
|
||||
}
|
||||
scrollManager.handleScroll();
|
||||
};
|
||||
|
||||
const onFocusBottommostMessage = (payload?: unknown) => {
|
||||
|
||||
@@ -23,7 +23,7 @@ import {observer} from 'mobx-react-lite';
|
||||
import {useEffect, useState} from 'react';
|
||||
import * as ModalActionCreators from '~/actions/ModalActionCreators';
|
||||
import {modal} from '~/actions/ModalActionCreators';
|
||||
import {ClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {PhoneAddModal} from '~/components/modals/PhoneAddModal';
|
||||
import {UserSettingsModal} from '~/components/modals/UserSettingsModal';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
@@ -103,7 +103,7 @@ export const UnclaimedAccountBarrier = observer(({onAction}: BarrierProps) => {
|
||||
small={true}
|
||||
onClick={() => {
|
||||
onAction?.();
|
||||
ModalActionCreators.push(modal(() => <ClaimAccountModal />));
|
||||
openClaimAccountModal({force: true});
|
||||
}}
|
||||
>
|
||||
<Trans>Claim Account</Trans>
|
||||
@@ -234,7 +234,7 @@ export const UnclaimedDMBarrier = observer(({onAction}: BarrierProps) => {
|
||||
small={true}
|
||||
onClick={() => {
|
||||
onAction?.();
|
||||
ModalActionCreators.push(modal(() => <ClaimAccountModal />));
|
||||
openClaimAccountModal({force: true});
|
||||
}}
|
||||
>
|
||||
<Trans>Claim Account</Trans>
|
||||
|
||||
@@ -205,6 +205,7 @@ export const DMChannelView = observer(({channelId}: DMChannelViewProps) => {
|
||||
const title = isDM && displayName ? `@${displayName}` : displayName;
|
||||
useFluxerDocumentTitle(title);
|
||||
const isGroupDM = channel?.type === ChannelTypes.GROUP_DM;
|
||||
const isPersonalNotes = channel?.type === ChannelTypes.DM_PERSONAL_NOTES;
|
||||
const callHeaderState = useCallHeaderState(channel);
|
||||
const call = callHeaderState.call;
|
||||
const showCompactVoiceView = callHeaderState.controlsVariant === 'inCall';
|
||||
@@ -411,7 +412,7 @@ export const DMChannelView = observer(({channelId}: DMChannelViewProps) => {
|
||||
textarea={
|
||||
isDM && isRecipientBlocked && recipient ? (
|
||||
<BlockedUserBarrier userId={recipient.id} username={recipient.username} />
|
||||
) : isCurrentUserUnclaimed ? (
|
||||
) : isCurrentUserUnclaimed && isDM && !isPersonalNotes && !isGroupDM ? (
|
||||
<UnclaimedDMBarrier />
|
||||
) : (
|
||||
<ChannelTextarea channel={channel} />
|
||||
|
||||
@@ -17,15 +17,19 @@
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {Trans, useLingui} from '@lingui/react/macro';
|
||||
import {WarningCircleIcon} from '@phosphor-icons/react';
|
||||
import {clsx} from 'clsx';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as RelationshipActionCreators from '~/actions/RelationshipActionCreators';
|
||||
import {APIErrorCodes} from '~/Constants';
|
||||
import {Input} from '~/components/form/Input';
|
||||
import {openClaimAccountModal} from '~/components/modals/ClaimAccountModal';
|
||||
import {StatusSlate} from '~/components/modals/shared/StatusSlate';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {getApiErrorCode} from '~/utils/ApiErrorUtils';
|
||||
import styles from './AddFriendForm.module.css';
|
||||
|
||||
@@ -35,11 +39,30 @@ interface AddFriendFormProps {
|
||||
|
||||
export const AddFriendForm: React.FC<AddFriendFormProps> = observer(({onSuccess}) => {
|
||||
const {t} = useLingui();
|
||||
|
||||
const [input, setInput] = React.useState('');
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [resultStatus, setResultStatus] = React.useState<'success' | 'error' | null>(null);
|
||||
const [errorCode, setErrorCode] = React.useState<string | null>(null);
|
||||
|
||||
const isClaimed = UserStore.currentUser?.isClaimed() ?? true;
|
||||
if (!isClaimed) {
|
||||
return (
|
||||
<StatusSlate
|
||||
Icon={WarningCircleIcon}
|
||||
title={<Trans>Claim your account</Trans>}
|
||||
description={<Trans>Claim your account to send friend requests.</Trans>}
|
||||
actions={[
|
||||
{
|
||||
text: <Trans>Claim Account</Trans>,
|
||||
onClick: () => openClaimAccountModal({force: true}),
|
||||
variant: 'primary',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const parseInput = (input: string): [string, string] => {
|
||||
const parts = input.split('#');
|
||||
if (parts.length > 1) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {AvatarStack} from '~/components/uikit/avatars/AvatarStack';
|
||||
import {Button} from '~/components/uikit/Button/Button';
|
||||
import FocusRing from '~/components/uikit/FocusRing/FocusRing';
|
||||
import {StatusAwareAvatar} from '~/components/uikit/StatusAwareAvatar';
|
||||
import {Tooltip} from '~/components/uikit/Tooltip/Tooltip';
|
||||
import type {ProfileRecord} from '~/records/ProfileRecord';
|
||||
import GuildMemberStore from '~/stores/GuildMemberStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
@@ -92,6 +93,7 @@ export const DMWelcomeSection: React.FC<DMWelcomeSectionProps> = observer(functi
|
||||
};
|
||||
|
||||
const hasMutualGuilds = mutualGuilds.length > 0;
|
||||
const currentUserUnclaimed = !(UserStore.currentUser?.isClaimed() ?? true);
|
||||
const shouldShowActionButton =
|
||||
!user.bot &&
|
||||
(relationshipType === undefined ||
|
||||
@@ -103,12 +105,22 @@ export const DMWelcomeSection: React.FC<DMWelcomeSectionProps> = observer(functi
|
||||
const renderActionButton = () => {
|
||||
if (user.bot) return null;
|
||||
switch (relationshipType) {
|
||||
case undefined:
|
||||
return (
|
||||
<Button small={true} onClick={handleSendFriendRequest}>
|
||||
case undefined: {
|
||||
const tooltipText = t`Claim your account to send friend requests.`;
|
||||
const button = (
|
||||
<Button small={true} onClick={handleSendFriendRequest} disabled={currentUserUnclaimed}>
|
||||
<Trans>Send Friend Request</Trans>
|
||||
</Button>
|
||||
);
|
||||
if (currentUserUnclaimed) {
|
||||
return (
|
||||
<Tooltip text={tooltipText} maxWidth="xl">
|
||||
<div>{button}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
case RelationshipTypes.INCOMING_REQUEST:
|
||||
return (
|
||||
<div className={styles.actionButtonsContainer}>
|
||||
|
||||
@@ -457,8 +457,8 @@ export const EmbedGifv: FC<
|
||||
const {width, aspectRatio} = style;
|
||||
const containerStyle = {
|
||||
'--embed-width': `${width}px`,
|
||||
maxWidth: `${width}px`,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
width,
|
||||
aspectRatio,
|
||||
} as React.CSSProperties;
|
||||
|
||||
@@ -488,7 +488,7 @@ export const EmbedGifv: FC<
|
||||
type="gifv"
|
||||
handlePress={openImagePreview}
|
||||
>
|
||||
<div className={styles.videoWrapper}>
|
||||
<div className={styles.videoWrapper} style={aspectRatio ? {aspectRatio} : undefined}>
|
||||
{(!loaded || error) && thumbHashURL && (
|
||||
<img src={thumbHashURL} className={styles.thumbHashPlaceholder} alt={t`Loading placeholder`} />
|
||||
)}
|
||||
@@ -551,6 +551,7 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
|
||||
const {shouldBlur, gateReason} = useNSFWMedia(nsfw, channelId);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const isHoveredRef = useRef(false);
|
||||
|
||||
const defaultName = deriveDefaultNameFromMessage({
|
||||
message,
|
||||
@@ -622,16 +623,21 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
|
||||
useEffect(() => {
|
||||
if (gifAutoPlay) return;
|
||||
|
||||
const img = imgRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!img || !container) return;
|
||||
if (!container) return;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (FocusManager.isFocused() && img) {
|
||||
img.src = optimizedAnimatedURL;
|
||||
isHoveredRef.current = true;
|
||||
if (FocusManager.isFocused()) {
|
||||
const img = imgRef.current;
|
||||
if (img) {
|
||||
img.src = optimizedAnimatedURL;
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
isHoveredRef.current = false;
|
||||
const img = imgRef.current;
|
||||
if (img) {
|
||||
img.src = optimizedStaticURL;
|
||||
}
|
||||
@@ -646,6 +652,24 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
|
||||
};
|
||||
}, [gifAutoPlay, optimizedAnimatedURL, optimizedStaticURL]);
|
||||
|
||||
useEffect(() => {
|
||||
if (gifAutoPlay) return;
|
||||
|
||||
const unsubscribe = FocusManager.subscribe((focused) => {
|
||||
const img = imgRef.current;
|
||||
if (!img) return;
|
||||
if (!focused) {
|
||||
img.src = optimizedStaticURL;
|
||||
return;
|
||||
}
|
||||
if (isHoveredRef.current && focused) {
|
||||
img.src = optimizedAnimatedURL;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [gifAutoPlay, optimizedAnimatedURL, optimizedStaticURL]);
|
||||
|
||||
if (shouldBlur) {
|
||||
const {style} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true});
|
||||
const {width: _width, height: _height, ...styleWithoutDimensions} = style;
|
||||
@@ -679,8 +703,8 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
|
||||
const {width, aspectRatio} = style;
|
||||
const containerStyle = {
|
||||
'--embed-width': `${width}px`,
|
||||
maxWidth: `${width}px`,
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
width,
|
||||
aspectRatio,
|
||||
} as React.CSSProperties;
|
||||
|
||||
@@ -716,7 +740,7 @@ export const EmbedGif: FC<GifvEmbedProps & {proxyURL: string; includeButton?: bo
|
||||
contentHash={contentHash}
|
||||
message={message}
|
||||
>
|
||||
<div className={styles.videoWrapper}>
|
||||
<div className={styles.videoWrapper} style={aspectRatio ? {aspectRatio} : undefined}>
|
||||
{(!loaded || error) && thumbHashURL && (
|
||||
<img src={thumbHashURL} className={styles.thumbHashPlaceholder} alt={t`Loading placeholder`} />
|
||||
)}
|
||||
|
||||
@@ -251,6 +251,11 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
|
||||
aspectRatio: true,
|
||||
},
|
||||
);
|
||||
const resolvedContainerStyle: React.CSSProperties = {
|
||||
...containerStyle,
|
||||
width: dimensions.width,
|
||||
maxWidth: '100%',
|
||||
};
|
||||
|
||||
const shouldRenderPlaceholder = error || !loaded;
|
||||
|
||||
@@ -295,7 +300,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
|
||||
<div className={styles.blurContainer}>
|
||||
<div className={clsx(styles.rowContainer, isInline && styles.justifyEnd)}>
|
||||
<div className={styles.innerContainer}>
|
||||
<div className={styles.imageWrapper} style={containerStyle}>
|
||||
<div className={styles.imageWrapper} style={resolvedContainerStyle}>
|
||||
<div className={styles.imageContainer}>
|
||||
{thumbHashURL && (
|
||||
<div className={styles.thumbHashContainer}>
|
||||
@@ -328,7 +333,7 @@ export const EmbedImage: FC<EmbedImageProps> = observer(
|
||||
<div className={clsx(styles.rowContainer, isInline && styles.justifyEnd)}>
|
||||
<MediaContainer
|
||||
className={clsx(styles.mediaContainer, styles.cursorPointer)}
|
||||
style={containerStyle}
|
||||
style={resolvedContainerStyle}
|
||||
showFavoriteButton={showFavoriteButton}
|
||||
isFavorited={isFavorited}
|
||||
onFavoriteClick={handleFavoriteClick}
|
||||
|
||||
@@ -247,7 +247,7 @@ const EmbedVideo: FC<EmbedVideoProps> = observer(
|
||||
}
|
||||
: {
|
||||
width: dimensions.width,
|
||||
maxWidth: dimensions.width,
|
||||
maxWidth: '100%',
|
||||
aspectRatio,
|
||||
};
|
||||
|
||||
|
||||
@@ -79,7 +79,6 @@ export const PickerSearchInput = React.forwardRef<HTMLInputElement, PickerSearch
|
||||
{value, onChange, placeholder, inputRef, onKeyDown, maxLength = 100, showBackButton = false, onBackButtonClick},
|
||||
forwardedRef,
|
||||
) => {
|
||||
const _inputElementRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const {t} = useLingui();
|
||||
const inputElementRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const {canFocus, safeFocusTextarea} = useInputFocusManagement(inputElementRef);
|
||||
|
||||
Reference in New Issue
Block a user