Story: Display forward info (#4070)
This commit is contained in:
parent
9ef631665f
commit
b4291a1505
@ -4,6 +4,7 @@ import type {
|
|||||||
ApiMediaArea,
|
ApiMediaArea,
|
||||||
ApiMediaAreaCoordinates,
|
ApiMediaAreaCoordinates,
|
||||||
ApiStealthMode,
|
ApiStealthMode,
|
||||||
|
ApiStoryForwardInfo,
|
||||||
ApiStoryView,
|
ApiStoryView,
|
||||||
ApiTypeStory,
|
ApiTypeStory,
|
||||||
MediaContent,
|
MediaContent,
|
||||||
@ -42,7 +43,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
|||||||
edited, pinned, expireDate, id, date, caption,
|
edited, pinned, expireDate, id, date, caption,
|
||||||
entities, media, privacy, views,
|
entities, media, privacy, views,
|
||||||
public: isPublic, noforwards, closeFriends, contacts, selectedContacts,
|
public: isPublic, noforwards, closeFriends, contacts, selectedContacts,
|
||||||
mediaAreas, sentReaction, out,
|
mediaAreas, sentReaction, out, fwdFrom,
|
||||||
} = story;
|
} = story;
|
||||||
|
|
||||||
const content: MediaContent = {
|
const content: MediaContent = {
|
||||||
@ -76,6 +77,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
|||||||
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
...(privacy && { visibility: buildPrivacyRules(privacy) }),
|
||||||
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
|
...(mediaAreas && { mediaAreas: mediaAreas.map(buildApiMediaArea).filter(Boolean) }),
|
||||||
...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }),
|
...(sentReaction && { sentReaction: buildApiReaction(sentReaction) }),
|
||||||
|
...(fwdFrom && { forwardInfo: buildApiStoryForwardInfo(fwdFrom) }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,3 +168,13 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
|
|||||||
|
|
||||||
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
|
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiStoryForwardInfo(forwardHeader: GramJs.TypeStoryFwdHeader): ApiStoryForwardInfo {
|
||||||
|
const { from, fromName, storyId } = forwardHeader;
|
||||||
|
|
||||||
|
return {
|
||||||
|
storyId,
|
||||||
|
fromPeerId: from && getApiChatIdFromMtpPeer(from),
|
||||||
|
fromName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -366,6 +366,12 @@ export interface ApiMessageForwardInfo {
|
|||||||
postAuthorTitle?: string;
|
postAuthorTitle?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiStoryForwardInfo {
|
||||||
|
fromPeerId?: string;
|
||||||
|
fromName?: string;
|
||||||
|
storyId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export type ApiMessageEntityDefault = {
|
export type ApiMessageEntityDefault = {
|
||||||
type: Exclude<
|
type: Exclude<
|
||||||
`${ApiMessageEntityTypes}`,
|
`${ApiMessageEntityTypes}`,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { ApiPrivacySettings } from '../../types';
|
import type { ApiPrivacySettings } from '../../types';
|
||||||
import type {
|
import type {
|
||||||
ApiGeoPoint, ApiReaction, ApiReactionCount, MediaContent,
|
ApiGeoPoint, ApiReaction, ApiReactionCount, ApiStoryForwardInfo, MediaContent,
|
||||||
} from './messages';
|
} from './messages';
|
||||||
|
|
||||||
export interface ApiStory {
|
export interface ApiStory {
|
||||||
@ -25,6 +25,7 @@ export interface ApiStory {
|
|||||||
visibility?: ApiPrivacySettings;
|
visibility?: ApiPrivacySettings;
|
||||||
sentReaction?: ApiReaction;
|
sentReaction?: ApiReaction;
|
||||||
mediaAreas?: ApiMediaArea[];
|
mediaAreas?: ApiMediaArea[];
|
||||||
|
forwardInfo?: ApiStoryForwardInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiStorySkipped {
|
export interface ApiStorySkipped {
|
||||||
|
|||||||
@ -123,7 +123,6 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
height: 1.125rem;
|
height: 1.125rem;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
flex: 1;
|
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: none;
|
content: none;
|
||||||
@ -131,6 +130,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.multiline {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
width: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
width: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
||||||
height: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
height: calc(1.125 * var(--message-text-size, 1rem)) !important;
|
||||||
|
|||||||
@ -209,7 +209,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="message-text">
|
<div className="message-text">
|
||||||
<p className="embedded-text-wrapper">
|
<p className={buildClassName('embedded-text-wrapper', isQuote && 'multiline')}>
|
||||||
{renderTextContent()}
|
{renderTextContent()}
|
||||||
</p>
|
</p>
|
||||||
<div className="message-title">
|
<div className="message-title">
|
||||||
|
|||||||
148
src/components/common/embedded/EmbeddedStoryForward.tsx
Normal file
148
src/components/common/embedded/EmbeddedStoryForward.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiPeer,
|
||||||
|
ApiStoryForwardInfo,
|
||||||
|
ApiTypeStory,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import type { IconName } from '../../../types/icons';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getSenderTitle,
|
||||||
|
isUserId,
|
||||||
|
} from '../../../global/helpers';
|
||||||
|
import { selectPeer, selectPeerStory } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { getPeerColorClass } from '../helpers/peerColor';
|
||||||
|
import renderText from '../helpers/renderText';
|
||||||
|
import { renderTextWithEntities } from '../helpers/renderTextWithEntities';
|
||||||
|
|
||||||
|
import { useFastClick } from '../../../hooks/useFastClick';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Icon from '../Icon';
|
||||||
|
import EmojiIconBackground from './EmojiIconBackground';
|
||||||
|
|
||||||
|
import './EmbeddedMessage.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
forwardInfo: ApiStoryForwardInfo;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
sender?: ApiPeer;
|
||||||
|
story?: ApiTypeStory;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EmbeddedStoryForward: FC<OwnProps & StateProps> = ({
|
||||||
|
className,
|
||||||
|
forwardInfo,
|
||||||
|
sender,
|
||||||
|
story,
|
||||||
|
}) => {
|
||||||
|
const { openStoryViewer, loadPeerStoriesByIds, openChat } = getActions();
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!story && forwardInfo.fromPeerId && forwardInfo.storyId) {
|
||||||
|
loadPeerStoriesByIds({
|
||||||
|
peerId: forwardInfo.fromPeerId,
|
||||||
|
storyIds: [forwardInfo.storyId],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [forwardInfo, story]);
|
||||||
|
|
||||||
|
const senderTitle = sender ? getSenderTitle(lang, sender) : forwardInfo.fromName;
|
||||||
|
|
||||||
|
const openOriginalStory = useLastCallback(() => {
|
||||||
|
const { fromPeerId, storyId } = forwardInfo;
|
||||||
|
if (!fromPeerId) return;
|
||||||
|
|
||||||
|
const isStoryReady = story && !('isDeleted' in story && story.isDeleted);
|
||||||
|
|
||||||
|
if (isStoryReady) {
|
||||||
|
openStoryViewer({
|
||||||
|
peerId: fromPeerId,
|
||||||
|
storyId,
|
||||||
|
isSingleStory: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
openChat({ id: fromPeerId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleClick, handleMouseDown } = useFastClick(openOriginalStory);
|
||||||
|
|
||||||
|
function renderTextContent() {
|
||||||
|
if (story && 'content' in story && story.content.text) {
|
||||||
|
return (
|
||||||
|
<p className="embedded-text-wrapper">
|
||||||
|
{renderTextWithEntities(story.content.text)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSender() {
|
||||||
|
if (!sender || !senderTitle) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon: IconName | undefined = !isUserId(sender.id) ? 'channel-filled' : 'user-filled';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{icon && <Icon name={icon} className="embedded-chat-icon" />}
|
||||||
|
{senderTitle && renderText(senderTitle)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={buildClassName(
|
||||||
|
'EmbeddedMessage',
|
||||||
|
className,
|
||||||
|
getPeerColorClass(sender, true, true),
|
||||||
|
)}
|
||||||
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
|
onClick={handleClick}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
{sender?.color?.backgroundEmojiId && (
|
||||||
|
<EmojiIconBackground
|
||||||
|
emojiDocumentId={sender.color.backgroundEmojiId}
|
||||||
|
className="EmbeddedMessage--background-icons"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="message-text">
|
||||||
|
{renderTextContent()}
|
||||||
|
<div className="message-title">
|
||||||
|
{renderSender()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { forwardInfo }): StateProps => {
|
||||||
|
const sender = forwardInfo.fromPeerId ? selectPeer(global, forwardInfo.fromPeerId) : undefined;
|
||||||
|
const story = forwardInfo.storyId && forwardInfo.fromPeerId
|
||||||
|
? selectPeerStory(global, forwardInfo.fromPeerId, forwardInfo.storyId) : undefined;
|
||||||
|
return {
|
||||||
|
sender,
|
||||||
|
story,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(EmbeddedStoryForward));
|
||||||
@ -15,6 +15,7 @@ import { EDITABLE_STORY_INPUT_CSS_SELECTOR, EDITABLE_STORY_INPUT_ID } from '../.
|
|||||||
import { getSenderTitle, isUserId } from '../../global/helpers';
|
import { getSenderTitle, isUserId } from '../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectChat, selectIsCurrentUserPremium,
|
selectChat, selectIsCurrentUserPremium,
|
||||||
|
selectPeer,
|
||||||
selectPeerStories, selectPeerStory,
|
selectPeerStories, selectPeerStory,
|
||||||
selectTabState, selectUser,
|
selectTabState, selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
@ -43,6 +44,7 @@ import useStoryProps from './hooks/useStoryProps';
|
|||||||
|
|
||||||
import Avatar from '../common/Avatar';
|
import Avatar from '../common/Avatar';
|
||||||
import Composer from '../common/Composer';
|
import Composer from '../common/Composer';
|
||||||
|
import Icon from '../common/Icon';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import DropdownMenu from '../ui/DropdownMenu';
|
import DropdownMenu from '../ui/DropdownMenu';
|
||||||
import MenuItem from '../ui/MenuItem';
|
import MenuItem from '../ui/MenuItem';
|
||||||
@ -74,6 +76,7 @@ interface OwnProps {
|
|||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
peer: ApiPeer;
|
peer: ApiPeer;
|
||||||
|
forwardSender?: ApiPeer;
|
||||||
story?: ApiTypeStory;
|
story?: ApiTypeStory;
|
||||||
isMuted: boolean;
|
isMuted: boolean;
|
||||||
orderedIds?: number[];
|
orderedIds?: number[];
|
||||||
@ -98,6 +101,7 @@ function Story({
|
|||||||
peerId,
|
peerId,
|
||||||
storyId,
|
storyId,
|
||||||
peer,
|
peer,
|
||||||
|
forwardSender,
|
||||||
isMuted,
|
isMuted,
|
||||||
isArchivedStories,
|
isArchivedStories,
|
||||||
isPrivateStories,
|
isPrivateStories,
|
||||||
@ -152,6 +156,7 @@ function Story({
|
|||||||
const {
|
const {
|
||||||
isDeletedStory,
|
isDeletedStory,
|
||||||
hasText,
|
hasText,
|
||||||
|
hasForwardInfo,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
previewBlobUrl,
|
previewBlobUrl,
|
||||||
isVideo,
|
isVideo,
|
||||||
@ -182,6 +187,10 @@ function Story({
|
|||||||
const areViewsExpired = Boolean(
|
const areViewsExpired = Boolean(
|
||||||
isOut && (story!.date + viewersExpirePeriod) < getServerTime(),
|
isOut && (story!.date + viewersExpirePeriod) < getServerTime(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const forwardSenderTitle = forwardSender ? getSenderTitle(lang, forwardSender)
|
||||||
|
: (isLoadedStory && story.forwardInfo?.fromName);
|
||||||
|
|
||||||
const canCopyLink = Boolean(
|
const canCopyLink = Boolean(
|
||||||
isLoadedStory
|
isLoadedStory
|
||||||
&& story.isPublic
|
&& story.isPublic
|
||||||
@ -381,6 +390,11 @@ function Story({
|
|||||||
openChat({ id: peerId });
|
openChat({ id: peerId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleForwardPeerClick = useLastCallback(() => {
|
||||||
|
onClose();
|
||||||
|
openChat({ id: forwardSender!.id });
|
||||||
|
});
|
||||||
|
|
||||||
const handleOpenPrevStory = useLastCallback(() => {
|
const handleOpenPrevStory = useLastCallback(() => {
|
||||||
setCurrentTime(0);
|
setCurrentTime(0);
|
||||||
openPreviousStory();
|
openPreviousStory();
|
||||||
@ -591,6 +605,19 @@ function Story({
|
|||||||
{renderText(getSenderTitle(lang, peer) || '')}
|
{renderText(getSenderTitle(lang, peer) || '')}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.storyMetaRow}>
|
<div className={styles.storyMetaRow}>
|
||||||
|
{forwardSenderTitle && (
|
||||||
|
<span
|
||||||
|
className={buildClassName(
|
||||||
|
styles.storyMeta, styles.forwardHeader, forwardSender && styles.clickable,
|
||||||
|
)}
|
||||||
|
onClick={forwardSender ? handleForwardPeerClick : undefined}
|
||||||
|
>
|
||||||
|
<Icon name="loop" />
|
||||||
|
<span className={styles.forwardHeaderText}>
|
||||||
|
{renderText(forwardSenderTitle)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{story && 'date' in story && (
|
{story && 'date' in story && (
|
||||||
<span className={styles.storyMeta}>{formatRelativeTime(lang, serverTime, story.date)}</span>
|
<span className={styles.storyMeta}>{formatRelativeTime(lang, serverTime, story.date)}</span>
|
||||||
)}
|
)}
|
||||||
@ -753,7 +780,7 @@ function Story({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{hasText && <div className={styles.captionGradient} />}
|
{hasText && <div className={styles.captionGradient} />}
|
||||||
{hasText && (
|
{(hasText || hasForwardInfo) && (
|
||||||
<StoryCaption
|
<StoryCaption
|
||||||
key={`caption-${storyId}-${peerId}`}
|
key={`caption-${storyId}-${peerId}`}
|
||||||
story={story as ApiStory}
|
story={story as ApiStory}
|
||||||
@ -812,9 +839,12 @@ export default memo(withGlobal<OwnProps>((global, {
|
|||||||
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|
viewModal || forwardedStoryId || tabState.reactionPicker?.storyId || isReportModalOpen || isPrivacyModalOpen
|
||||||
|| isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal,
|
|| isPremiumModalOpen || isDeleteModalOpen || safeLinkModalUrl || isStealthModalOpen || mapModal,
|
||||||
);
|
);
|
||||||
|
const forwardSenderId = story && 'forwardInfo' in story ? story.forwardInfo?.fromPeerId : undefined;
|
||||||
|
const forwardSender = forwardSenderId ? selectPeer(global, forwardSenderId) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peer: (user || chat)!,
|
peer: (user || chat)!,
|
||||||
|
forwardSender,
|
||||||
story,
|
story,
|
||||||
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
|
orderedIds: isArchivedStories ? archiveIds : (isPrivateStories ? pinnedIds : orderedIds),
|
||||||
isMuted,
|
isMuted,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import useLang from '../../hooks/useLang';
|
|||||||
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
|
||||||
|
import EmbeddedStoryForward from '../common/embedded/EmbeddedStoryForward';
|
||||||
import MessageText from '../common/MessageText';
|
import MessageText from '../common/MessageText';
|
||||||
|
|
||||||
import styles from './StoryViewer.module.scss';
|
import styles from './StoryViewer.module.scss';
|
||||||
@ -27,6 +28,7 @@ interface OwnProps {
|
|||||||
|
|
||||||
const EXPAND_ANIMATION_DURATION_MS = 400;
|
const EXPAND_ANIMATION_DURATION_MS = 400;
|
||||||
const OVERFLOW_THRESHOLD_PX = 5.75 * REM;
|
const OVERFLOW_THRESHOLD_PX = 5.75 * REM;
|
||||||
|
const LINES_TO_SHOW = 3;
|
||||||
|
|
||||||
function StoryCaption({
|
function StoryCaption({
|
||||||
story, isExpanded, className, onExpand, onFold,
|
story, isExpanded, className, onExpand, onFold,
|
||||||
@ -37,6 +39,8 @@ function StoryCaption({
|
|||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const textRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
const showMoreButtonRef = useRef<HTMLDivElement>(null);
|
const showMoreButtonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const caption = story.content.text;
|
const caption = story.content.text;
|
||||||
@ -74,15 +78,22 @@ function StoryCaption({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!showMoreButtonRef.current || !contentRef.current) {
|
if (!showMoreButtonRef.current || !contentRef.current || !textRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const button = showMoreButtonRef.current;
|
|
||||||
const container = contentRef.current;
|
const container = contentRef.current;
|
||||||
|
const textContainer = textRef.current;
|
||||||
|
|
||||||
|
const textOffsetTop = textContainer.offsetTop;
|
||||||
|
const lineHeight = parseInt(getComputedStyle(textContainer).lineHeight, 10);
|
||||||
|
const overflowShift = textOffsetTop + lineHeight * LINES_TO_SHOW;
|
||||||
|
|
||||||
|
const button = showMoreButtonRef.current;
|
||||||
|
|
||||||
const { offsetWidth } = button;
|
const { offsetWidth } = button;
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
|
container.style.setProperty('--_overflow-shift', `${overflowShift}px`);
|
||||||
container.style.setProperty('--expand-button-width', `${offsetWidth}px`);
|
container.style.setProperty('--expand-button-width', `${offsetWidth}px`);
|
||||||
});
|
});
|
||||||
}, [canExpand]);
|
}, [canExpand]);
|
||||||
@ -112,11 +123,19 @@ function StoryCaption({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={buildClassName(styles.captionInner, 'allow-selection', 'custom-scroll')}
|
className={buildClassName(styles.captionInner, 'allow-selection', 'custom-scroll')}
|
||||||
>
|
>
|
||||||
<MessageText
|
{story.forwardInfo && (
|
||||||
messageOrStory={story}
|
<EmbeddedStoryForward
|
||||||
withTranslucentThumbs
|
forwardInfo={story.forwardInfo}
|
||||||
forcePlayback
|
className={styles.forwardInfo}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
<div ref={textRef} className={styles.captionText}>
|
||||||
|
<MessageText
|
||||||
|
messageOrStory={story}
|
||||||
|
withTranslucentThumbs
|
||||||
|
forcePlayback
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shouldRenderShowMore && (
|
{shouldRenderShowMore && (
|
||||||
|
|||||||
@ -516,7 +516,7 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@supports (bottom: env(safe-area-inset-bottom)) {
|
@supports (bottom: env(safe-area-inset-bottom)) {
|
||||||
bottom: calc(5rem + env(safe-area-inset-bottom));
|
bottom: calc(4.25rem + env(safe-area-inset-bottom));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -545,8 +545,13 @@
|
|||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.captionText :global(.custom-emoji) {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.hasOverflow {
|
.hasOverflow {
|
||||||
transform: translateY(calc(100% - 5.75rem));
|
--_overflow-shift: 5.75rem;
|
||||||
|
transform: translateY(calc(100% - var(--_overflow-shift)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.expanded {
|
.expanded {
|
||||||
@ -570,7 +575,7 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
mask-image: linear-gradient(to top, black 0%, black 0%), linear-gradient(to left, black 75%, transparent 100%);
|
mask-image: linear-gradient(to top, black 0%, black 0%), linear-gradient(to left, black 75%, transparent 100%);
|
||||||
mask-position: 100% 100%, 100% 4.5rem;
|
mask-position: 100% 100%, 100% calc(var(--_overflow-shift) - 1.25rem);
|
||||||
mask-size: 100% 100%, calc(var(--expand-button-width, 0%) + 4rem) 1.25em;
|
mask-size: 100% 100%, calc(var(--expand-button-width, 0%) + 4rem) 1.25em;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
-webkit-mask-composite: xor;
|
-webkit-mask-composite: xor;
|
||||||
@ -720,3 +725,33 @@
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forwardHeader {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.125rem;
|
||||||
|
|
||||||
|
&.clickable {
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forwardHeaderText {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.forwardHeader.clickable:hover & {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.forwardInfo {
|
||||||
|
--accent-color: var(--color-white);
|
||||||
|
--accent-background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|||||||
@ -16,9 +16,11 @@ const FIRST_PRELOAD_DELAY = 1000;
|
|||||||
const canPreload = pause(FIRST_PRELOAD_DELAY);
|
const canPreload = pause(FIRST_PRELOAD_DELAY);
|
||||||
|
|
||||||
function useStoryPreloader(peerIds: string[]): void;
|
function useStoryPreloader(peerIds: string[]): void;
|
||||||
function useStoryPreloader(peerId: string, aroundStoryId?: number): void;
|
function useStoryPreloader(peerId?: string, aroundStoryId?: number): void;
|
||||||
function useStoryPreloader(peerId: string | string[], aroundStoryId?: number) {
|
function useStoryPreloader(peerId?: string | string[], aroundStoryId?: number) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (peerId === undefined) return;
|
||||||
|
|
||||||
const preloadHashes = async (mediaHashes: { hash: string; format: ApiMediaFormat }[]) => {
|
const preloadHashes = async (mediaHashes: { hash: string; format: ApiMediaFormat }[]) => {
|
||||||
await canPreload;
|
await canPreload;
|
||||||
mediaHashes.forEach(({ hash, format }) => {
|
mediaHashes.forEach(({ hash, format }) => {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export default function useStoryProps(
|
|||||||
const isLoadedStory = story && 'content' in story;
|
const isLoadedStory = story && 'content' in story;
|
||||||
const isDeletedStory = story && 'isDeleted' in story;
|
const isDeletedStory = story && 'isDeleted' in story;
|
||||||
const hasText = isLoadedStory ? hasMessageText(story) : false;
|
const hasText = isLoadedStory ? hasMessageText(story) : false;
|
||||||
|
const hasForwardInfo = isLoadedStory && Boolean(story.forwardInfo);
|
||||||
|
|
||||||
let thumbnail: string | undefined;
|
let thumbnail: string | undefined;
|
||||||
if (isLoadedStory) {
|
if (isLoadedStory) {
|
||||||
@ -43,6 +44,7 @@ export default function useStoryProps(
|
|||||||
isLoadedStory,
|
isLoadedStory,
|
||||||
isDeletedStory,
|
isDeletedStory,
|
||||||
hasText,
|
hasText,
|
||||||
|
hasForwardInfo,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
previewHash,
|
previewHash,
|
||||||
previewBlobUrl,
|
previewBlobUrl,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user