diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index ab2005b17..181c4ed39 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -981,6 +981,7 @@ "EventLogFilterEditedMessages" = "Edited messages"; "EventLogFilterLeavingMembers" = "Members leaving"; "ChannelManagementTitle" = "Admins"; +"MyProfileHeader" = "My Profile"; "EventLogAllAdmins" = "All admins"; "UserRestrictionsCanDo" = "What can this user do?"; "UserRestrictionsBlock" = "Ban and remove from group"; diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 66a7e4cd4..03ba7f4e7 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -68,6 +68,7 @@ import styles from './ChatExtra.module.scss'; type OwnProps = { chatOrUserId: string; + isOwnProfile?: boolean; isSavedDialog?: boolean; isInSettings?: boolean; className?: string; @@ -106,7 +107,7 @@ const ChatExtra: FC = ({ user, chat, userFullInfo, - isInSettings, + isOwnProfile, canInviteUsers, isMuted, phoneCodeList, @@ -122,6 +123,7 @@ const ChatExtra: FC = ({ botVerification, className, style, + isInSettings, }) => { const { showNotification, @@ -283,7 +285,7 @@ const ChatExtra: FC = ({ }, { withNodes: true }); const isRestricted = chatId ? selectIsChatRestricted(getGlobal(), chatId) : false; - if (isRestricted || (isSelf && !isInSettings)) { + if (isRestricted || (isSelf && !isOwnProfile && !isInSettings)) { return undefined; } @@ -422,7 +424,7 @@ const ChatExtra: FC = ({ )} - {!isInSettings && ( + {!isOwnProfile && !isInSettings && ( {lang('Notifications')} = ({ {oldLang('BusinessProfileLocation')} )} - {hasSavedMessages && !isInSettings && ( + {hasSavedMessages && !isOwnProfile && !isInSettings && ( {oldLang('SavedMessagesTab')} diff --git a/src/components/common/profile/ProfileInfo.module.scss b/src/components/common/profile/ProfileInfo.module.scss index d5c02e348..5311a4133 100644 --- a/src/components/common/profile/ProfileInfo.module.scss +++ b/src/components/common/profile/ProfileInfo.module.scss @@ -27,6 +27,7 @@ } .plain.minimized { + padding-block: 1.5rem 0; color: var(--color-text); .userRatingNegativeWrapper, @@ -34,6 +35,14 @@ color: var(--color-white); } + .status { + color: var(--color-text-secondary); + } + + .getStatus, .userStatus { + opacity: 1; + } + :global(.VerifiedIcon), :global(.StarIcon) { --color-fill: var(--color-primary); diff --git a/src/components/left/main/LeftSideMenuItems.tsx b/src/components/left/main/LeftSideMenuItems.tsx index 872e914bf..61fcc467f 100644 --- a/src/components/left/main/LeftSideMenuItems.tsx +++ b/src/components/left/main/LeftSideMenuItems.tsx @@ -93,7 +93,7 @@ const LeftSideMenuItems = ({ const bots = useMemo(() => Object.values(attachBots).filter((bot) => bot.isForSideMenu), [attachBots]); const handleSelectMyProfile = useLastCallback(() => { - openChatWithInfo({ id: currentUserId, shouldReplaceHistory: true, profileTab: 'stories' }); + openChatWithInfo({ id: currentUserId, shouldReplaceHistory: true, isOwnProfile: true }); }); const handleSelectSaved = useLastCallback(() => { diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx index 0c22267f6..0e13326f6 100644 --- a/src/components/middle/HeaderMenuContainer.tsx +++ b/src/components/middle/HeaderMenuContainer.tsx @@ -874,6 +874,7 @@ export default memo(withGlobal( const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const savedDialog = isSavedDialog ? selectChat(global, String(threadId)) : undefined; const isAccountFrozen = selectIsCurrentUserFrozen(global); + const chatInfo = selectTabState(global).chatInfo; return { chat, @@ -889,8 +890,7 @@ export default memo(withGlobal( hasLinkedChat: Boolean(chatFullInfo?.linkedChatId), botCommands: chatBot ? userFullInfo?.botInfo?.commands : undefined, botPrivacyPolicyUrl: chatBot ? userFullInfo?.botInfo?.privacyPolicyUrl : undefined, - isChatInfoShown: selectTabState(global).isChatInfoShown - && currentChatId === chatId && currentThreadId === threadId, + isChatInfoShown: chatInfo.isOpen && currentChatId === chatId && currentThreadId === threadId, canCreateTopic, canEditTopic, canManage, diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx index 1f721119a..27ab8c204 100644 --- a/src/components/middle/MiddleHeader.tsx +++ b/src/components/middle/MiddleHeader.tsx @@ -147,7 +147,8 @@ const MiddleHeader: FC = ({ const handleOpenChat = useLastCallback((event: React.MouseEvent | React.TouchEvent) => { if ((event.target as Element).closest('.title > .custom-emoji')) return; - openThreadWithInfo({ chatId, threadId }); + // Force close My Profile if clicked on Saved Messages header + openThreadWithInfo({ chatId, threadId, isOwnProfile: false }); }); const { diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 0f2d87e52..ecc95b453 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -1,4 +1,3 @@ -import type { FC } from '@teact'; import { memo, useCallback, useEffect, useMemo, useRef, useState } from '@teact'; import { getActions, getGlobal, withGlobal } from '../../global'; @@ -167,7 +166,7 @@ type StateProps = { isRestricted?: boolean; activeDownloads: TabState['activeDownloads']; isChatProtected?: boolean; - nextProfileTab?: ProfileTabType; + chatInfo: TabState['chatInfo']; animationLevel: AnimationLevel; shouldWarnAboutFiles?: boolean; similarChannels?: string[]; @@ -177,7 +176,7 @@ type StateProps = { limitSimilarPeers: number; isTopicInfo?: boolean; isSavedDialog?: boolean; - forceScrollProfileTab?: boolean; + isSavedMessages?: boolean; isSynced?: boolean; hasAvatar?: boolean; }; @@ -197,10 +196,13 @@ const TABS: TabProps[] = [ const HIDDEN_RENDER_DELAY = 1000; const INTERSECTION_THROTTLE = 500; -const Profile: FC = ({ +const SHARED_MEDIA_TYPES = new Set(['media', 'documents', 'links', 'audio', 'voice']); + +const Profile = ({ chatId, isActive, threadId, + chatInfo, profileState, theme, monoforumChannel, @@ -239,7 +241,6 @@ const Profile: FC = ({ isRestricted, activeDownloads, isChatProtected, - nextProfileTab, animationLevel, shouldWarnAboutFiles, similarChannels, @@ -248,11 +249,11 @@ const Profile: FC = ({ limitSimilarPeers, isTopicInfo, isSavedDialog, - forceScrollProfileTab, + isSavedMessages, isSynced, hasAvatar, onProfileStateChange, -}) => { +}: OwnProps & StateProps) => { const { setSharedMediaSearchType, loadMoreMembers, @@ -274,6 +275,7 @@ const Profile: FC = ({ loadStarGiftCollections, loadStoryAlbums, resetSelectedStoryAlbum, + changeProfileTab, } = getActions(); const containerRef = useRef(); @@ -285,15 +287,18 @@ const Profile: FC = ({ const [deletingUserId, setDeletingUserId] = useState(); const [isGiftTransitionEnabled, enableGiftTransition, disableGiftTransition] = useFlag(); + const isClosed = !chatInfo.isOpen; + const { profileTab, forceScrollProfileTab, isOwnProfile } = chatInfo; + const profileId = isSavedDialog ? String(threadId) : chatId; - const isSavedMessages = profileId === currentUserId && !isSavedDialog; + const isGeneralSavedMessages = isSavedMessages && !isSavedDialog; const [isProfileExpanded, expandProfile, collapseProfile] = useFlag(); const [restoreContentHeightKey, setRestoreContentHeightKey] = useState(0); const tabs = useMemo(() => { const arr: TabProps[] = []; - if (isSavedMessages && !isSavedDialog) { + if (isGeneralSavedMessages) { arr.push({ type: 'dialogs', key: 'ProfileTabSavedDialogs' }); } @@ -301,7 +306,7 @@ const Profile: FC = ({ arr.push({ type: 'stories', key: 'ProfileTabStories' }); } - if (hasStoriesTab && isSavedMessages) { + if (hasStoriesTab && isOwnProfile) { arr.push({ type: 'storiesArchive', key: 'ProfileTabStoriesArchive' }); } @@ -309,67 +314,73 @@ const Profile: FC = ({ arr.push({ type: 'gifts', key: 'ProfileTabGifts' }); } - if (hasMembersTab) { + if (hasMembersTab && !isOwnProfile) { arr.push({ type: 'members', key: isChannel ? 'ProfileTabSubscribers' : 'ProfileTabMembers' }); } - if (hasPreviewMediaTab) { + if (hasPreviewMediaTab && !isOwnProfile) { arr.push({ type: 'previewMedia', key: 'ProfileTabBotPreview' }); } - arr.push(...TABS); + if (!isOwnProfile) { + arr.push(...TABS); + } // Voice messages filter currently does not work in forum topics. Return it when it's fixed on the server side. - if (!isTopicInfo) { + if (!isTopicInfo && !isOwnProfile) { arr.push({ type: 'voice', key: 'ProfileTabVoice' }); } - if (hasCommonChatsTab) { + if (hasCommonChatsTab && !isOwnProfile) { arr.push({ type: 'commonChats', key: 'ProfileTabSharedGroups' }); } - if (isChannel && similarChannels?.length) { + if (isChannel && similarChannels?.length && !isOwnProfile) { arr.push({ type: 'similarChannels', key: 'ProfileTabSimilarChannels' }); } - if (isBot && similarBots?.length) { + if (isBot && similarBots?.length && !isOwnProfile) { arr.push({ type: 'similarBots', key: 'ProfileTabSimilarBots' }); } + // Fallback to prevent errors in edge cases + // TODO: Handle no tabs case, skip shared media block + if (!arr.length) { + arr.push(TABS[0]); + } + return arr.map((tab) => ({ type: tab.type, title: lang(tab.key), })); }, [ - isSavedMessages, isSavedDialog, hasStoriesTab, hasGiftsTab, hasMembersTab, hasPreviewMediaTab, isTopicInfo, - hasCommonChatsTab, isChannel, isBot, similarChannels?.length, similarBots?.length, lang, + isGeneralSavedMessages, hasStoriesTab, hasGiftsTab, hasMembersTab, hasPreviewMediaTab, isTopicInfo, + hasCommonChatsTab, isChannel, isBot, similarChannels?.length, similarBots?.length, lang, isOwnProfile, ]); - const initialTab = useMemo(() => { - if (!nextProfileTab) { - return 0; - } - - const index = tabs.findIndex(({ type }) => type === nextProfileTab); - return index === -1 ? 0 : index; - }, [nextProfileTab, tabs]); - const [allowAutoScrollToTabs, startAutoScrollToTabsIfNeeded, stopAutoScrollToTabs] = useFlag(false); - const [activeTab, setActiveTab] = useState(initialTab); + const setActiveTab = useLastCallback((type: ProfileTabType) => { + if (isClosed) return; + changeProfileTab({ profileTab: type }); + setSharedMediaSearchType({ mediaType: SHARED_MEDIA_TYPES.has(type) ? type as SharedMediaType : undefined }); + }); useEffect(() => { - if (!nextProfileTab) return; - const index = tabs.findIndex(({ type }) => type === nextProfileTab); + if (isClosed) return; + if (profileTab) { + // Force reset scroll marker + changeProfileTab({ profileTab, shouldScrollTo: undefined }); + return; + }; - if (index === -1) return; - setActiveTab(index); - }, [nextProfileTab, tabs]); + setActiveTab(tabs[0].type); // Set default tab + }, [isClosed, profileTab, tabs]); const handleSwitchTab = useCallback((index: number) => { startAutoScrollToTabsIfNeeded(); - setActiveTab(index); - }, []); + setActiveTab(tabs[index].type); + }, [tabs]); useEffect(() => { if (hasPreviewMediaTab && !botPreviewMedia) { @@ -414,8 +425,12 @@ const Profile: FC = ({ const giftIds = useMemo(() => renderingGifts?.map((gift) => getSavedGiftKey(gift)), [renderingGifts]); - const renderingActiveTab = activeTab > tabs.length - 1 ? tabs.length - 1 : activeTab; - const tabType = tabs[renderingActiveTab].type; + const activeTabIndex = useMemo(() => { + const index = tabs.findIndex(({ type }) => type === profileTab); + return index === -1 ? 0 : index; + }, [profileTab, tabs]); + + const tabType = tabs[activeTabIndex].type; const handleLoadCommonChats = useCallback(() => { loadCommonChats({ userId: chatId }); }, [chatId]); @@ -481,7 +496,9 @@ const Profile: FC = ({ similarBots, }); - const isFirstTab = (isSavedMessages && resultType === 'dialogs') + const shouldRenderProfileInfo = !noProfileInfo && !isSavedMessages; + + const isFirstTab = (isGeneralSavedMessages && resultType === 'dialogs') || (hasStoriesTab && resultType === 'stories') || resultType === 'members' || (!hasMembersTab && resultType === 'media'); @@ -562,11 +579,6 @@ const Profile: FC = ({ setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.InProgress }); }); - // Update search type when switching tabs or forum topics - useEffect(() => { - setSharedMediaSearchType({ mediaType: tabType as SharedMediaType }); - }, [setSharedMediaSearchType, tabType, threadId]); - const handleSelectMedia = useLastCallback((messageId: number) => { openMediaViewer({ chatId: profileId, @@ -602,21 +614,21 @@ const Profile: FC = ({ }); useEffectWithPrevDeps(([prevHasMemberTabs]) => { - if (prevHasMemberTabs === undefined || activeTab === 0 || prevHasMemberTabs === hasMembersTab) { + if (prevHasMemberTabs === undefined || activeTabIndex === 0 || prevHasMemberTabs === hasMembersTab) { return; } - const newActiveTab = activeTab + (hasMembersTab ? 1 : -1); + const newActiveTab = Math.min(activeTabIndex + (hasMembersTab ? 1 : -1), tabs.length - 1); - setActiveTab(Math.min(newActiveTab, tabs.length - 1)); - }, [hasMembersTab, activeTab, tabs]); + setActiveTab(tabs[newActiveTab].type); + }, [hasMembersTab, activeTabIndex, tabs]); const handleResetGiftsFilter = useLastCallback(() => { resetGiftProfileFilter({ peerId: chatId }); }); useTopOverscroll( - containerRef, handleExpandProfile, handleCollapseProfile, !hasAvatar, + containerRef, handleExpandProfile, handleCollapseProfile, !hasAvatar || !shouldRenderProfileInfo, ); useEffect(() => { @@ -628,17 +640,19 @@ const Profile: FC = ({ selectorToPreventScroll: '.Profile', onSwipe: (e, direction) => { if (direction === SwipeDirection.Left) { - setActiveTab(Math.min(renderingActiveTab + 1, tabs.length - 1)); + const nextIndex = Math.min(activeTabIndex + 1, tabs.length - 1); + setActiveTab(tabs[nextIndex].type); return true; } else if (direction === SwipeDirection.Right) { - setActiveTab(Math.max(0, renderingActiveTab - 1)); + const nextIndex = Math.max(0, activeTabIndex - 1); + setActiveTab(tabs[nextIndex].type); return true; } return false; }, }); - }, [renderingActiveTab, tabs.length]); + }, [activeTabIndex, tabs]); let renderingDelay; // @optimization Used to unparallelize rendering of message list and profile media @@ -650,7 +664,7 @@ const Profile: FC = ({ } const canRenderContent = useAsyncRendering([chatId, threadId, resultType, - renderingActiveTab, activeCollectionId, selectedStoryAlbumId], renderingDelay); + activeTabIndex, activeCollectionId, selectedStoryAlbumId], renderingDelay); function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined { return memberId === currentUserId || !canDeleteMembers ? undefined : [{ @@ -1045,6 +1059,7 @@ const Profile: FC = ({ @@ -1095,10 +1110,11 @@ const Profile: FC = ({ } const activeListSelector = `.shared-media-transition > .Transition_slide-active`; + // eslint-disable-next-line @stylistic/max-len + const nestedSelector = `${activeListSelector} > .Transition > .Transition_slide-active > .Transition > .Transition_slide-active`; const itemSelector = !shouldUseTransitionForContent ? `${activeListSelector} .${resultType}-list > .scroll-item` - /* eslint-disable @stylistic/max-len */ - : `${activeListSelector} > .Transition > .Transition_slide-active > .Transition > .Transition_slide-active > .gifts-list > .scroll-item`; + : `${nestedSelector} > .${resultType}-list > .scroll-item`; return ( = ({ > {renderContent()} - + )} @@ -1177,6 +1193,10 @@ export default memo(withGlobal( const userFullInfo = selectUserFullInfo(global, chatId); const messagesById = selectChatMessages(global, chatId); + const tabState = selectTabState(global); + const { chatInfo, savedGifts } = tabState; + const { isOwnProfile } = chatInfo; + const { animationLevel, shouldWarnAboutFiles } = selectSharedSettings(global); const { currentType: mediaSearchType, resultsByType } = selectCurrentSharedMediaSearch(global) || {}; @@ -1187,7 +1207,8 @@ export default memo(withGlobal( const { byId: usersById, statusesById: userStatusesById } = global.users; const { byId: chatsById } = global.chats; - const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); + const isSavedMessages = chatId === global.currentUserId && !isOwnProfile; + const isSavedDialog = !isOwnProfile ? getIsSavedDialog(chatId, threadId, global.currentUserId) : undefined; const isGroup = chat && isChatGroup(chat); const isChannel = chat && isChatChannel(chat); @@ -1210,7 +1231,7 @@ export default memo(withGlobal( const peer = user || chat; const peerFullInfo = userFullInfo || chatFullInfo; - const hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog + const hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedMessages && Boolean(userFullInfo?.commonChatsCount); const commonChats = selectUserCommonChats(global, chatId); @@ -1218,10 +1239,8 @@ export default memo(withGlobal( const botPreviewMedia = global.users.previewMediaByBotId[chatId]; const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories)) - && !isSavedDialog; + && !isSavedMessages; const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined; - const tabState = selectTabState(global); - const { nextProfileTab, forceScrollProfileTab, savedGifts } = tabState; const selectedStoryAlbumId = selectActiveStoriesCollectionId(global); const storyIds = selectedStoryAlbumId !== 'all' ? peerStories?.idsByAlbumId?.[selectedStoryAlbumId]?.ids @@ -1230,7 +1249,7 @@ export default memo(withGlobal( const storyByIds = peerStories?.byId; const archiveStoryIds = peerStories?.archiveIds; - const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedDialog; + const hasGiftsTab = Boolean(peerFullInfo?.starGiftCount) && !isSavedMessages; const activeCollectionId = selectActiveGiftsCollectionId(global, chatId); const peerGifts = savedGifts.collectionsByPeerId[chatId]?.[activeCollectionId]; @@ -1274,8 +1293,7 @@ export default memo(withGlobal( activeCollectionId, giftsFilter: savedGifts.filter, isChatProtected: chat?.isProtected, - nextProfileTab, - forceScrollProfileTab, + chatInfo, animationLevel, shouldWarnAboutFiles, similarChannels: similarChannelIds, @@ -1284,6 +1302,7 @@ export default memo(withGlobal( isCurrentUserPremium, isTopicInfo, isSavedDialog, + isSavedMessages, isSynced: global.isSynced, limitSimilarPeers: selectPremiumLimit(global, 'recommendedChannels'), members: hasMembersTab ? members : undefined, diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 60808b0af..fcaf870f5 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -2,7 +2,7 @@ import type { FC } from '@teact'; import { memo, useEffect, useRef, useState } from '@teact'; import { getActions, withGlobal } from '../../global'; -import type { AnimationLevel, ProfileTabType, ThreadId } from '../../types'; +import type { AnimationLevel, ThreadId } from '../../types'; import { ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent } from '../../types'; import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config'; @@ -55,10 +55,10 @@ type StateProps = { animationLevel: AnimationLevel; shouldSkipHistoryAnimations?: boolean; nextManagementScreen?: ManagementScreens; - nextProfileTab?: ProfileTabType; shouldCloseRightColumn?: boolean; isSavedMessages?: boolean; isSavedDialog?: boolean; + isOwnProfile?: boolean; }; const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY; @@ -81,10 +81,10 @@ const RightColumn: FC = ({ animationLevel, shouldSkipHistoryAnimations, nextManagementScreen, - nextProfileTab, shouldCloseRightColumn, isSavedMessages, isSavedDialog, + isOwnProfile, }) => { const { toggleChatInfo, @@ -100,7 +100,6 @@ const RightColumn: FC = ({ toggleStoryStatistics, setOpenedInviteInfo, requestNextManagementScreen, - resetNextProfileTab, closeCreateTopicPanel, closeEditTopicPanel, closeBoostStatistics, @@ -260,12 +259,6 @@ const RightColumn: FC = ({ } }, [nextManagementScreen]); - useEffect(() => { - if (!nextProfileTab) return; - - resetNextProfileTab(); - }, [nextProfileTab]); - useEffect(() => { if (shouldCloseRightColumn) { close(); @@ -320,7 +313,7 @@ const RightColumn: FC = ({ case RightColumnContent.ChatInfo: return ( ( const areActiveChatsLoaded = selectAreActiveChatsLoaded(global); const { animationLevel } = selectSharedSettings(global); const { - management, shouldSkipHistoryAnimations, nextProfileTab, shouldCloseRightColumn, + management, shouldSkipHistoryAnimations, shouldCloseRightColumn, chatInfo, } = selectTabState(global); const nextManagementScreen = chatId ? management.byChatId[chatId]?.nextScreen : undefined; - const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined; + const isOwnProfile = chatInfo?.isOwnProfile; + const isSavedMessages = chatId && !isOwnProfile ? selectIsChatWithSelf(global, chatId) : undefined; const isSavedDialog = chatId ? getIsSavedDialog(chatId, threadId, global.currentUserId) : undefined; return { @@ -441,10 +435,10 @@ export default memo(withGlobal( animationLevel, shouldSkipHistoryAnimations, nextManagementScreen, - nextProfileTab, shouldCloseRightColumn, isSavedMessages, isSavedDialog, + isOwnProfile, }; }, )(RightColumn)); diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index cbb543a5a..0bd14b591 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../global'; import type { ApiExportedInvite } from '../../api/types'; import type { GiftProfileFilterOptions, ThreadId } from '../../types'; import { MAIN_THREAD_ID } from '../../api/types'; -import { ManagementScreens, ProfileState } from '../../types'; +import { ManagementScreens, ProfileState, SettingsScreens } from '../../types'; import { ANIMATION_END_DELAY, SAVED_FOLDER_ID } from '../../config'; import { @@ -94,6 +94,7 @@ type StateProps = { isInsideTopic?: boolean; canEditTopic?: boolean; isSavedMessages?: boolean; + isOwnProfile?: boolean; }; const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY; @@ -179,6 +180,7 @@ const RightHeader: FC = ({ giftProfileFilter, canUseGiftFilter, canUseGiftAdminFilter, + isOwnProfile, onClose, onScreenSelect, }) => { @@ -192,6 +194,7 @@ const RightHeader: FC = ({ deleteExportedChatInvite, openEditTopicPanel, updateGiftProfileFilter, + openSettingsScreen, } = getActions(); const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); @@ -246,6 +249,10 @@ const RightHeader: FC = ({ toggleStatistics(); }); + const handleEditProfile = useLastCallback(() => { + openSettingsScreen({ screen: SettingsScreens.EditProfile }); + }); + const handleClose = useLastCallback(() => { onClose(!isSavedMessages); }); @@ -345,6 +352,10 @@ const RightHeader: FC = ({ const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1; function getHeaderTitle() { + if (isOwnProfile) { + return lang('MyProfileHeader'); + } + if (isSavedMessages) { return oldLang('SavedMessages'); } @@ -673,6 +684,17 @@ const RightHeader: FC = ({ )} + {isOwnProfile && ( + + )} ); @@ -738,7 +760,8 @@ export default withGlobal( const topic = isInsideTopic ? selectTopic(global, chatId!, threadId!) : undefined; const canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic); const isBot = user && isUserBot(user); - const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined; + const isOwnProfile = tabState.chatInfo?.isOwnProfile; + const isSavedMessages = chatId && !isOwnProfile ? selectIsChatWithSelf(global, chatId) : undefined; const canEditBot = isBot && user?.canEditBot; const canAddContact = user && getCanAddContact(user); @@ -775,6 +798,7 @@ export default withGlobal( giftProfileFilter, canUseGiftFilter, canUseGiftAdminFilter, + isOwnProfile, }; }, )(RightHeader); diff --git a/src/global/actions/ui/chats.ts b/src/global/actions/ui/chats.ts index 5f9e83cb7..650dd978b 100644 --- a/src/global/actions/ui/chats.ts +++ b/src/global/actions/ui/chats.ts @@ -1,9 +1,10 @@ -import type { ActionReturnType } from '../../types'; +import type { ProfileTabType } from '../../../types'; +import type { ActionReturnType, GlobalState } from '../../types'; import { MAIN_THREAD_ID } from '../../../api/types'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { createMessageHashUrl } from '../../../util/routing'; -import { addActionHandler, setGlobal } from '../../index'; +import { addActionHandler, execAfterActions, getGlobal, setGlobal } from '../../index'; import { closeMiddleSearch, exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation, @@ -69,6 +70,10 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe forwardMessages: {}, isShareMessageModalShown: false, }), + // Reset chat info state for new chat + chatInfo: { + isOpen: tabState.chatInfo.isOpen, + }, }, tabId); } @@ -102,33 +107,69 @@ addActionHandler('openPreviousChat', (global, actions, payload): ActionReturnTyp }); addActionHandler('openChatWithInfo', (global, actions, payload): ActionReturnType => { - const { profileTab, forceScrollProfileTab = false, tabId = getCurrentTabId() } = payload; + const { profileTab, forceScrollProfileTab, isOwnProfile, tabId = getCurrentTabId(), ...rest } = payload; - global = updateTabState(global, { - ...selectTabState(global, tabId), - isChatInfoShown: true, - nextProfileTab: profileTab, - forceScrollProfileTab, - }, tabId); - global = { ...global, lastIsChatInfoShown: true }; - setGlobal(global); + const currentMessageList = selectCurrentMessageList(global, tabId); + const isSameMessageList = currentMessageList?.chatId === rest.id + && currentMessageList?.threadId === MAIN_THREAD_ID + && currentMessageList?.type === (rest.type || 'thread'); - actions.openChat({ ...payload, tabId }); + processChatInfoState({ global, isSameMessageList, profileTab, forceScrollProfileTab, isOwnProfile, tabId }); + + actions.openChat({ ...rest, tabId }); }); addActionHandler('openThreadWithInfo', (global, actions, payload): ActionReturnType => { - const { tabId = getCurrentTabId() } = payload; + const { profileTab, forceScrollProfileTab, isOwnProfile, tabId = getCurrentTabId(), ...rest } = payload; - global = updateTabState(global, { - ...selectTabState(global, tabId), - isChatInfoShown: true, - }, tabId); - global = { ...global, lastIsChatInfoShown: true }; - setGlobal(global); + const currentMessageList = selectCurrentMessageList(global, tabId); + const isSameMessageList = currentMessageList?.chatId === rest.chatId + && currentMessageList?.threadId === rest.threadId + && currentMessageList?.type === (rest.type || 'thread'); - actions.openThread({ ...payload, tabId }); + processChatInfoState({ global, isSameMessageList, profileTab, forceScrollProfileTab, isOwnProfile, tabId }); + + actions.openThread({ ...rest, tabId }); }); +function processChatInfoState({ + global, + isSameMessageList, + profileTab, + forceScrollProfileTab, + isOwnProfile, + tabId, +}: { + global: T; + isSameMessageList: boolean; + profileTab?: ProfileTabType; + forceScrollProfileTab?: boolean; + isOwnProfile?: boolean; + tabId: number; +}) { + const currentChatInfo = selectTabState(global, tabId).chatInfo; + + const newProfileTab = profileTab ?? (isSameMessageList ? currentChatInfo.profileTab : undefined); + const newForceScrollProfileTab = forceScrollProfileTab + ?? (isSameMessageList ? currentChatInfo.forceScrollProfileTab : undefined); + const newIsOwnProfile = isOwnProfile ?? (isSameMessageList ? currentChatInfo.isOwnProfile : undefined); + + execAfterActions(() => { + global = getGlobal(); + global = updateTabState(global, { + ...selectTabState(global, tabId), + chatInfo: { + isOpen: true, + profileTab: newProfileTab, + forceScrollProfileTab: newForceScrollProfileTab, + isOwnProfile: newIsOwnProfile, + }, + }, tabId); + global = { ...global, lastIsChatInfoShown: true }; + setGlobal(global); + }); +} + addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => { const { chatId, text, threadId = MAIN_THREAD_ID, files, filter, tabId = getCurrentTabId(), diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index f13aeebaa..68e1ab0e8 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -51,10 +51,18 @@ const MAX_STORED_EMOJIS = 8 * 4; // Represents four rows of recent emojis addActionHandler('toggleChatInfo', (global, actions, payload): ActionReturnType => { const { force, tabId = getCurrentTabId() } = payload || {}; - const isChatInfoShown = force !== undefined ? force : !selectTabState(global, tabId).isChatInfoShown; + const chatInfo = selectTabState(global, tabId).chatInfo; + const willChatInfoBeShown = force !== undefined ? force : !chatInfo.isOpen; - global = updateTabState(global, { isChatInfoShown }, tabId); - global = { ...global, lastIsChatInfoShown: isChatInfoShown }; + if (willChatInfoBeShown !== chatInfo.isOpen) { + global = updateTabState(global, { + chatInfo: { + ...chatInfo, + isOpen: willChatInfoBeShown, + }, + }, tabId); + } + global = { ...global, lastIsChatInfoShown: willChatInfoBeShown }; return global; }); @@ -156,15 +164,24 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe }, tabId); }); -addActionHandler('resetNextProfileTab', (global, actions, payload): ActionReturnType => { - const { tabId = getCurrentTabId() } = payload || {}; +addActionHandler('changeProfileTab', (global, actions, payload): ActionReturnType => { + const { profileTab, shouldScrollTo, tabId = getCurrentTabId() } = payload; const { chatId } = selectCurrentMessageList(global, tabId) || {}; if (!chatId) { return undefined; } - return updateTabState(global, { nextProfileTab: undefined, forceScrollProfileTab: false }, tabId); + const chatInfo = selectTabState(global, tabId).chatInfo; + + return updateTabState(global, { + chatInfo: { + ...chatInfo, + isOpen: true, + profileTab, + forceScrollProfileTab: shouldScrollTo, + }, + }, tabId); }); addActionHandler('toggleStatistics', (global, actions, payload): ActionReturnType => { diff --git a/src/global/index.ts b/src/global/index.ts index f63052d28..40f4bbe58 100644 --- a/src/global/index.ts +++ b/src/global/index.ts @@ -52,5 +52,6 @@ export const addActionHandler = typed.addActionHandler as void; +export const execAfterActions = typed.execAfterActions; export const withGlobal = typed.withGlobal; export type GlobalActions = ReturnType; diff --git a/src/global/init.ts b/src/global/init.ts index 439be509b..57d17addb 100644 --- a/src/global/init.ts +++ b/src/global/init.ts @@ -34,10 +34,14 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => { const initialTabState = cloneDeep(INITIAL_TAB_STATE); initialTabState.id = tabId; - initialTabState.isChatInfoShown = Boolean(global.lastIsChatInfoShown); initialTabState.audioPlayer.playbackRate = global.audioPlayer.lastPlaybackRate; initialTabState.audioPlayer.isPlaybackRateActive = global.audioPlayer.isLastPlaybackRateActive; initialTabState.mediaViewer.playbackRate = global.mediaViewer.lastPlaybackRate; + if (global.lastIsChatInfoShown) { + initialTabState.chatInfo = { + isOpen: true, + }; + } global = { ...global, diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 3ef7f3b6c..730e3e0e7 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -339,7 +339,6 @@ export const INITIAL_TAB_STATE: TabState = { id: 0, isMasterTab: false, isLeftColumnShown: true, - isChatInfoShown: false, newChatMembersProgress: NewChatMembersProgress.Closed, uiReadyState: 0, shouldInit: true, @@ -390,6 +389,10 @@ export const INITIAL_TAB_STATE: TabState = { byChatId: {}, }, + chatInfo: { + isOpen: false, + }, + savedGifts: { filter: { ...DEFAULT_GIFT_PROFILE_FILTER_OPTIONS, diff --git a/src/global/selectors/ui.ts b/src/global/selectors/ui.ts index 982f170cd..7d11b0243 100644 --- a/src/global/selectors/ui.ts +++ b/src/global/selectors/ui.ts @@ -59,7 +59,7 @@ export function selectRightColumnContentKey( RightColumnContent.GifSearch ) : tabState.newChatMembersProgress !== NewChatMembersProgress.Closed ? ( RightColumnContent.AddingMembers - ) : tabState.isChatInfoShown && tabState.messageLists.length ? ( + ) : tabState.chatInfo.isOpen && tabState.messageLists.length ? ( RightColumnContent.ChatInfo ) : undefined; } diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 85383411d..8605ef06c 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -311,7 +311,7 @@ export interface ActionPayloads { hashtag: string; } & WithTabId; setSharedMediaSearchType: { - mediaType: SharedMediaType; + mediaType?: SharedMediaType; } & WithTabId; searchSharedMediaMessages: WithTabId | undefined; searchChatMediaMessages: { @@ -360,8 +360,13 @@ export interface ActionPayloads { openChatWithInfo: ActionPayloads['openChat'] & { profileTab?: ProfileTabType; forceScrollProfileTab?: boolean; + isOwnProfile?: boolean; + } & WithTabId; + openThreadWithInfo: ActionPayloads['openThread'] & { + profileTab?: ProfileTabType; + forceScrollProfileTab?: boolean; + isOwnProfile?: boolean; } & WithTabId; - openThreadWithInfo: ActionPayloads['openThread'] & WithTabId; openLinkedChat: { id: string } & WithTabId; loadMoreMembers: { chatId: string; @@ -1186,6 +1191,7 @@ export interface ActionPayloads { chatId?: string; originMessageId: number; originChannelId: string; + threadId?: never; } | { isComments?: false; chatId: string; @@ -1241,7 +1247,10 @@ export interface ActionPayloads { chatId: string; isEnabled: boolean; }; - resetNextProfileTab: WithTabId | undefined; + changeProfileTab: { + profileTab: ProfileTabType | undefined; + shouldScrollTo?: boolean; + } & WithTabId; openForumPanel: { chatId: string; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 868272cf5..b6cd7d5a8 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -105,7 +105,6 @@ export type TabState = { shouldPreventComposerAnimation?: boolean; inviteHash?: string; canInstall?: boolean; - isChatInfoShown: boolean; isStatisticsShown?: boolean; isLeftColumnShown: boolean; newChatMembersProgress?: NewChatMembersProgress; @@ -126,8 +125,12 @@ export type TabState = { }; shouldCloseRightColumn?: boolean; - nextProfileTab?: ProfileTabType; - forceScrollProfileTab?: boolean; + chatInfo: { + isOpen: boolean; + profileTab?: ProfileTabType; + forceScrollProfileTab?: boolean; + isOwnProfile?: boolean; + }; nextFoldersAction?: ReducerAction; shareFolderScreen?: { folderId: number; diff --git a/src/lib/teact/teactn.tsx b/src/lib/teact/teactn.tsx index f992119d8..61e6d9027 100644 --- a/src/lib/teact/teactn.tsx +++ b/src/lib/teact/teactn.tsx @@ -132,43 +132,59 @@ export function forceOnHeavyAnimationOnce() { } let actionQueue: NoneToVoidFunction[] = []; +let afterActionQueue: NoneToVoidFunction[] = []; function handleAction(name: string, payload?: ActionPayload, options?: ActionOptions): Promise { const deferred = new Deferred(); actionQueue.push(() => { actionHandlers[name]?.forEach((handler) => { - const response = handler(DEBUG ? getUntypedGlobal() : currentGlobal, actions, payload); - if (!response) { + const result = handler(DEBUG ? getUntypedGlobal() : currentGlobal, actions, payload); + if (!result) { deferred.resolve(); return; } - if (typeof response.then === 'function') { - response.then(() => { + if (typeof result.then === 'function') { + result.then(() => { deferred.resolve(); }); return; } - setUntypedGlobal(response as GlobalState, options); + setUntypedGlobal(result as GlobalState, options); deferred.resolve(); }); }); + // Important: Keep 1 as start requirement to avoid immediate nested action calls + // Do not remove element from array before it is executed for the same reason if (actionQueue.length === 1) { try { while (actionQueue.length) { actionQueue[0](); actionQueue.shift(); } + while (afterActionQueue.length) { + afterActionQueue[0](); + afterActionQueue.shift(); + } } finally { actionQueue = []; + afterActionQueue = []; } } return deferred.promise; } +/** + * Execute a function after all actions in stack are executed + * Call only from action handlers + */ +export function execAfterActions(fn: NoneToVoidFunction) { + afterActionQueue.push(fn); +} + function updateContainers() { let DEBUG_startAt: number | undefined; if (DEBUG) { @@ -357,6 +373,7 @@ export function typify< handler: ActionHandlers[ActionName], ) => void, withGlobal: withUntypedGlobal as WithGlobalFn, + execAfterActions, }; } diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 0cd29dc4e..79cfe6fa7 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -856,6 +856,7 @@ export interface LangPair { 'EventLogFilterEditedMessages': undefined; 'EventLogFilterLeavingMembers': undefined; 'ChannelManagementTitle': undefined; + 'MyProfileHeader': undefined; 'EventLogAllAdmins': undefined; 'UserRestrictionsCanDo': undefined; 'UserRestrictionsBlock': undefined;