[Perf] Various fixes for chat opening animation on Android (#1234)
This commit is contained in:
parent
0a594a84e1
commit
fa8e750433
@ -1,5 +1,4 @@
|
||||
.MessageOutgoingStatus {
|
||||
position: relative;
|
||||
width: 1.19rem;
|
||||
height: 1.19rem;
|
||||
overflow: hidden;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
.Badge-transition {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
transition: transform .3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
|
||||
@ -53,7 +53,6 @@
|
||||
}
|
||||
|
||||
.status {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@ -106,10 +105,6 @@
|
||||
margin-inline-end: .25rem;
|
||||
}
|
||||
|
||||
.media-preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@ -130,11 +125,13 @@
|
||||
}
|
||||
|
||||
.icon-play {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: .75rem;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: .1875rem;
|
||||
margin-inline-start: -1.25rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
bottom: 0.0625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,7 +59,6 @@ type OwnProps = {
|
||||
folderId?: number;
|
||||
orderDiff: number;
|
||||
animationType: ChatAnimationTypes;
|
||||
isSelected: boolean;
|
||||
isPinned?: boolean;
|
||||
};
|
||||
|
||||
@ -75,6 +74,7 @@ type StateProps = {
|
||||
draft?: ApiFormattedText;
|
||||
messageListType?: MessageListType;
|
||||
animationLevel?: number;
|
||||
isSelected?: boolean;
|
||||
lastSyncTime?: number;
|
||||
};
|
||||
|
||||
@ -88,7 +88,6 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
folderId,
|
||||
orderDiff,
|
||||
animationType,
|
||||
isSelected,
|
||||
isPinned,
|
||||
chat,
|
||||
isMuted,
|
||||
@ -101,6 +100,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
draft,
|
||||
messageListType,
|
||||
animationLevel,
|
||||
isSelected,
|
||||
lastSyncTime,
|
||||
openChat,
|
||||
focusLastMessage,
|
||||
@ -324,7 +324,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
: undefined;
|
||||
const { targetUserId: actionTargetUserId, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||
const privateChatUserId = getPrivateChatUserId(chat);
|
||||
const { type: messageListType } = selectCurrentMessageList(global) || {};
|
||||
const {
|
||||
chatId: currentChatId,
|
||||
threadId: currentThreadId,
|
||||
type: messageListType,
|
||||
} = selectCurrentMessageList(global) || {};
|
||||
|
||||
return {
|
||||
chat,
|
||||
@ -338,6 +342,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
||||
messageListType,
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
isSelected: chatId === currentChatId && currentThreadId === MAIN_THREAD_ID,
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
};
|
||||
},
|
||||
|
||||
@ -5,7 +5,7 @@ import { withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
import {
|
||||
ApiChat, ApiChatFolder, ApiUser, MAIN_THREAD_ID,
|
||||
ApiChat, ApiChatFolder, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import { NotifyException, NotifySettings } from '../../../types';
|
||||
|
||||
@ -15,7 +15,7 @@ import usePrevious from '../../../hooks/usePrevious';
|
||||
import { mapValues, pick } from '../../../util/iteratees';
|
||||
import { getChatOrder, prepareChatList, prepareFolderListIds } from '../../../modules/helpers';
|
||||
import {
|
||||
selectChatFolder, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings,
|
||||
selectChatFolder, selectNotifyExceptions, selectNotifySettings,
|
||||
} from '../../../modules/selectors';
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import { useChatAnimationType } from './hooks';
|
||||
@ -36,15 +36,13 @@ type StateProps = {
|
||||
usersById: Record<number, ApiUser>;
|
||||
chatFolder?: ApiChatFolder;
|
||||
listIds?: number[];
|
||||
currentChatId?: number;
|
||||
orderedPinnedIds?: number[];
|
||||
lastSyncTime?: number;
|
||||
isInDiscussionThread?: boolean;
|
||||
notifySettings: NotifySettings;
|
||||
notifyExceptions?: Record<number, NotifyException>;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat'>;
|
||||
type DispatchProps = Pick<GlobalActions, 'loadMoreChats' | 'preloadTopChatMessages' | 'openChat' | 'openNextChat'>;
|
||||
|
||||
enum FolderTypeToListType {
|
||||
'all' = 'active',
|
||||
@ -60,15 +58,14 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chatsById,
|
||||
usersById,
|
||||
listIds,
|
||||
currentChatId,
|
||||
orderedPinnedIds,
|
||||
lastSyncTime,
|
||||
isInDiscussionThread,
|
||||
notifySettings,
|
||||
notifyExceptions,
|
||||
loadMoreChats,
|
||||
preloadTopChatMessages,
|
||||
openChat,
|
||||
openNextChat,
|
||||
}) => {
|
||||
const [currentListIds, currentPinnedIds] = useMemo(() => {
|
||||
return folderType === 'folder' && chatFolder
|
||||
@ -140,7 +137,6 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chatId={id}
|
||||
isPinned
|
||||
folderId={folderId}
|
||||
isSelected={id === currentChatId && !isInDiscussionThread}
|
||||
animationType={getAnimationType(id)}
|
||||
orderDiff={orderDiffById[id]}
|
||||
// @ts-ignore
|
||||
@ -153,7 +149,6 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
teactOrderKey={getChatOrder(chat)}
|
||||
chatId={chat.id}
|
||||
folderId={folderId}
|
||||
isSelected={chat.id === currentChatId && !isInDiscussionThread}
|
||||
animationType={getAnimationType(chat.id)}
|
||||
orderDiff={orderDiffById[chat.id]}
|
||||
// @ts-ignore
|
||||
@ -181,21 +176,8 @@ const ChatList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const targetIndexDelta = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : undefined;
|
||||
if (!targetIndexDelta) return;
|
||||
|
||||
if (!currentChatId) {
|
||||
e.preventDefault();
|
||||
openChat({ id: orderedIds[0] });
|
||||
return;
|
||||
}
|
||||
|
||||
const position = orderedIds.indexOf(currentChatId);
|
||||
|
||||
if (position === -1) {
|
||||
return;
|
||||
}
|
||||
const nextId = orderedIds[position + targetIndexDelta];
|
||||
|
||||
e.preventDefault();
|
||||
openChat({ id: nextId });
|
||||
openNextChat({ targetIndexDelta, orderedIds });
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -238,15 +220,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
users: { byId: usersById },
|
||||
lastSyncTime,
|
||||
} = global;
|
||||
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
const listType = folderType !== 'folder' ? FolderTypeToListType[folderType] : undefined;
|
||||
const chatFolder = folderId ? selectChatFolder(global, folderId) : undefined;
|
||||
|
||||
return {
|
||||
chatsById,
|
||||
usersById,
|
||||
currentChatId,
|
||||
lastSyncTime,
|
||||
...(listType ? {
|
||||
listIds: listIds[listType],
|
||||
@ -254,7 +233,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
} : {
|
||||
chatFolder,
|
||||
}),
|
||||
isInDiscussionThread: currentThreadId !== MAIN_THREAD_ID,
|
||||
notifySettings: selectNotifySettings(global),
|
||||
notifyExceptions: selectNotifyExceptions(global),
|
||||
};
|
||||
@ -263,5 +241,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
'loadMoreChats',
|
||||
'preloadTopChatMessages',
|
||||
'openChat',
|
||||
'openNextChat',
|
||||
]),
|
||||
)(ChatList));
|
||||
|
||||
@ -48,10 +48,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media-preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
@ -62,11 +58,13 @@
|
||||
}
|
||||
|
||||
.icon-play {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: .75rem;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
top: .1875rem;
|
||||
margin-inline-start: -1.25rem;
|
||||
margin-inline-end: 0.5rem;
|
||||
bottom: 0.0625rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,13 +81,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
// @optimization
|
||||
#Main.middle-column-open & {
|
||||
.custom-scroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
#Main.history-animation-disabled & {
|
||||
transition: none;
|
||||
|
||||
@ -184,17 +177,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SymbolMenu {
|
||||
@media (max-width: 600px) {
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
body.animation-level-0 & {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
body:not(.is-middle-column-open) & {
|
||||
transform: translate3d(100vw, 0, 0) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,11 +113,6 @@ const Main: FC<StateProps & DispatchProps> = ({
|
||||
shouldSkipHistoryAnimations && 'history-animation-disabled',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// For animating Symbol Menu on mobile
|
||||
document.body.classList.toggle('is-middle-column-open', className.includes('middle-column-open'));
|
||||
}, [className]);
|
||||
|
||||
// Add `body` classes when toggling right column
|
||||
useEffect(() => {
|
||||
if (animationLevel > 0) {
|
||||
|
||||
@ -205,11 +205,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.scrolled .sticky-date {
|
||||
&.scrolled:not(.is-animating) .sticky-date {
|
||||
position: sticky;
|
||||
top: 0.625rem;
|
||||
}
|
||||
|
||||
&.is-animating {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&.is-animating .message-select-control {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.has-header-tools & .sticky-date {
|
||||
top: 3.75rem;
|
||||
}
|
||||
|
||||
@ -69,6 +69,7 @@ type OwnProps = {
|
||||
threadId: number;
|
||||
type: MessageListType;
|
||||
canPost: boolean;
|
||||
isReady: boolean;
|
||||
onFabToggle: (shouldShow: boolean) => void;
|
||||
onNotchToggle: (shouldShow: boolean) => void;
|
||||
hasTools?: boolean;
|
||||
@ -122,6 +123,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isChatLoaded,
|
||||
isChannelChat,
|
||||
canPost,
|
||||
isReady,
|
||||
isChatWithSelf,
|
||||
messageIds,
|
||||
messagesById,
|
||||
@ -331,9 +333,12 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
// Memorize height for scroll animation
|
||||
const { height: windowHeight } = useWindowSize();
|
||||
|
||||
useEffect(() => {
|
||||
containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight);
|
||||
}, [windowHeight]);
|
||||
if (isReady) {
|
||||
containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight);
|
||||
}
|
||||
}, [windowHeight, isReady]);
|
||||
|
||||
// Initial message loading
|
||||
useEffect(() => {
|
||||
@ -353,7 +358,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
// Remember scroll position before repositioning it
|
||||
useOnChange(() => {
|
||||
if (!messageIds || !listItemElementsRef.current) {
|
||||
if (!messageIds || !listItemElementsRef.current || !isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -370,7 +375,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
anchorIdRef.current = anchor.id;
|
||||
anchorTopRef.current = anchor.getBoundingClientRect().top;
|
||||
// This should match deps for `useLayoutEffectWithPrevDeps` below
|
||||
}, [messageIds, isViewportNewest, containerHeight, hasTools]);
|
||||
}, [messageIds, isViewportNewest, containerHeight, hasTools, isReady]);
|
||||
|
||||
// Handles updated message list, takes care of scroll repositioning
|
||||
useLayoutEffectWithPrevDeps(([
|
||||
@ -519,6 +524,7 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isSelectModeActive && 'select-mode-active',
|
||||
hasFocusing && 'has-focusing',
|
||||
isScrolled && 'scrolled',
|
||||
!isReady && 'is-animating',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
ANIMATION_END_DELAY,
|
||||
DARK_THEME_BG_COLOR,
|
||||
LIGHT_THEME_BG_COLOR,
|
||||
ANIMATION_LEVEL_MIN,
|
||||
} from '../../config';
|
||||
import {
|
||||
IS_SINGLE_COLUMN_LAYOUT,
|
||||
@ -85,8 +86,9 @@ type StateProps = {
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'openChat' | 'unpinAllMessages' | 'loadUser' |
|
||||
'closeLocalTextSearch' | 'exitMessageSelectMode'>;
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'openChat' | 'unpinAllMessages' | 'loadUser' | 'closeLocalTextSearch' | 'exitMessageSelectMode'
|
||||
)>;
|
||||
|
||||
const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined;
|
||||
|
||||
@ -129,6 +131,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
const [isFabShown, setIsFabShown] = useState<boolean | undefined>();
|
||||
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
|
||||
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
|
||||
const [isReady, setIsReady] = useState(!IS_SINGLE_COLUMN_LAYOUT || animationLevel === ANIMATION_LEVEL_MIN);
|
||||
|
||||
const hasTools = hasPinnedOrAudioMessage && (
|
||||
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|
||||
@ -162,6 +165,18 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
setIsNotchShown(undefined);
|
||||
}, [chatId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (animationLevel === ANIMATION_LEVEL_MIN) {
|
||||
setIsReady(true);
|
||||
}
|
||||
}, [animationLevel]);
|
||||
|
||||
const handleTransitionEnd = (e: React.TransitionEvent<HTMLDivElement>) => {
|
||||
if (e.propertyName === 'transform' && e.target === e.currentTarget) {
|
||||
setIsReady(Boolean(chatId));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isPrivate) {
|
||||
loadUser({ userId: chatId });
|
||||
@ -263,6 +278,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
<div
|
||||
id="MiddleColumn"
|
||||
className={className}
|
||||
onTransitionEnd={handleTransitionEnd}
|
||||
// @ts-ignore teact-feature
|
||||
style={`
|
||||
--composer-hidden-scale: ${composerHiddenScale};
|
||||
@ -290,6 +306,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
chatId={renderingChatId}
|
||||
threadId={renderingThreadId}
|
||||
messageListType={renderingMessageListType}
|
||||
isReady={isReady}
|
||||
/>
|
||||
<Transition
|
||||
name={shouldSkipHistoryAnimations ? 'none' : animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
|
||||
@ -307,6 +324,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
hasTools={renderingHasTools}
|
||||
onFabToggle={setIsFabShown}
|
||||
onNotchToggle={setIsNotchShown}
|
||||
isReady={isReady}
|
||||
/>
|
||||
<div className={footerClassName}>
|
||||
{renderingCanPost && (
|
||||
@ -316,6 +334,7 @@ const MiddleColumn: FC<StateProps & DispatchProps> = ({
|
||||
messageListType={renderingMessageListType}
|
||||
dropAreaState={dropAreaState}
|
||||
onDropHide={handleHideDropArea}
|
||||
isReady={isReady}
|
||||
/>
|
||||
)}
|
||||
{isPinnedMessageList && (
|
||||
|
||||
@ -73,6 +73,7 @@ type OwnProps = {
|
||||
chatId: number;
|
||||
threadId: number;
|
||||
messageListType: MessageListType;
|
||||
isReady?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -106,6 +107,7 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
messageListType,
|
||||
isReady,
|
||||
pinnedMessageIds,
|
||||
messagesById,
|
||||
canUnpin,
|
||||
@ -143,10 +145,10 @@ const MiddleHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (threadId === MAIN_THREAD_ID && lastSyncTime) {
|
||||
if (threadId === MAIN_THREAD_ID && lastSyncTime && isReady) {
|
||||
loadPinnedMessages({ chatId });
|
||||
}
|
||||
}, [chatId, loadPinnedMessages, lastSyncTime, threadId]);
|
||||
}, [chatId, loadPinnedMessages, lastSyncTime, threadId, isReady]);
|
||||
|
||||
// Reset pinned index when switching chats and pinning/unpinning
|
||||
useEffect(() => {
|
||||
|
||||
@ -244,6 +244,7 @@ const AttachmentModal: FC<OwnProps> = ({
|
||||
/>
|
||||
<MessageInput
|
||||
id="caption-input-text"
|
||||
isAttachmentModalInput
|
||||
html={caption}
|
||||
editableInputId={EDITABLE_INPUT_MODAL_ID}
|
||||
placeholder={lang('Caption')}
|
||||
|
||||
@ -94,6 +94,7 @@ type OwnProps = {
|
||||
messageListType: MessageListType;
|
||||
dropAreaState: string;
|
||||
onDropHide: NoneToVoidFunction;
|
||||
isReady: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -156,6 +157,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
shouldSchedule,
|
||||
canScheduleUntilOnline,
|
||||
onDropHide,
|
||||
isReady,
|
||||
editingMessage,
|
||||
chatId,
|
||||
threadId,
|
||||
@ -225,15 +227,13 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}, [chatId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID) {
|
||||
if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID && isReady) {
|
||||
loadScheduledHistory();
|
||||
}
|
||||
}, [chatId, loadScheduledHistory, lastSyncTime, threadId]);
|
||||
}, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!appendixRef.current) {
|
||||
return;
|
||||
}
|
||||
if (!appendixRef.current) return;
|
||||
|
||||
appendixRef.current.innerHTML = APPENDIX;
|
||||
}, []);
|
||||
@ -303,6 +303,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
Boolean(shouldSuggestStickers && allowedAttachmentOptions.canSendStickers && !attachments.length),
|
||||
html,
|
||||
stickersForEmoji,
|
||||
!isReady,
|
||||
);
|
||||
const {
|
||||
isEmojiTooltipOpen, closeEmojiTooltip, filteredEmojis, insertEmoji,
|
||||
@ -314,6 +315,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
setHtml,
|
||||
baseEmojiKeywords,
|
||||
emojiKeywords,
|
||||
!isReady,
|
||||
);
|
||||
|
||||
const insertTextAndUpdateCursor = useCallback((text: string, inputId: string = EDITABLE_INPUT_ID) => {
|
||||
@ -608,6 +610,8 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}, [isRightColumnShown, closeSymbolMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isReady) return;
|
||||
|
||||
if (isSelectModeActive) {
|
||||
disableHover();
|
||||
} else {
|
||||
@ -615,7 +619,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
enableHover();
|
||||
}, SELECT_MODE_TRANSITION_MS);
|
||||
}
|
||||
}, [isSelectModeActive, enableHover, disableHover]);
|
||||
}, [isSelectModeActive, enableHover, disableHover, isReady]);
|
||||
|
||||
const mainButtonHandler = useCallback(() => {
|
||||
switch (mainButtonState) {
|
||||
@ -687,7 +691,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{allowedAttachmentOptions.canAttachMedia && (
|
||||
{allowedAttachmentOptions.canAttachMedia && isReady && (
|
||||
<Portal containerId="#middle-column-portals">
|
||||
<DropArea
|
||||
isOpen={dropAreaState !== DropAreaState.None}
|
||||
@ -763,7 +767,7 @@ const Composer: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
>
|
||||
<i className="icon-smile" />
|
||||
<i className="icon-keyboard" />
|
||||
<Spinner color="gray" />
|
||||
{!isSymbolMenuLoaded && <Spinner color="gray" />}
|
||||
</Button>
|
||||
) : (
|
||||
<ResponsiveHoverButton
|
||||
|
||||
@ -32,6 +32,7 @@ const TRANSITION_DURATION_FACTOR = 50;
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
isAttachmentModalInput?: boolean;
|
||||
editableInputId?: string;
|
||||
html: string;
|
||||
placeholder: string;
|
||||
@ -73,6 +74,7 @@ function clearSelection() {
|
||||
|
||||
const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
id,
|
||||
isAttachmentModalInput,
|
||||
editableInputId,
|
||||
html,
|
||||
placeholder,
|
||||
@ -101,8 +103,9 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const [selectedRange, setSelectedRange] = useState<Range>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAttachmentModalInput) return;
|
||||
updateInputHeight(false);
|
||||
}, []);
|
||||
}, [isAttachmentModalInput]);
|
||||
|
||||
useLayoutEffectWithPrevDeps(([prevHtml]) => {
|
||||
if (html !== inputRef.current!.innerHTML) {
|
||||
|
||||
@ -19,6 +19,14 @@
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0);
|
||||
}
|
||||
}
|
||||
|
||||
body.animation-level-0 & {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
&:not(.middle-column-open) {
|
||||
transform: translate3d(100vw, 0, 0) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&-main {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiSticker, ApiVideo } from '../../../api/types';
|
||||
|
||||
@ -38,10 +39,14 @@ export type OwnProps = {
|
||||
addRecentEmoji: AnyToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
isLeftColumnShown: boolean;
|
||||
};
|
||||
|
||||
let isActivated = false;
|
||||
|
||||
const SymbolMenu: FC<OwnProps> = ({
|
||||
isOpen, allowedAttachmentOptions,
|
||||
const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
isOpen, allowedAttachmentOptions, isLeftColumnShown,
|
||||
onLoad, onClose,
|
||||
onEmojiSelect, onStickerSelect, onGifSelect,
|
||||
onRemoveSymbol, onSearchOpen, addRecentEmoji,
|
||||
@ -188,6 +193,7 @@ const SymbolMenu: FC<OwnProps> = ({
|
||||
const className = buildClassName(
|
||||
'SymbolMenu mobile-menu',
|
||||
transitionClassNames,
|
||||
!isLeftColumnShown && 'middle-column-open',
|
||||
);
|
||||
|
||||
return (
|
||||
@ -216,4 +222,10 @@ const SymbolMenu: FC<OwnProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SymbolMenu);
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
isLeftColumnShown: global.isLeftColumnShown,
|
||||
};
|
||||
},
|
||||
)(SymbolMenu));
|
||||
|
||||
@ -36,6 +36,7 @@ export default function useEmojiTooltip(
|
||||
onUpdateHtml: (html: string) => void,
|
||||
baseEmojiKeywords?: Record<string, string[]>,
|
||||
emojiKeywords?: Record<string, string[]>,
|
||||
isDisabled = false,
|
||||
) {
|
||||
const [isOpen, markIsOpen, unmarkIsOpen] = useFlag();
|
||||
|
||||
@ -60,6 +61,7 @@ export default function useEmojiTooltip(
|
||||
|
||||
// Initialize data on first render.
|
||||
useEffect(() => {
|
||||
if (isDisabled) return;
|
||||
const exec = () => {
|
||||
setById(emojiData.emojis);
|
||||
};
|
||||
@ -70,10 +72,10 @@ export default function useEmojiTooltip(
|
||||
ensureEmojiData()
|
||||
.then(exec);
|
||||
}
|
||||
}, []);
|
||||
}, [isDisabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!byId) {
|
||||
if (!byId || isDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -107,7 +109,7 @@ export default function useEmojiTooltip(
|
||||
}, {} as Record<string, Emoji[]>);
|
||||
setByName(emojisByName);
|
||||
setNames(Object.keys(emojisByName));
|
||||
}, [baseEmojiKeywords, byId, emojiKeywords]);
|
||||
}, [isDisabled, baseEmojiKeywords, byId, emojiKeywords]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAllowed || !html || !byId || !keywords || !keywords.length) {
|
||||
|
||||
@ -11,6 +11,7 @@ export default function useStickerTooltip(
|
||||
isAllowed: boolean,
|
||||
html: string,
|
||||
stickers?: ApiSticker[],
|
||||
isDisabled = false,
|
||||
) {
|
||||
const { loadStickersForEmoji, clearStickersForEmoji } = getDispatch();
|
||||
const isSingleEmoji = (
|
||||
@ -20,6 +21,8 @@ export default function useStickerTooltip(
|
||||
const hasStickers = Boolean(stickers) && isSingleEmoji;
|
||||
|
||||
useEffect(() => {
|
||||
if (isDisabled) return;
|
||||
|
||||
if (isAllowed && isSingleEmoji) {
|
||||
loadStickersForEmoji({ emoji: html });
|
||||
} else if (hasStickers || !isSingleEmoji) {
|
||||
@ -27,7 +30,7 @@ export default function useStickerTooltip(
|
||||
}
|
||||
// We omit `hasStickers` here to prevent re-fetching after manually closing tooltip (via <Esc>).
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed]);
|
||||
}, [html, isSingleEmoji, clearStickersForEmoji, loadStickersForEmoji, isAllowed, isDisabled]);
|
||||
|
||||
return {
|
||||
isStickerTooltipOpen: hasStickers,
|
||||
|
||||
@ -416,7 +416,7 @@ export type ActionTypes = (
|
||||
'joinChannel' | 'leaveChannel' | 'deleteChannel' | 'toggleChatPinned' | 'toggleChatArchived' | 'toggleChatUnread' |
|
||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' |
|
||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
||||
// messages
|
||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
||||
|
||||
@ -54,3 +54,23 @@ addReducer('resetChatCreation', (global) => {
|
||||
chatCreation: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('openNextChat', (global, actions, payload) => {
|
||||
const { targetIndexDelta, orderedIds } = payload;
|
||||
|
||||
const { chatId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
if (!chatId) {
|
||||
actions.openChat({ id: orderedIds[0] });
|
||||
return;
|
||||
}
|
||||
|
||||
const position = orderedIds.indexOf(chatId);
|
||||
|
||||
if (position === -1) {
|
||||
return;
|
||||
}
|
||||
const nextId = orderedIds[position + targetIndexDelta];
|
||||
|
||||
actions.openChat({ id: nextId });
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ import { getChatTitle } from './chats';
|
||||
|
||||
const CONTENT_NOT_SUPPORTED = 'The message is not supported on this version of Telegram';
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
const TRUNCATED_SUMMARY_LENGTH = 80;
|
||||
|
||||
export function getMessageKey(message: ApiMessage) {
|
||||
const { chatId, id } = message;
|
||||
@ -32,16 +33,18 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
|
||||
text, photo, video, audio, voice, document, sticker, contact, poll, invoice,
|
||||
} = message.content;
|
||||
|
||||
const truncatedText = text && text.text.substr(0, TRUNCATED_SUMMARY_LENGTH);
|
||||
|
||||
if (message.groupedId) {
|
||||
return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('lng_in_dlg_album')}`;
|
||||
return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('lng_in_dlg_album')}`;
|
||||
}
|
||||
|
||||
if (photo) {
|
||||
return `${noEmoji ? '' : '🖼 '}${text ? text.text : lang('AttachPhoto')}`;
|
||||
return `${noEmoji ? '' : '🖼 '}${truncatedText || lang('AttachPhoto')}`;
|
||||
}
|
||||
|
||||
if (video) {
|
||||
return `${noEmoji ? '' : '📹 '}${text ? text.text : lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`;
|
||||
return `${noEmoji ? '' : '📹 '}${truncatedText || lang(video.isGif ? 'AttachGif' : 'AttachVideo')}`;
|
||||
}
|
||||
|
||||
if (sticker) {
|
||||
@ -53,11 +56,11 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
|
||||
}
|
||||
|
||||
if (voice) {
|
||||
return `${noEmoji ? '' : '🎤 '}${text ? text.text : lang('AttachAudio')}`;
|
||||
return `${noEmoji ? '' : '🎤 '}${truncatedText || lang('AttachAudio')}`;
|
||||
}
|
||||
|
||||
if (document) {
|
||||
return `${noEmoji ? '' : '📎 '}${text ? text.text : document.fileName}`;
|
||||
return `${noEmoji ? '' : '📎 '}${truncatedText || document.fileName}`;
|
||||
}
|
||||
|
||||
if (contact) {
|
||||
@ -73,7 +76,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
|
||||
}
|
||||
|
||||
if (text) {
|
||||
return text.text;
|
||||
return truncatedText;
|
||||
}
|
||||
|
||||
return CONTENT_NOT_SUPPORTED;
|
||||
|
||||
@ -47,9 +47,10 @@ export function replaceChats(global: GlobalState, newById: Record<number, ApiCha
|
||||
};
|
||||
}
|
||||
|
||||
export function updateChat(
|
||||
// @optimization Don't spread/unspread global for each element, do it in a batch
|
||||
function getUpdatedChat(
|
||||
global: GlobalState, chatId: number, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
): GlobalState {
|
||||
): ApiChat {
|
||||
const { byId } = global.chats;
|
||||
const chat = byId[chatId];
|
||||
const shouldOmitMinInfo = chatUpdate.isMin && chat && !chat.isMin;
|
||||
@ -60,9 +61,19 @@ export function updateChat(
|
||||
};
|
||||
|
||||
if (!updatedChat.id || !updatedChat.type) {
|
||||
return global;
|
||||
return updatedChat;
|
||||
}
|
||||
|
||||
return updatedChat;
|
||||
}
|
||||
|
||||
export function updateChat(
|
||||
global: GlobalState, chatId: number, chatUpdate: Partial<ApiChat>, photo?: ApiPhoto,
|
||||
): GlobalState {
|
||||
const { byId } = global.chats;
|
||||
|
||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, photo);
|
||||
|
||||
return replaceChats(global, {
|
||||
...byId,
|
||||
[chatId]: updatedChat,
|
||||
@ -70,8 +81,17 @@ export function updateChat(
|
||||
}
|
||||
|
||||
export function updateChats(global: GlobalState, updatedById: Record<number, ApiChat>): GlobalState {
|
||||
Object.keys(updatedById).forEach((id) => {
|
||||
global = updateChat(global, Number(id), updatedById[Number(id)]);
|
||||
const updatedChats = Object.keys(updatedById).map(Number).reduce<Record<number, ApiChat>>((acc, id) => {
|
||||
const updatedChat = getUpdatedChat(global, id, updatedById[id]);
|
||||
if (updatedChat) {
|
||||
acc[id] = updatedChat;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
global = replaceChats(global, {
|
||||
...global.chats.byId,
|
||||
...updatedChats,
|
||||
});
|
||||
|
||||
return global;
|
||||
@ -80,10 +100,19 @@ export function updateChats(global: GlobalState, updatedById: Record<number, Api
|
||||
// @optimization Allows to avoid redundant updates which cause a lot of renders
|
||||
export function addChats(global: GlobalState, addedById: Record<number, ApiChat>): GlobalState {
|
||||
const { byId } = global.chats;
|
||||
Object.keys(addedById).map(Number).forEach((id) => {
|
||||
const addedChats = Object.keys(addedById).map(Number).reduce<Record<number, ApiChat>>((acc, id) => {
|
||||
if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) {
|
||||
global = updateChat(global, id, addedById[id]);
|
||||
const updatedChat = getUpdatedChat(global, id, addedById[id]);
|
||||
if (updatedChat) {
|
||||
acc[id] = updatedChat;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
global = replaceChats(global, {
|
||||
...global.chats.byId,
|
||||
...addedChats,
|
||||
});
|
||||
|
||||
return global;
|
||||
|
||||
@ -13,29 +13,54 @@ export function replaceUsers(global: GlobalState, newById: Record<number, ApiUse
|
||||
},
|
||||
};
|
||||
}
|
||||
export function updateUser(global: GlobalState, userId: number, userUpdate: Partial<ApiUser>): GlobalState {
|
||||
|
||||
// @optimization Don't spread/unspread global for each element, do it in a batch
|
||||
function getUpdatedUser(global: GlobalState, userId: number, userUpdate: Partial<ApiUser>): ApiUser {
|
||||
const { byId } = global.users;
|
||||
const { hash, userIds: contactUserIds } = global.contactList || {};
|
||||
const user = byId[userId];
|
||||
const shouldOmitMinInfo = userUpdate.isMin && user && !user.isMin;
|
||||
|
||||
const updatedUser = {
|
||||
...user,
|
||||
...(shouldOmitMinInfo ? omit(userUpdate, ['isMin', 'accessHash']) : userUpdate),
|
||||
};
|
||||
|
||||
if (!updatedUser.id || !updatedUser.type) {
|
||||
return global;
|
||||
return user;
|
||||
}
|
||||
|
||||
if (updatedUser.isContact && (contactUserIds && !contactUserIds.includes(userId))) {
|
||||
global = {
|
||||
...global,
|
||||
contactList: {
|
||||
hash: hash || 0,
|
||||
userIds: [userId, ...contactUserIds],
|
||||
},
|
||||
};
|
||||
}
|
||||
return updatedUser;
|
||||
}
|
||||
|
||||
function updateContactList(global: GlobalState, updatedUsers: ApiUser[]): GlobalState {
|
||||
const { hash, userIds: contactUserIds } = global.contactList || {};
|
||||
|
||||
if (!contactUserIds) return global;
|
||||
|
||||
const newContactUserIds = updatedUsers
|
||||
.filter((user) => user && user.isContact && !contactUserIds.includes(user.id))
|
||||
.map((user) => user.id);
|
||||
|
||||
if (newContactUserIds.length === 0) return global;
|
||||
|
||||
return {
|
||||
...global,
|
||||
contactList: {
|
||||
hash: hash || 0,
|
||||
userIds: [
|
||||
...newContactUserIds,
|
||||
...contactUserIds,
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function updateUser(global: GlobalState, userId: number, userUpdate: Partial<ApiUser>): GlobalState {
|
||||
const { byId } = global.users;
|
||||
|
||||
const updatedUser = getUpdatedUser(global, userId, userUpdate);
|
||||
|
||||
global = updateContactList(global, [updatedUser]);
|
||||
|
||||
return replaceUsers(global, {
|
||||
...byId,
|
||||
@ -43,9 +68,21 @@ export function updateUser(global: GlobalState, userId: number, userUpdate: Part
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function updateUsers(global: GlobalState, updatedById: Record<number, ApiUser>): GlobalState {
|
||||
Object.keys(updatedById).map(Number).forEach((id) => {
|
||||
global = updateUser(global, id, updatedById[id]);
|
||||
const updatedUsers = Object.keys(updatedById).map(Number).reduce<Record<number, ApiUser>>((acc, id) => {
|
||||
const updatedUser = getUpdatedUser(global, id, updatedById[id]);
|
||||
if (updatedUser) {
|
||||
acc[id] = updatedUser;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
global = updateContactList(global, Object.values(updatedUsers));
|
||||
|
||||
global = replaceUsers(global, {
|
||||
...global.users.byId,
|
||||
...updatedUsers,
|
||||
});
|
||||
|
||||
return global;
|
||||
@ -54,10 +91,22 @@ export function updateUsers(global: GlobalState, updatedById: Record<number, Api
|
||||
// @optimization Allows to avoid redundant updates which cause a lot of renders
|
||||
export function addUsers(global: GlobalState, addedById: Record<number, ApiUser>): GlobalState {
|
||||
const { byId } = global.users;
|
||||
Object.keys(addedById).map(Number).forEach((id) => {
|
||||
|
||||
const addedUsers = Object.keys(addedById).map(Number).reduce<Record<number, ApiUser>>((acc, id) => {
|
||||
if (!byId[id] || (byId[id].isMin && !addedById[id].isMin)) {
|
||||
global = updateUser(global, id, addedById[id]);
|
||||
const updatedUser = getUpdatedUser(global, id, addedById[id]);
|
||||
if (updatedUser) {
|
||||
acc[id] = updatedUser;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
global = updateContactList(global, Object.values(addedUsers));
|
||||
|
||||
global = replaceUsers(global, {
|
||||
...global.users.byId,
|
||||
...addedUsers,
|
||||
});
|
||||
|
||||
return global;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user