Message: Support sticky avatar (#5360)
This commit is contained in:
parent
8a8aea1b4d
commit
d00230ad33
@ -92,13 +92,13 @@ export function useStickerPickerObservers(
|
||||
const stickerSetEl = document.getElementById(`${idPrefix}-${index}`)!;
|
||||
const isClose = Math.abs(currentIndex - index) === 1;
|
||||
|
||||
animateScroll(
|
||||
containerRef.current!,
|
||||
stickerSetEl,
|
||||
'start',
|
||||
FOCUS_MARGIN,
|
||||
isClose ? SCROLL_MAX_DISTANCE_WHEN_CLOSE : SCROLL_MAX_DISTANCE_WHEN_FAR,
|
||||
);
|
||||
animateScroll({
|
||||
container: containerRef.current!,
|
||||
element: stickerSetEl,
|
||||
position: 'start',
|
||||
margin: FOCUS_MARGIN,
|
||||
maxDistance: isClose ? SCROLL_MAX_DISTANCE_WHEN_CLOSE : SCROLL_MAX_DISTANCE_WHEN_FAR,
|
||||
});
|
||||
|
||||
return index;
|
||||
});
|
||||
|
||||
@ -97,7 +97,12 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
animateScroll(messagesContainer, lastMessageElement, 'end', FOCUS_MARGIN);
|
||||
animateScroll({
|
||||
container: messagesContainer,
|
||||
element: lastMessageElement,
|
||||
position: 'end',
|
||||
margin: FOCUS_MARGIN,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -555,16 +555,13 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
// Break out of `forceLayout`
|
||||
requestMeasure(() => {
|
||||
const shouldScrollToBottom = !isBackgroundModeActive() || !firstUnreadElement;
|
||||
|
||||
animateScroll(
|
||||
animateScroll({
|
||||
container,
|
||||
shouldScrollToBottom ? lastItemElement! : firstUnreadElement!,
|
||||
shouldScrollToBottom ? 'end' : 'start',
|
||||
BOTTOM_FOCUS_MARGIN,
|
||||
undefined,
|
||||
undefined,
|
||||
noMessageSendingAnimation ? 0 : undefined,
|
||||
);
|
||||
element: shouldScrollToBottom ? lastItemElement! : firstUnreadElement!,
|
||||
position: shouldScrollToBottom ? 'end' : 'start',
|
||||
margin: BOTTOM_FOCUS_MARGIN,
|
||||
forceDuration: noMessageSendingAnimation ? 0 : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -31,6 +31,7 @@ import useScrollHooks from './hooks/useScrollHooks';
|
||||
|
||||
import ActionMessage from './ActionMessage';
|
||||
import Message from './message/Message';
|
||||
import SenderGroupContainer from './message/SenderGroupContainer';
|
||||
import SponsoredMessage from './message/SponsoredMessage';
|
||||
import MessageListBotInfo from './MessageListBotInfo';
|
||||
|
||||
@ -142,12 +143,10 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
messageIds && prevMessageIds && messageIds[messageIds.length - 2] === prevMessageIds[prevMessageIds.length - 1],
|
||||
);
|
||||
|
||||
const dateGroups = messageGroups.map((
|
||||
dateGroup: MessageDateGroup,
|
||||
dateGroupIndex: number,
|
||||
dateGroupsArray: MessageDateGroup[],
|
||||
) => {
|
||||
const senderGroups = dateGroup.senderGroups.map((
|
||||
function calculateSenderGroups(
|
||||
dateGroup: MessageDateGroup, dateGroupIndex: number, dateGroupsArray: MessageDateGroup[],
|
||||
) {
|
||||
return dateGroup.senderGroups.map((
|
||||
senderGroup,
|
||||
senderGroupIndex,
|
||||
senderGroupsArray,
|
||||
@ -186,7 +185,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
|
||||
let currentDocumentGroupId: string | undefined;
|
||||
|
||||
return senderGroup.map((
|
||||
const senderGroupElements = senderGroup.map((
|
||||
messageOrAlbum,
|
||||
messageIndex,
|
||||
) => {
|
||||
@ -260,7 +259,44 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
),
|
||||
]);
|
||||
}).flat();
|
||||
|
||||
if (!withUsers) return senderGroupElements;
|
||||
|
||||
const lastMessageOrAlbum = senderGroup[senderGroup.length - 1];
|
||||
const lastMessage = isAlbum(lastMessageOrAlbum) ? lastMessageOrAlbum.mainMessage : lastMessageOrAlbum;
|
||||
const lastMessageId = getMessageOriginalId(lastMessage);
|
||||
|
||||
const isTopicTopMessage = lastMessage.id === threadId;
|
||||
const isOwn = isOwnMessage(lastMessage);
|
||||
|
||||
const firstMessageOrAlbum = senderGroup[0];
|
||||
const firstMessage = isAlbum(firstMessageOrAlbum) ? firstMessageOrAlbum.mainMessage : firstMessageOrAlbum;
|
||||
const firstMessageId = getMessageOriginalId(firstMessage);
|
||||
|
||||
const key = `${firstMessageId}-${lastMessageId}`;
|
||||
const id = (firstMessageId === lastMessageId) ? `message-group-${firstMessageId}`
|
||||
: `message-group-${firstMessageId}-${lastMessageId}`;
|
||||
|
||||
const withAvatar = withUsers && !isOwn && (!isTopicTopMessage || !isComments);
|
||||
return (
|
||||
<SenderGroupContainer
|
||||
key={key}
|
||||
id={id}
|
||||
message={lastMessage}
|
||||
withAvatar={withAvatar}
|
||||
>
|
||||
{senderGroupElements}
|
||||
</SenderGroupContainer>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const dateGroups = messageGroups.map((
|
||||
dateGroup: MessageDateGroup,
|
||||
dateGroupIndex: number,
|
||||
dateGroupsArray: MessageDateGroup[],
|
||||
) => {
|
||||
const senderGroups = calculateSenderGroups(dateGroup, dateGroupIndex, dateGroupsArray);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@ -174,7 +174,13 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
setActiveCategoryIndex(index);
|
||||
const categoryEl = containerRef.current!.closest<HTMLElement>('.SymbolMenu-main')!
|
||||
.querySelector(`#emoji-category-${index}`)! as HTMLElement;
|
||||
animateScroll(containerRef.current!, categoryEl, 'start', FOCUS_MARGIN, SMOOTH_SCROLL_DISTANCE);
|
||||
animateScroll({
|
||||
container: containerRef.current!,
|
||||
element: categoryEl,
|
||||
position: 'start',
|
||||
margin: FOCUS_MARGIN,
|
||||
maxDistance: SMOOTH_SCROLL_DISTANCE,
|
||||
});
|
||||
});
|
||||
|
||||
const handleEmojiSelect = useLastCallback((emoji: string, name: string) => {
|
||||
|
||||
@ -618,7 +618,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
const {
|
||||
handleAvatarClick,
|
||||
handleSenderClick,
|
||||
handleViaBotClick,
|
||||
handleReplyClick,
|
||||
@ -974,19 +973,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
contentWidth, noMediaCorners, style, reactionsMaxWidth,
|
||||
} = sizeCalculations;
|
||||
|
||||
function renderAvatar() {
|
||||
const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
size="small"
|
||||
peer={avatarPeer}
|
||||
text={hiddenName}
|
||||
onClick={avatarPeer ? handleAvatarClick : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderMessageText(isForAnimation?: boolean) {
|
||||
if (!textMessage) return undefined;
|
||||
return (
|
||||
@ -1629,7 +1615,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{withAvatar && renderAvatar()}
|
||||
<div
|
||||
className={buildClassName('message-content-wrapper',
|
||||
contentClassName.includes('text') && 'can-select-text',
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
.root {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.avatarContainer {
|
||||
position: absolute;
|
||||
flex-direction: column-reverse;
|
||||
display: flex;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
|
||||
transform: translateX(0);
|
||||
transition: transform var(--select-transition);
|
||||
|
||||
:global(.select-mode-active) & {
|
||||
transform: translateX(2.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
.senderAvatar {
|
||||
position: sticky !important;
|
||||
bottom: 0.25rem;
|
||||
|
||||
:global(.select-mode-active) & {
|
||||
bottom: 5rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
bottom: 3.6875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
src/components/middle/message/SenderGroupContainer.tsx
Normal file
133
src/components/middle/message/SenderGroupContainer.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage,
|
||||
ApiPeer,
|
||||
} from '../../../api/types';
|
||||
|
||||
import {
|
||||
isAnonymousForwardsChat,
|
||||
isAnonymousOwnMessage,
|
||||
isSystemBot,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectForwardedSender,
|
||||
selectIsChatWithSelf,
|
||||
selectSender,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
|
||||
import styles from './SenderGroupContainer.module.scss';
|
||||
|
||||
type OwnProps =
|
||||
{
|
||||
message: ApiMessage;
|
||||
withAvatar?: boolean;
|
||||
children: React.ReactNode;
|
||||
id: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sender?: ApiPeer;
|
||||
canShowSender: boolean;
|
||||
originSender?: ApiPeer;
|
||||
isChatWithSelf?: boolean;
|
||||
isRepliesChat?: boolean;
|
||||
isAnonymousForwards?: boolean;
|
||||
};
|
||||
|
||||
const SenderGroupContainer: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
withAvatar,
|
||||
children,
|
||||
id,
|
||||
sender,
|
||||
canShowSender,
|
||||
originSender,
|
||||
isChatWithSelf,
|
||||
isRepliesChat,
|
||||
isAnonymousForwards,
|
||||
}) => {
|
||||
const { openChat } = getActions();
|
||||
|
||||
const { forwardInfo } = message;
|
||||
|
||||
const messageSender = canShowSender ? sender : undefined;
|
||||
|
||||
const shouldPreferOriginSender = forwardInfo
|
||||
&& (isChatWithSelf || isRepliesChat || isAnonymousForwards || !messageSender);
|
||||
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
|
||||
|
||||
const handleAvatarClick = useLastCallback(() => {
|
||||
if (!avatarPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
openChat({ id: avatarPeer.id });
|
||||
});
|
||||
|
||||
function renderAvatar() {
|
||||
const hiddenName = (!avatarPeer && forwardInfo) ? forwardInfo.hiddenUserName : undefined;
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
size="small"
|
||||
className={styles.senderAvatar}
|
||||
peer={avatarPeer}
|
||||
text={hiddenName}
|
||||
onClick={avatarPeer ? handleAvatarClick : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
'sender-group-container',
|
||||
styles.root,
|
||||
);
|
||||
|
||||
return (
|
||||
<div id={id} className={className}>
|
||||
{withAvatar && (
|
||||
<div className={styles.avatarContainer}>
|
||||
{renderAvatar()}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps): StateProps => {
|
||||
const {
|
||||
message, withAvatar,
|
||||
} = ownProps;
|
||||
const { chatId } = message;
|
||||
|
||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||
const isSystemBotChat = isSystemBot(chatId);
|
||||
const isAnonymousForwards = isAnonymousForwardsChat(chatId);
|
||||
|
||||
const forceSenderName = !isChatWithSelf && isAnonymousOwnMessage(message);
|
||||
const canShowSender = withAvatar || forceSenderName;
|
||||
const sender = selectSender(global, message);
|
||||
const originSender = selectForwardedSender(global, message);
|
||||
|
||||
return {
|
||||
sender,
|
||||
canShowSender,
|
||||
originSender,
|
||||
isChatWithSelf,
|
||||
isRepliesChat: isSystemBotChat,
|
||||
isAnonymousForwards,
|
||||
};
|
||||
},
|
||||
)(SenderGroupContainer));
|
||||
@ -48,17 +48,19 @@ export default function useFocusMessage({
|
||||
const scrollPosition = scrollTargetPosition || isToBottom ? 'end' : 'centerOrTop';
|
||||
|
||||
const exec = () => {
|
||||
const result = animateScroll(
|
||||
messagesContainer,
|
||||
elementRef.current!,
|
||||
scrollPosition,
|
||||
FOCUS_MARGIN,
|
||||
focusDirection !== undefined ? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined,
|
||||
focusDirection,
|
||||
undefined,
|
||||
isResizingContainer,
|
||||
true,
|
||||
);
|
||||
const maxDistance = focusDirection !== undefined
|
||||
? (isToBottom ? BOTTOM_FOCUS_OFFSET : RELOCATED_FOCUS_OFFSET) : undefined;
|
||||
|
||||
const result = animateScroll({
|
||||
container: messagesContainer,
|
||||
element: elementRef.current!,
|
||||
position: scrollPosition,
|
||||
margin: FOCUS_MARGIN,
|
||||
maxDistance,
|
||||
forceDirection: focusDirection,
|
||||
forceNormalContainerHeight: isResizingContainer,
|
||||
shouldReturnMutationFn: true,
|
||||
});
|
||||
|
||||
if (isQuote) {
|
||||
const firstQuote = elementRef.current!.querySelector<HTMLSpanElement>('.is-quote');
|
||||
|
||||
@ -23,7 +23,6 @@ export default function useInnerHandlers({
|
||||
asForwarded,
|
||||
isScheduled,
|
||||
album,
|
||||
avatarPeer,
|
||||
senderPeer,
|
||||
botSender,
|
||||
messageTopic,
|
||||
@ -66,14 +65,6 @@ export default function useInnerHandlers({
|
||||
replyToMsgId, replyToPeerId, replyToTopId, isQuote, quoteText,
|
||||
} = getMessageReplyInfo(message) || {};
|
||||
|
||||
const handleAvatarClick = useLastCallback(() => {
|
||||
if (!avatarPeer) {
|
||||
return;
|
||||
}
|
||||
|
||||
openChat({ id: avatarPeer.id });
|
||||
});
|
||||
|
||||
const handleSenderClick = useLastCallback(() => {
|
||||
if (!senderPeer) {
|
||||
showNotification({ message: lang('HidAccount') });
|
||||
@ -255,7 +246,6 @@ export default function useInnerHandlers({
|
||||
});
|
||||
|
||||
return {
|
||||
handleAvatarClick,
|
||||
handleSenderClick,
|
||||
handleViaBotClick,
|
||||
handleReplyClick,
|
||||
|
||||
@ -33,7 +33,12 @@ export default function useProfileState(
|
||||
if (container.scrollTop < tabsEl.offsetTop) {
|
||||
onProfileStateChange(getStateFromTabType(tabType));
|
||||
isScrollingProgrammatically = true;
|
||||
animateScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION);
|
||||
animateScroll({
|
||||
container,
|
||||
element: tabsEl,
|
||||
position: 'start',
|
||||
forceDuration: TRANSITION_DURATION,
|
||||
});
|
||||
setTimeout(() => {
|
||||
isScrollingProgrammatically = false;
|
||||
}, PROGRAMMATIC_SCROLL_TIMEOUT_MS);
|
||||
@ -59,13 +64,13 @@ export default function useProfileState(
|
||||
}
|
||||
|
||||
isScrollingProgrammatically = true;
|
||||
animateScroll(
|
||||
|
||||
animateScroll({
|
||||
container,
|
||||
container.firstElementChild as HTMLElement,
|
||||
'start',
|
||||
undefined,
|
||||
container.offsetHeight * 2,
|
||||
);
|
||||
element: container.firstElementChild as HTMLElement,
|
||||
position: 'start',
|
||||
maxDistance: container.offsetHeight * 2,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
isScrollingProgrammatically = false;
|
||||
|
||||
@ -15,19 +15,27 @@ import { selectCanAnimateInterface } from '../global/selectors';
|
||||
import { animateSingle, cancelSingleAnimation } from './animation';
|
||||
import { IS_ANDROID } from './windowEnvironment';
|
||||
|
||||
type Params = Parameters<typeof createMutateFunction>;
|
||||
export type AnimateScrollArgs = {
|
||||
container: HTMLElement;
|
||||
element: HTMLElement;
|
||||
position: ScrollTargetPosition;
|
||||
margin?: number;
|
||||
maxDistance?: number;
|
||||
forceDirection?: FocusDirection;
|
||||
forceDuration?: number;
|
||||
forceNormalContainerHeight?: boolean;
|
||||
shouldReturnMutationFn?: boolean;
|
||||
};
|
||||
|
||||
let isAnimating = false;
|
||||
let currentArgs: Parameters<typeof createMutateFunction> | undefined;
|
||||
let currentArgs: AnimateScrollArgs | undefined;
|
||||
let onHeavyAnimationEnd: NoneToVoidFunction | undefined;
|
||||
|
||||
export default function animateScroll(...args: Params | [...Params, boolean]) {
|
||||
currentArgs = args.slice(0, 8) as Params;
|
||||
export default function animateScroll(args: AnimateScrollArgs) {
|
||||
currentArgs = args;
|
||||
const mutate = createMutateFunction(args);
|
||||
|
||||
const mutate = createMutateFunction(...currentArgs);
|
||||
|
||||
const shouldReturnMutationFn = args[8];
|
||||
if (shouldReturnMutationFn) {
|
||||
if (args.shouldReturnMutationFn) {
|
||||
return mutate;
|
||||
}
|
||||
|
||||
@ -43,20 +51,39 @@ export function restartCurrentScrollAnimation() {
|
||||
cancelSingleAnimation();
|
||||
|
||||
requestMeasure(() => {
|
||||
requestMutation(createMutateFunction(...currentArgs!));
|
||||
requestMutation(createMutateFunction(currentArgs!));
|
||||
});
|
||||
}
|
||||
|
||||
function createMutateFunction(
|
||||
container: HTMLElement,
|
||||
element: HTMLElement,
|
||||
position: ScrollTargetPosition,
|
||||
margin = 0,
|
||||
maxDistance = SCROLL_MAX_DISTANCE,
|
||||
forceDirection?: FocusDirection,
|
||||
forceDuration?: number,
|
||||
forceNormalContainerHeight?: boolean,
|
||||
) {
|
||||
function getOffsetToContainer(element: HTMLElement, container: HTMLElement) {
|
||||
let offsetTop = 0;
|
||||
let offsetLeft = 0;
|
||||
|
||||
let current: HTMLElement | null = element;
|
||||
|
||||
while (current && current !== container && !current.contains(container)) {
|
||||
offsetTop += current.offsetTop;
|
||||
offsetLeft += current.offsetLeft;
|
||||
|
||||
current = current.offsetParent as HTMLElement;
|
||||
}
|
||||
|
||||
return { top: offsetTop, left: offsetLeft };
|
||||
}
|
||||
|
||||
function createMutateFunction(args: AnimateScrollArgs) {
|
||||
const {
|
||||
container,
|
||||
element,
|
||||
position,
|
||||
margin = 0,
|
||||
maxDistance = SCROLL_MAX_DISTANCE,
|
||||
forceDirection,
|
||||
forceNormalContainerHeight,
|
||||
} = args;
|
||||
|
||||
let forceDuration = args.forceDuration;
|
||||
|
||||
if (
|
||||
forceDirection === FocusDirection.Static
|
||||
|| !selectCanAnimateInterface(getGlobal())
|
||||
@ -64,8 +91,10 @@ function createMutateFunction(
|
||||
forceDuration = 0;
|
||||
}
|
||||
|
||||
const { offsetTop: elementTop, offsetHeight: elementHeight } = element;
|
||||
const { offsetHeight: elementHeight } = element;
|
||||
const { scrollTop: currentScrollTop, offsetHeight: containerHeight, scrollHeight } = container;
|
||||
const elementTop = getOffsetToContainer(element, container).top;
|
||||
|
||||
const targetContainerHeight = forceNormalContainerHeight && container.dataset.normalHeight
|
||||
? Number(container.dataset.normalHeight)
|
||||
: containerHeight;
|
||||
|
||||
@ -25,6 +25,11 @@ export default function setTooltipItemVisible(selector: string, index: number, c
|
||||
if (!visibleIndexes.includes(index)
|
||||
|| (index === first && !isFullyVisible(container, allElements[first]))) {
|
||||
const position = index > visibleIndexes[visibleIndexes.length - 1] ? 'start' : 'end';
|
||||
animateScroll(container, allElements[index], position, SCROLL_MARGIN);
|
||||
animateScroll({
|
||||
container,
|
||||
element: allElements[index],
|
||||
position,
|
||||
margin: SCROLL_MARGIN,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user