Topic management: Display icon picker (#2247)

This commit is contained in:
Alexander Zinchuk 2023-01-10 02:07:51 +01:00
parent 9e4ec8286c
commit b4f283d207
29 changed files with 474 additions and 154 deletions

View File

@ -1516,7 +1516,7 @@ export function editTopic({
channel: buildInputPeer(id, accessHash),
topicId,
title,
iconEmojiId: iconEmojiId ? BigInt(iconEmojiId) : undefined,
iconEmojiId: BigInt(iconEmojiId || '0'),
closed: isClosed,
hidden: isHidden,
}), true);

View File

@ -43,7 +43,7 @@ export {
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
removeRecentSticker, clearRecentStickers, fetchCustomEmoji, fetchPremiumGifts, fetchCustomEmojiSets,
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects,
fetchFeaturedEmojiStickers, fetchGenericEmojiEffects, fetchDefaultTopicIcons,
} from './symbols';
export {

View File

@ -248,6 +248,21 @@ export async function fetchPremiumGifts() {
};
}
export async function fetchDefaultTopicIcons() {
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
stickerset: new GramJs.InputStickerSetEmojiDefaultTopicIcons(),
}));
if (!(result instanceof GramJs.messages.StickerSet)) {
return undefined;
}
return {
set: buildStickerSet(result.set),
stickers: processStickerResult(result.documents),
};
}
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
q: query,

View File

@ -76,6 +76,8 @@ export { default as GifSearch } from '../components/right/GifSearch';
export { default as Statistics } from '../components/right/statistics/Statistics';
export { default as MessageStatistics } from '../components/right/statistics/MessageStatistics';
export { default as PollResults } from '../components/right/PollResults';
export { default as CreateTopic } from '../components/right/CreateTopic';
export { default as EditTopic } from '../components/right/EditTopic';
export { default as Management } from '../components/right/management/Management';

View File

@ -20,7 +20,7 @@ import {
isChatSuperGroup,
} from '../../global/helpers';
import {
selectChat, selectChatMessages, selectChatOnlineCount, selectThreadInfo,
selectChat, selectChatMessages, selectChatOnlineCount, selectThreadInfo, selectThreadMessagesCount,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import renderText from './helpers/renderText';
@ -61,6 +61,7 @@ type StateProps =
onlineCount?: number;
areMessagesLoaded: boolean;
animationLevel: AnimationLevel;
messagesCount?: number;
}
& Pick<GlobalState, 'lastSyncTime'>;
@ -85,6 +86,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
animationLevel,
lastSyncTime,
topic,
messagesCount,
onClick,
}) => {
const {
@ -148,7 +150,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
if (isTopic) {
return (
<span className="status" dir="auto">
{threadInfo?.messagesCount ? lang('messages', threadInfo.messagesCount, 'i') : renderText(chat.title)}
{messagesCount ? lang('messages', messagesCount, 'i') : renderText(chat.title)}
</span>
);
}
@ -227,6 +229,7 @@ export default memo(withGlobal<OwnProps>(
const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined;
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
const topic = threadId ? chat?.topics?.[threadId] : undefined;
const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!);
return {
lastSyncTime,
@ -236,6 +239,7 @@ export default memo(withGlobal<OwnProps>(
topic,
areMessagesLoaded,
animationLevel: global.settings.byKey.animationLevel,
messagesCount,
};
},
)(GroupChatInfo));

View File

@ -11,11 +11,10 @@ import type { GlobalState } from '../../global/types';
import type { AnimationLevel } from '../../types';
import { MediaViewerOrigin } from '../../types';
import { GENERAL_TOPIC_ID } from '../../config';
import { IS_TOUCH_ENV } from '../../util/environment';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import {
selectChat, selectCurrentMessageList, selectThreadInfo, selectUser, selectUserStatus,
selectChat, selectCurrentMessageList, selectThreadMessagesCount, selectUser, selectUserStatus,
} from '../../global/selectors';
import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers';
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
@ -187,9 +186,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
/>
<h3 className={styles.topicTitle} dir={lang.isRtl ? 'rtl' : undefined}>{renderText(topic!.title)}</h3>
<p className={styles.topicMessagesCounter}>
{messagesCount && messagesCount > 1
? lang('Chat.Title.Topic', messagesCount + (topic!.id === GENERAL_TOPIC_ID ? 1 : -1), 'i')
: lang('lng_forum_no_messages')}
{messagesCount ? lang('Chat.Title.Topic', messagesCount, 'i') : lang('lng_forum_no_messages')}
</p>
</div>
);
@ -308,7 +305,6 @@ export default memo(withGlobal<OwnProps>(
const { mediaId, avatarOwnerId } = global.mediaViewer;
const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
const threadInfo = currentTopicId ? selectThreadInfo(global, userId, currentTopicId) : undefined;
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;
return {
@ -323,7 +319,7 @@ export default memo(withGlobal<OwnProps>(
avatarOwnerId,
...(topic && {
topic,
messagesCount: threadInfo?.messagesCount,
messagesCount: selectThreadMessagesCount(global, userId, currentTopicId!),
}),
};
},

View File

@ -44,12 +44,16 @@
right: 0;
width: 1.25rem;
height: 1.25rem;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
color: white;
background: var(--premium-gradient);
z-index: 1;
}
&.interactive {

View File

@ -124,7 +124,6 @@ const Chat: FC<OwnProps & StateProps> = ({
}) => {
const {
openChat,
openForumPanel,
focusLastMessage,
loadTopics,
} = getActions();
@ -156,19 +155,12 @@ const Chat: FC<OwnProps & StateProps> = ({
});
const handleClick = useCallback(() => {
if (isForum) {
openForumPanel({ chatId });
return;
}
openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
if (isSelected && canScrollDown) {
focusLastMessage();
}
}, [
isForum, openChat, chatId, isSelected, canScrollDown, openForumPanel, focusLastMessage,
]);
}, [openChat, chatId, isSelected, canScrollDown, focusLastMessage]);
const handleDragEnter = useCallback((e) => {
e.preventDefault();

View File

@ -180,6 +180,7 @@ const Main: FC<StateProps> = ({
loadAvailableReactions,
loadStickerSets,
loadPremiumGifts,
loadDefaultTopicIcons,
loadAddedStickers,
loadFavoriteStickers,
ensureTimeFormat,
@ -222,12 +223,13 @@ const Main: FC<StateProps> = ({
loadAttachBots();
loadContactList();
loadPremiumGifts();
loadDefaultTopicIcons();
checkAppVersion();
}
}, [
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachBots, loadContactList,
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects,
loadPremiumGifts, checkAppVersion, loadConfig, loadGenericEmojiEffects, loadDefaultTopicIcons,
]);
// Language-based API calls

View File

@ -27,8 +27,8 @@ import {
getCanAddContact,
isChatChannel,
isChatGroup,
getHasAdminRight,
getCanManageTopic,
isUserRightBanned,
} from '../../global/helpers';
import useShowTransition from '../../hooks/useShowTransition';
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
@ -91,6 +91,7 @@ type StateProps = {
isPrivate?: boolean;
isMuted?: boolean;
isTopic?: boolean;
isForum?: boolean;
canAddContact?: boolean;
canReportChat?: boolean;
canDeleteChat?: boolean;
@ -113,6 +114,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
botCommands,
withForumActions,
isTopic,
isForum,
isChatInfoShown,
canStartBot,
canRestartBot,
@ -157,6 +159,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
openChatWithInfo,
openCreateTopicPanel,
openEditTopicPanel,
openChat,
} = getActions();
const [isMenuOpen, setIsMenuOpen] = useState(true);
@ -166,7 +169,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
const isViewGroupInfoShown = usePrevDuringAnimation(
(!isChatInfoShown && (withForumActions || isTopic)) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION,
(!isChatInfoShown && isForum) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION,
);
const handleReport = useCallback(() => {
@ -222,6 +225,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
closeMenu();
}, [openEditTopicPanel, chatId, threadId, closeMenu]);
const handleViewAsTopicsClick = useCallback(() => {
openChat({ id: undefined });
closeMenu();
}, [closeMenu, openChat]);
const handleEnterVoiceChatClick = useCallback(() => {
if (canCreateVoiceChat) {
// TODO show popup to schedule
@ -352,6 +360,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
{lang('lng_forum_topic_edit')}
</MenuItem>
)}
{IS_SINGLE_COLUMN_LAYOUT && !withForumActions && isForum && !isTopic && (
<MenuItem
icon="forums"
onClick={handleViewAsTopicsClick}
>
{lang('Chat.ContextViewAsTopics')}
</MenuItem>
)}
{withForumActions && Boolean(pendingJoinRequests) && (
<MenuItem
icon="user"
@ -538,7 +554,7 @@ export default memo(withGlobal<OwnProps>(
);
const topic = chat?.topics?.[threadId];
const canCreateTopic = chat.isForum && (chat.isCreator || getHasAdminRight(chat, 'manageTopics'));
const canCreateTopic = chat.isForum && !isUserRightBanned(chat, 'manageTopics');
const canEditTopic = topic && getCanManageTopic(chat, topic);
return {
@ -546,6 +562,7 @@ export default memo(withGlobal<OwnProps>(
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
isPrivate,
isTopic: chat?.isForum && !isMainThread,
isForum: chat?.isForum,
canAddContact,
canReportChat,
canDeleteChat: getCanDeleteChat(chat),

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../../global';
import type { ApiStickerSet, ApiSticker, ApiChat } from '../../../api/types';
import type { ApiStickerSet, ApiSticker } from '../../../api/types';
import type { StickerSetOrRecent } from '../../../types';
import {
@ -41,17 +41,18 @@ import StickerSetCover from './StickerSetCover';
import './StickerPicker.scss';
type OwnProps = {
chatId: string;
className: string;
chatId?: string;
className?: string;
loadAndPlay: boolean;
withDefaultTopicIcons?: boolean;
onCustomEmojiSelect: (sticker: ApiSticker) => void;
};
type StateProps = {
chat?: ApiChat;
stickerSetsById: Record<string, ApiStickerSet>;
addedCustomEmojiIds?: string[];
recentCustomEmoji: ApiSticker[];
defaultTopicIconsId?: string;
featuredCustomEmojiIds?: string[];
canAnimate?: boolean;
isSavedMessages?: boolean;
@ -74,6 +75,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
canAnimate,
isSavedMessages,
isCurrentUserPremium,
withDefaultTopicIcons,
defaultTopicIconsId,
onCustomEmojiSelect,
}) => {
// eslint-disable-next-line no-null/no-null
@ -124,7 +127,16 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
const defaultSets = [];
if (recentCustomEmoji.length) {
if (withDefaultTopicIcons) {
const defaultTopicIconsPack = stickerSetsById[defaultTopicIconsId!];
if (defaultTopicIconsPack.stickers?.length) {
defaultSets.push({
...defaultTopicIconsPack,
id: RECENT_SYMBOL_SET_ID,
title: lang('RecentStickers'),
});
}
} else if (recentCustomEmoji.length) {
defaultSets.push({
id: RECENT_SYMBOL_SET_ID,
title: lang('RecentStickers'),
@ -144,7 +156,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
...existingAddedSetIds,
...featuredSetIds,
];
}, [addedCustomEmojiIds, featuredCustomEmojiIds, lang, recentCustomEmoji, stickerSetsById]);
}, [
addedCustomEmojiIds, defaultTopicIconsId, featuredCustomEmojiIds, lang, recentCustomEmoji, stickerSetsById,
withDefaultTopicIcons,
]);
const noPopulatedSets = useMemo(() => (
areAddedLoaded
@ -280,6 +295,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
observeIntersection={observeIntersection}
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
isSavedMessages={isSavedMessages}
shouldHideRecentHeader={withDefaultTopicIcons}
withDefaultTopicIcon={withDefaultTopicIcons}
isCustomEmojiPicker
isCurrentUserPremium={isCurrentUserPremium}
onStickerSelect={handleEmojiSelect}
@ -296,7 +313,7 @@ export default memo(withGlobal<OwnProps>(
setsById,
} = global.stickers;
const isSavedMessages = selectIsChatWithSelf(global, chatId);
const isSavedMessages = Boolean(chatId && selectIsChatWithSelf(global, chatId));
const recentCustomEmoji = Object.values(pickTruthy(global.customEmojis.byId, global.recentCustomEmojis));
@ -308,6 +325,7 @@ export default memo(withGlobal<OwnProps>(
isCurrentUserPremium: selectIsCurrentUserPremium(global),
recentCustomEmoji,
featuredCustomEmojiIds: global.customEmojis.featuredIds,
defaultTopicIconsId: global.defaultTopicIconsId,
};
},
)(CustomEmojiPicker));

View File

@ -5,11 +5,7 @@
position: relative;
height: calc(100% - 3rem);
overflow-y: auto;
padding: 0.5rem;
@media (max-width: 600px) {
padding: 0.5rem 0.25rem;
}
padding: 0.5rem 0.25rem;
}
&-header {
@ -92,12 +88,8 @@
}
.symbol-set-container {
width: 25rem;
width: 100%;
line-height: 0;
@media (max-width: 600px) {
width: 100%;
}
}
.sticker-set-button {

View File

@ -1,5 +1,5 @@
import React, {
memo, useCallback, useMemo, useRef,
memo, useCallback, useLayoutEffect, useMemo, useRef, useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global';
@ -10,21 +10,24 @@ import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { useOnIntersect } from '../../../hooks/useIntersectionObserver';
import {
DEFAULT_TOPIC_ICON_STICKER_ID,
EMOJI_SIZE_PICKER, FAVORITE_SYMBOL_SET_ID, RECENT_SYMBOL_SET_ID, STICKER_SIZE_PICKER,
} from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import windowSize from '../../../util/windowSize';
import buildClassName from '../../../util/buildClassName';
import { selectIsAlwaysHighPriorityEmoji, selectIsSetPremium } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag';
import useMediaTransition from '../../../hooks/useMediaTransition';
import { useResizeObserver } from '../../../hooks/useResizeObserver';
import StickerButton from '../../common/StickerButton';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Button from '../../ui/Button';
import grey from '../../../assets/icons/forumTopic/grey.svg';
type OwnProps = {
stickerSet: StickerSetOrRecent;
loadAndPlay: boolean;
@ -34,6 +37,8 @@ type OwnProps = {
isSavedMessages?: boolean;
isCurrentUserPremium?: boolean;
isCustomEmojiPicker?: boolean;
shouldHideRecentHeader?: boolean;
withDefaultTopicIcon?: boolean;
observeIntersection: ObserveFn;
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
onStickerUnfave?: (sticker: ApiSticker) => void;
@ -41,11 +46,11 @@ type OwnProps = {
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
};
const STICKERS_PER_ROW_ON_DESKTOP = 5;
const EMOJI_PER_ROW_ON_DESKTOP = 8;
const STICKER_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 16;
const EMOJI_MARGIN = IS_SINGLE_COLUMN_LAYOUT ? 8 : 10;
const MOBILE_CONTAINER_PADDING = 8;
const CONTAINER_PADDING = 8;
const ITEMS_PER_ROW_FALLBACK = 8;
const StickerSet: FC<OwnProps> = ({
stickerSet,
@ -56,6 +61,8 @@ const StickerSet: FC<OwnProps> = ({
isSavedMessages,
isCurrentUserPremium,
isCustomEmojiPicker,
shouldHideRecentHeader,
withDefaultTopicIcon,
observeIntersection,
onStickerSelect,
onStickerUnfave,
@ -75,11 +82,13 @@ const StickerSet: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
// eslint-disable-next-line no-null/no-null
const sharedCanvas2Ref = useRef<HTMLCanvasElement>(null);
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(null);
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
const lang = useLang();
const [itemsPerRow, setItemsPerRow] = useState(ITEMS_PER_ROW_FALLBACK);
useOnIntersect(ref, observeIntersection);
const transitionClassNames = useMediaTransition(shouldRender);
@ -110,21 +119,48 @@ const StickerSet: FC<OwnProps> = ({
}
}, [isCurrentUserPremium, isPremiumSet, openPremiumModal, stickerSet, toggleStickerSet]);
const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
const handleDefaultTopicIconClick = useCallback(() => {
onStickerSelect?.({
id: DEFAULT_TOPIC_ICON_STICKER_ID,
isLottie: false,
isVideo: false,
stickerSetInfo: {
shortName: 'dummy',
},
} satisfies ApiSticker);
}, [onStickerSelect]);
const itemSize = isEmoji ? EMOJI_SIZE_PICKER : STICKER_SIZE_PICKER;
const itemsPerRow = isEmoji ? EMOJI_PER_ROW_ON_DESKTOP : STICKERS_PER_ROW_ON_DESKTOP;
const margin = isEmoji ? EMOJI_MARGIN : STICKER_MARGIN;
const stickersPerRow = IS_SINGLE_COLUMN_LAYOUT
? Math.floor((windowSize.get().width - MOBILE_CONTAINER_PADDING) / (itemSize + margin))
: itemsPerRow;
const calculateItemsPerRow = useCallback((width: number) => {
if (!width) return ITEMS_PER_ROW_FALLBACK;
return Math.floor((width - CONTAINER_PADDING) / (itemSize + margin));
}, [itemSize, margin]);
const handleResize = useCallback((entry: ResizeObserverEntry) => {
setItemsPerRow(calculateItemsPerRow(entry.contentRect.width));
}, [calculateItemsPerRow]);
useResizeObserver(ref, handleResize);
useLayoutEffect(() => {
if (!ref.current) return;
setItemsPerRow(calculateItemsPerRow(ref.current.clientWidth));
}, [calculateItemsPerRow]);
const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
const canCut = !stickerSet.installedDate && stickerSet.id !== RECENT_SYMBOL_SET_ID;
const [isCut, , expand] = useFlag(canCut);
const itemsBeforeCutout = stickersPerRow * 3 - 1;
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, stickerSet.count) / stickersPerRow) * (itemSize + margin);
const height = isCut ? heightWhenCut : Math.ceil(stickerSet.count / stickersPerRow) * (itemSize + margin);
const itemsBeforeCutout = itemsPerRow * 3 - 1;
const totalItemsCount = withDefaultTopicIcon ? stickerSet.count + 1 : stickerSet.count;
const heightWhenCut = Math.ceil(Math.min(itemsBeforeCutout, totalItemsCount) / itemsPerRow) * (itemSize + margin);
const height = isCut ? heightWhenCut : Math.ceil(totalItemsCount / itemsPerRow) * (itemSize + margin);
const shouldHideHeader = isRecent && shouldHideRecentHeader;
const favoriteStickerIdsSet = useMemo(() => (
favoriteStickers ? new Set(favoriteStickers.map(({ id }) => id)) : undefined
@ -139,27 +175,29 @@ const StickerSet: FC<OwnProps> = ({
buildClassName('symbol-set', isLocked && 'symbol-set-locked')
}
>
<div className="symbol-set-header">
<p className="symbol-set-name">
{isLocked && <i className="symbol-set-locked-icon icon-lock-badge" />}
{stickerSet.title}
</p>
{isRecent && (
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
)}
{!isRecent && isEmoji && !stickerSet.installedDate && (
<Button
className="symbol-set-add-button"
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
onClick={handleAddClick}
pill
size="tiny"
fluid
>
{isPremiumSet && isLocked ? lang('Unlock') : lang('Add')}
</Button>
)}
</div>
{!shouldHideHeader && (
<div className="symbol-set-header">
<p className="symbol-set-name">
{isLocked && <i className="symbol-set-locked-icon icon-lock-badge" />}
{stickerSet.title}
</p>
{isRecent && (
<i className="symbol-set-remove icon-close" onClick={openConfirmModal} />
)}
{!isRecent && isEmoji && !stickerSet.installedDate && (
<Button
className="symbol-set-add-button"
withPremiumGradient={isPremiumSet && !isCurrentUserPremium}
onClick={handleAddClick}
pill
size="tiny"
fluid
>
{isPremiumSet && isLocked ? lang('Unlock') : lang('Add')}
</Button>
)}
</div>
)}
<div
className={buildClassName('symbol-set-container shared-canvas-container', transitionClassNames)}
style={`height: ${height}px;`}
@ -169,14 +207,24 @@ const StickerSet: FC<OwnProps> = ({
className="shared-canvas"
style={canCut ? `height: ${heightWhenCut}px;` : undefined}
/>
{(isRecent || isFavorite || canCut) && <canvas ref={sharedCanvas2Ref} className="shared-canvas" />}
{(isRecent || isFavorite || canCut) && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />}
{withDefaultTopicIcon && (
<Button
className="StickerButton custom-emoji"
color="translucent"
onClick={handleDefaultTopicIconClick}
key="default-topic-icon"
>
<img src={grey} alt="Reset" />
</Button>
)}
{shouldRender && stickerSet.stickers && stickerSet.stickers
.slice(0, isCut ? itemsBeforeCutout : stickerSet.stickers.length)
.map((sticker, i) => {
const isHqEmoji = (isRecent || isFavorite)
&& selectIsAlwaysHighPriorityEmoji(getGlobal(), sticker.stickerSetInfo);
const canvasRef = (canCut && i >= itemsBeforeCutout) || isHqEmoji
? sharedCanvas2Ref
? sharedCanvasHqRef
: sharedCanvasRef;
return (
@ -198,9 +246,15 @@ const StickerSet: FC<OwnProps> = ({
/>
);
})}
{isCut && stickerSet.count > itemsBeforeCutout && (
<Button className="StickerButton custom-emoji set-expand" round color="translucent" onClick={expand}>
+{stickerSet.count - itemsBeforeCutout}
{isCut && totalItemsCount > itemsBeforeCutout && (
<Button
className="StickerButton custom-emoji set-expand"
round
color="translucent"
onClick={expand}
key="more"
>
+{totalItemsCount - itemsBeforeCutout}
</Button>
)}
</div>

View File

@ -0,0 +1,16 @@
import React, { memo } from '../../lib/teact/teact';
import type { FC } from '../../lib/teact/teact';
import type { OwnProps } from './CreateTopic';
import { Bundles } from '../../util/moduleLoader';
import useModuleLoader from '../../hooks/useModuleLoader';
import Loading from '../ui/Loading';
const CreateTopicAsync: FC<OwnProps> = (props) => {
const CreateTopic = useModuleLoader(Bundles.Extra, 'CreateTopic');
// eslint-disable-next-line react/jsx-props-no-spreading
return CreateTopic ? <CreateTopic {...props} /> : <Loading />;
};
export default memo(CreateTopicAsync);

View File

@ -4,10 +4,11 @@ import React, {
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiChat } from '../../api/types';
import type { ApiChat, ApiSticker } from '../../api/types';
import type { GlobalState } from '../../global/types';
import { selectChat } from '../../global/selectors';
import { DEFAULT_TOPIC_ICON_STICKER_ID } from '../../config';
import { selectChat, selectIsCurrentUserPremium } from '../../global/selectors';
import { getTopicColors } from '../../util/forumColors';
import cycleRestrict from '../../util/cycleRestrict';
import buildClassName from '../../util/buildClassName';
@ -20,12 +21,14 @@ import TopicIcon from '../common/TopicIcon';
import InputText from '../ui/InputText';
import FloatingActionButton from '../ui/FloatingActionButton';
import Spinner from '../ui/Spinner';
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
import Transition from '../ui/Transition';
import styles from './ManageTopic.module.scss';
const ICON_SIZE = 5 * REM;
type OwnProps = {
export type OwnProps = {
isActive: boolean;
onClose: NoneToVoidFunction;
};
@ -33,17 +36,20 @@ type OwnProps = {
type StateProps = {
chat?: ApiChat;
createTopicPanel?: GlobalState['createTopicPanel'];
isCurrentUserPremium?: boolean;
};
const CreateTopic: FC<OwnProps & StateProps> = ({
isActive,
chat,
createTopicPanel,
isCurrentUserPremium,
onClose,
}) => {
const { createTopic, closeCreateTopicPanel } = getActions();
const { createTopic, openPremiumModal } = getActions();
const [title, setTitle] = useState('');
const [iconColorIndex, setIconColorIndex] = useState(0);
const [iconEmojiId, setIconEmojiId] = useState<string | undefined>(undefined);
const lang = useLang();
const isTouched = Boolean(title);
@ -67,17 +73,32 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
chatId: chat!.id,
title,
iconColor: getTopicColors()[iconColorIndex],
iconEmojiId,
});
closeCreateTopicPanel();
}, [chat, closeCreateTopicPanel, createTopic, iconColorIndex, title]);
}, [chat, createTopic, iconColorIndex, iconEmojiId, title]);
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
if (!emoji.isFree && !isCurrentUserPremium) {
openPremiumModal({ initialSection: 'animated_emoji' });
return;
}
if (emoji.id === DEFAULT_TOPIC_ICON_STICKER_ID) {
setIconEmojiId(undefined);
return;
}
setIconEmojiId(emoji.id);
}, [isCurrentUserPremium, openPremiumModal]);
const dummyTopic = useMemo(() => {
return {
id: 0,
title,
iconColor: getTopicColors()[iconColorIndex],
iconEmojiId,
};
}, [iconColorIndex, title]);
}, [iconColorIndex, iconEmojiId, title]);
if (!chat?.isForum) {
return undefined;
@ -85,15 +106,24 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
return (
<div className={styles.root}>
<div className="custom-scroll">
<div className={buildClassName(styles.top, 'section')}>
<div className={buildClassName(styles.content, 'custom-scroll')}>
<div className={buildClassName(styles.section, styles.top)}>
<span className={styles.heading}>{lang('CreateTopicTitle')}</span>
<TopicIcon
topic={dummyTopic}
className={buildClassName(styles.icon, styles.clickable)}
onClick={handleIconClick}
size={ICON_SIZE}
/>
<Transition
name="zoom-fade"
activeKey={Number(dummyTopic.iconEmojiId) || 0}
shouldCleanup
direction={1}
className={styles.iconWrapper}
>
<TopicIcon
topic={dummyTopic}
className={buildClassName(styles.icon, styles.clickable)}
onClick={handleIconClick}
size={ICON_SIZE}
noLoopLimit
/>
</Transition>
<InputText
value={title}
onChange={handleTitleChange}
@ -102,6 +132,14 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
teactExperimentControlled
/>
</div>
<div className={buildClassName(styles.section, styles.bottom)}>
<CustomEmojiPicker
loadAndPlay={isActive}
onCustomEmojiSelect={handleCustomEmojiSelect}
className={styles.iconPicker}
withDefaultTopicIcons
/>
</div>
</div>
<FloatingActionButton
isShown={isTouched}
@ -125,6 +163,7 @@ export default memo(withGlobal(
return {
chat: createTopicPanel?.chatId ? selectChat(global, createTopicPanel.chatId) : undefined,
createTopicPanel,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
},
)(CreateTopic));

View File

@ -0,0 +1,16 @@
import React, { memo } from '../../lib/teact/teact';
import type { FC } from '../../lib/teact/teact';
import type { OwnProps } from './EditTopic';
import { Bundles } from '../../util/moduleLoader';
import useModuleLoader from '../../hooks/useModuleLoader';
import Loading from '../ui/Loading';
const EditTopicAsync: FC<OwnProps> = (props) => {
const EditTopic = useModuleLoader(Bundles.Extra, 'EditTopic');
// eslint-disable-next-line react/jsx-props-no-spreading
return EditTopic ? <EditTopic {...props} /> : <Loading />;
};
export default memo(EditTopicAsync);

View File

@ -4,10 +4,11 @@ import React, {
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiChat, ApiTopic } from '../../api/types';
import type { ApiChat, ApiSticker, ApiTopic } from '../../api/types';
import type { GlobalState } from '../../global/types';
import { selectChat } from '../../global/selectors';
import { DEFAULT_TOPIC_ICON_STICKER_ID, GENERAL_TOPIC_ID } from '../../config';
import { selectChat, selectIsCurrentUserPremium } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { REM } from '../common/helpers/mediaDimensions';
@ -19,12 +20,14 @@ import InputText from '../ui/InputText';
import FloatingActionButton from '../ui/FloatingActionButton';
import Spinner from '../ui/Spinner';
import Loading from '../ui/Loading';
import CustomEmojiPicker from '../middle/composer/CustomEmojiPicker';
import Transition from '../ui/Transition';
import styles from './ManageTopic.module.scss';
const ICON_SIZE = 5 * REM;
type OwnProps = {
export type OwnProps = {
isActive: boolean;
onClose: NoneToVoidFunction;
};
@ -33,6 +36,7 @@ type StateProps = {
chat?: ApiChat;
topic?: ApiTopic;
editTopicPanel?: GlobalState['editTopicPanel'];
isCurrentUserPremium?: boolean;
};
const EditTopic: FC<OwnProps & StateProps> = ({
@ -40,14 +44,16 @@ const EditTopic: FC<OwnProps & StateProps> = ({
chat,
topic,
editTopicPanel,
isCurrentUserPremium,
onClose,
}) => {
const { editTopic, closeEditTopicPanel } = getActions();
const { editTopic, openPremiumModal } = getActions();
const [title, setTitle] = useState('');
const [isTouched, setIsTouched] = useState(false);
const [iconEmojiId, setIconEmojiId] = useState<string | undefined>(undefined);
const lang = useLang();
const isLoading = Boolean(editTopicPanel?.isLoading);
const isGeneral = topic?.id === GENERAL_TOPIC_ID;
useHistoryBack({
isActive,
@ -55,33 +61,51 @@ const EditTopic: FC<OwnProps & StateProps> = ({
});
useEffect(() => {
if (topic?.title) {
if (topic?.title || topic?.iconEmojiId) {
setTitle(topic.title);
setIsTouched(false);
setIconEmojiId(topic.iconEmojiId);
}
}, [topic?.title]);
}, [topic]);
const isTouched = useMemo(() => {
return title !== topic?.title || iconEmojiId !== topic?.iconEmojiId;
}, [iconEmojiId, title, topic?.iconEmojiId, topic?.title]);
const handleTitleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const newTitle = e.target.value;
setTitle(newTitle);
setIsTouched(newTitle !== topic?.title);
}, [topic?.title]);
}, []);
const handleEditTopic = useCallback(() => {
editTopic({
chatId: chat!.id,
title,
topicId: topic!.id,
iconEmojiId,
});
closeEditTopicPanel();
}, [chat, closeEditTopicPanel, editTopic, title, topic]);
}, [chat, editTopic, iconEmojiId, title, topic]);
const handleCustomEmojiSelect = useCallback((emoji: ApiSticker) => {
if (!emoji.isFree && !isCurrentUserPremium) {
openPremiumModal({ initialSection: 'animated_emoji' });
return;
}
if (emoji.id === DEFAULT_TOPIC_ICON_STICKER_ID) {
setIconEmojiId(undefined);
return;
}
setIconEmojiId(emoji.id);
}, [isCurrentUserPremium, openPremiumModal]);
const dummyTopic = useMemo(() => {
return {
...topic!,
title,
iconEmojiId,
};
}, [title, topic]);
}, [iconEmojiId, title, topic]);
if (!chat?.isForum) {
return undefined;
@ -89,24 +113,45 @@ const EditTopic: FC<OwnProps & StateProps> = ({
return (
<div className={styles.root}>
<div className="custom-scroll">
<div className={buildClassName(styles.content, 'custom-scroll')}>
{!topic && <Loading />}
{topic && (
<div className={buildClassName(styles.top, 'section')}>
<span className={styles.heading}>{lang('CreateTopicTitle')}</span>
<TopicIcon
topic={dummyTopic}
className={styles.icon}
size={ICON_SIZE}
/>
<InputText
value={title}
onChange={handleTitleChange}
label={lang('lng_forum_topic_title')}
disabled={isLoading}
teactExperimentControlled
/>
</div>
<>
<div className={buildClassName(styles.section, styles.top)}>
<span className={styles.heading}>{lang('CreateTopicTitle')}</span>
<Transition
name="zoom-fade"
activeKey={Number(dummyTopic.iconEmojiId) || 0}
shouldCleanup
direction={1}
className={styles.iconWrapper}
>
<TopicIcon
topic={dummyTopic}
className={styles.icon}
size={ICON_SIZE}
noLoopLimit
/>
</Transition>
<InputText
value={title}
onChange={handleTitleChange}
label={lang('lng_forum_topic_title')}
disabled={isLoading}
teactExperimentControlled
/>
</div>
{!isGeneral && (
<div className={buildClassName(styles.section, styles.bottom)}>
<CustomEmojiPicker
loadAndPlay={isActive}
onCustomEmojiSelect={handleCustomEmojiSelect}
className={styles.iconPicker}
withDefaultTopicIcons
/>
</div>
)}
</>
)}
</div>
<FloatingActionButton
@ -134,6 +179,7 @@ export default memo(withGlobal(
chat,
topic,
editTopicPanel,
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
},
)(EditTopic));

View File

@ -2,26 +2,48 @@
position: relative;
height: 100%;
background-color: var(--color-background-secondary);
--topic-icon-size: 5rem;
}
.top {
.content {
display: flex;
flex-direction: column;
height: 100%;
}
.section {
display: flex;
justify-content: center;
flex-direction: column;
padding: 1rem 1.5rem;
background-color: var(--color-background);
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
margin-bottom: 0.625rem;
}
.icon {
--custom-emoji-size: 5rem;
width: 5rem;
height: 5rem;
font-size: 3rem;
align-self: center;
.top {
padding: 1rem 1.5rem;
}
.bottom {
flex-grow: 1;
min-height: 0;
margin-bottom: 0;
}
.iconWrapper {
margin: 1.5rem 0;
align-self: center;
width: var(--topic-icon-size);
height: var(--topic-icon-size);
--custom-emoji-size: var(--topic-icon-size);
font-size: calc(var(--topic-icon-size) - 2rem);
}
.icon {
width: 100%;
height: 100%;
}
.clickable {
@ -33,3 +55,7 @@
font-size: 0.9375rem;
color: var(--color-text-secondary);
}
.icon-picker {
min-height: 10rem;
}

View File

@ -33,8 +33,8 @@ import StickerSearch from './StickerSearch.async';
import GifSearch from './GifSearch.async';
import PollResults from './PollResults.async';
import AddChatMembers from './AddChatMembers';
import CreateTopic from './CreateTopic';
import EditTopic from './EditTopic';
import CreateTopic from './CreateTopic.async';
import EditTopic from './EditTopic.async';
import './RightColumn.scss';

View File

@ -171,6 +171,7 @@ export const RECENT_SYMBOL_SET_ID = 'recent';
export const FAVORITE_SYMBOL_SET_ID = 'favorite';
export const CHAT_STICKER_SET_ID = 'chatStickers';
export const PREMIUM_STICKER_SET_ID = 'premium';
export const DEFAULT_TOPIC_ICON_STICKER_ID = 'topic-default-icon';
export const EMOJI_IMG_REGEX = /<img[^>]+alt="([^"]+)"(?![^>]*data-document-id)[^>]*>/gm;
export const BASE_EMOJI_KEYWORD_LANG = 'en';

View File

@ -1373,13 +1373,16 @@ addActionHandler('loadTopics', async (global, actions, payload) => {
});
addActionHandler('loadTopicById', async (global, actions, payload) => {
const { chatId, topicId } = payload;
const { chatId, topicId, shouldCloseChatOnError } = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('fetchTopicById', { chat, topicId });
if (!result) {
if (shouldCloseChatOnError) {
actions.openChat({ id: undefined });
}
return;
}
@ -1413,7 +1416,9 @@ addActionHandler('toggleForum', async (global, actions, payload) => {
});
addActionHandler('createTopic', async (global, actions, payload) => {
const { chatId, title, iconColor } = payload;
const {
chatId, title, iconColor, iconEmojiId,
} = payload;
const chat = selectChat(global, chatId);
if (!chat) return;
@ -1425,10 +1430,13 @@ addActionHandler('createTopic', async (global, actions, payload) => {
},
});
const topicId = await callApi('createTopic', { chat, title, iconColor });
const topicId = await callApi('createTopic', {
chat, title, iconColor, iconEmojiId,
});
if (topicId) {
actions.openChat({ id: chatId, threadId: topicId, shouldReplaceHistory: true });
}
actions.closeCreateTopicPanel();
});
addActionHandler('deleteTopic', async (global, actions, payload) => {
@ -1451,12 +1459,23 @@ addActionHandler('editTopic', async (global, actions, payload) => {
const topic = chat?.topics?.[topicId];
if (!chat || !topic) return;
setGlobal({
...global,
editTopicPanel: {
chatId,
topicId,
isLoading: true,
},
});
const result = await callApi('editTopic', { chat, topicId, ...rest });
if (!result) return;
global = getGlobal();
global = updateTopic(global, chatId, topicId, rest);
setGlobal(global);
actions.closeEditTopicPanel();
});
addActionHandler('toggleTopicPinned', (global, actions, payload) => {

View File

@ -198,6 +198,24 @@ addActionHandler('loadPremiumGifts', async () => {
});
});
addActionHandler('loadDefaultTopicIcons', async (global) => {
const stickerSet = await callApi('fetchDefaultTopicIcons');
if (!stickerSet) {
return;
}
global = getGlobal();
const { set, stickers } = stickerSet;
const fullSet = { ...set, stickers };
global = updateStickerSet(global, fullSet.id, fullSet);
setGlobal({
...global,
defaultTopicIconsId: fullSet.id,
});
});
addActionHandler('loadStickers', (global, actions, payload) => {
const { stickerSetInfo } = payload;
const cachedSet = selectStickerSet(global, stickerSetInfo);

View File

@ -104,9 +104,6 @@ async function loadAndReplaceMessages() {
const currentChat = activeCurrentChatId ? global.chats.byId[activeCurrentChatId] : undefined;
if (activeCurrentChatId && currentChat) {
if (currentChat.isForum) {
getActions().loadTopics({ chatId: activeCurrentChatId, force: true });
}
const result = await loadTopMessages(currentChat, activeThreadId, threadInfo?.lastReadInboxMessageId);
global = getGlobal();
const { chatId: newCurrentChatId } = selectCurrentMessageList(global) || {};
@ -162,6 +159,15 @@ async function loadAndReplaceMessages() {
setGlobal(global);
if (currentChat?.isForum) {
getActions().loadTopics({ chatId: activeCurrentChatId!, force: true });
if (currentThreadId && currentThreadId !== MAIN_THREAD_ID) {
getActions().loadTopicById({
chatId: activeCurrentChatId!, topicId: currentThreadId, shouldCloseChatOnError: true,
});
}
}
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
if (audioChatId && audioMessageId && !selectChatMessage(global, audioChatId, audioMessageId)) {
getActions().closeAudioPlayer();

View File

@ -23,6 +23,7 @@ import {
deleteChatScheduledMessages,
updateThreadUnreadFromForwardedMessage,
updateTopic,
deleteTopic,
} from '../../reducers';
import {
selectChatMessage,
@ -376,7 +377,7 @@ addActionHandler('apiUpdate', (global, actions, update) => {
case 'deleteMessages': {
const { ids, chatId } = update;
deleteMessages(chatId, ids, actions, global);
deleteMessages(global, chatId, ids, actions);
break;
}
@ -402,7 +403,7 @@ addActionHandler('apiUpdate', (global, actions, update) => {
if (chatMessages) {
const ids = Object.keys(chatMessages.byId).map(Number);
deleteMessages(chatId, ids, actions, getGlobal());
deleteMessages(global, chatId, ids, actions);
} else {
actions.requestChatUpdate({ chatId });
}
@ -734,7 +735,7 @@ function updateListedAndViewportIds(global: GlobalState, actions: GlobalActions,
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', {
...threadInfo,
lastMessageId: message.id,
messagesCount: threadInfo.messagesCount + 1,
messagesCount: (threadInfo.messagesCount || 0) + 1,
});
}
@ -808,10 +809,13 @@ function findLastMessage(global: GlobalState, chatId: string) {
return undefined;
}
function deleteMessages(chatId: string | undefined, ids: number[], actions: GlobalActions, global: GlobalState) {
function deleteMessages(global: GlobalState, chatId: string | undefined, ids: number[], actions: GlobalActions) {
// Channel update
if (chatId) {
const chat = selectChat(global, chatId);
if (!chat) return;
ids.forEach((id) => {
global = updateChatMessage(global, chatId, id, {
isDeleting: true,
@ -821,6 +825,10 @@ function deleteMessages(chatId: string | undefined, ids: number[], actions: Glob
if (newLastMessage) {
global = updateChatLastMessage(global, chatId, newLastMessage, true);
}
if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id);
}
});
actions.requestChatUpdate({ chatId });

View File

@ -5,13 +5,13 @@ import { MAIN_THREAD_ID } from '../../../api/types';
import {
exitMessageSelectMode, replaceThreadParam, updateCurrentMessageList,
} from '../../reducers';
import { selectCurrentMessageList } from '../../selectors';
import { selectChat, selectCurrentMessageList } from '../../selectors';
import { closeLocalTextSearch } from './localSearch';
addActionHandler('openChat', (global, actions, payload) => {
const {
id,
threadId = MAIN_THREAD_ID,
threadId,
type = 'thread',
shouldReplaceHistory = false,
} = payload;
@ -35,7 +35,7 @@ addActionHandler('openChat', (global, actions, payload) => {
|| currentMessageList.type !== type
)) {
if (id) {
global = replaceThreadParam(global, id, threadId, 'replyStack', []);
global = replaceThreadParam(global, id, threadId || MAIN_THREAD_ID, 'replyStack', []);
}
global = exitMessageSelectMode(global);
@ -54,10 +54,16 @@ addActionHandler('openChat', (global, actions, payload) => {
};
}
if (id !== global.forumPanelChatId) {
if (id && id !== global.forumPanelChatId) {
actions.closeForumPanel();
}
if (id && !threadId) {
const chat = selectChat(global, id);
// Prevent chat opening on forum click
if (chat?.isForum) return global;
}
return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory);
});

View File

@ -1,11 +1,14 @@
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import type { ApiError, ApiNotification } from '../../../api/types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { APP_VERSION, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT } from '../../../config';
import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT } from '../../../util/environment';
import getReadableErrorText from '../../../util/getReadableErrorText';
import { selectChatMessage, selectCurrentMessageList, selectIsTrustedBot } from '../../selectors';
import {
selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectIsTrustedBot,
} from '../../selectors';
import generateIdFor from '../../../util/generateIdFor';
import { unique } from '../../../util/iteratees';
@ -443,6 +446,10 @@ addActionHandler('updateLastRenderedCustomEmojis', (global, actions, payload) =>
addActionHandler('openCreateTopicPanel', (global, actions, payload) => {
const { chatId } = payload;
// Topic panel can be opened only if there is a selected chat
const currentChat = selectCurrentChat(global);
if (!currentChat) actions.openChat({ id: chatId, threadId: MAIN_THREAD_ID });
return {
...global,
createTopicPanel: {
@ -461,6 +468,10 @@ addActionHandler('closeCreateTopicPanel', (global) => {
addActionHandler('openEditTopicPanel', (global, actions, payload) => {
const { chatId, topicId } = payload;
// Topic panel can be opened only if there is a selected chat
const currentChat = selectCurrentChat(global);
if (!currentChat) actions.openChat({ id: chatId });
return {
...global,
editTopicPanel: {

View File

@ -250,8 +250,7 @@ export function deleteChatMessages(
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'pinnedIds', mainPinnedIds);
if (threadInfo && newMessageCount !== undefined) {
global = replaceThreadParam(global, chatId, threadId, 'threadInfo', {
...threadInfo,
global = updateThreadInfo(global, chatId, threadId, {
messagesCount: newMessageCount,
});
}
@ -581,7 +580,7 @@ export function updateThreadUnreadFromForwardedMessage(
global = replaceThreadParam(global, chatId, channelPostId, 'threadInfo', {
...threadInfoOld,
lastMessageId,
messagesCount: threadInfoOld.messagesCount + (isDeleting ? -1 : 1),
messagesCount: (threadInfoOld.messagesCount || 0) + (isDeleting ? -1 : 1),
});
}
}

View File

@ -173,6 +173,15 @@ export function selectReplyStack(global: GlobalState, chatId: string, threadId:
return selectThreadParam(global, chatId, threadId, 'replyStack');
}
export function selectThreadMessagesCount(global: GlobalState, chatId: string, threadId: number) {
const chat = selectChat(global, chatId);
const threadInfo = selectThreadInfo(global, chatId, threadId);
if (!chat || !threadInfo || threadInfo.messagesCount === undefined) return undefined;
// In forum topics first message is ignored, but not in General
if (chat.isForum && threadId !== GENERAL_TOPIC_ID) return threadInfo.messagesCount - 1;
return threadInfo.messagesCount;
}
export function selectThreadOriginChat(global: GlobalState, chatId: string, threadId: number) {
if (threadId === MAIN_THREAD_ID) {
return selectChat(global, chatId);

View File

@ -349,6 +349,7 @@ export type GlobalState = {
animatedEmojiEffects?: ApiStickerSet;
genericEmojiEffects?: ApiStickerSet;
premiumGifts?: ApiStickerSet;
defaultTopicIconsId?: string;
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
gifs: {
@ -1305,6 +1306,7 @@ export interface ActionPayloads {
};
loadPremiumGifts: never;
loadDefaultTopicIcons: never;
loadPremiumStickers: {
hash?: string;
};
@ -1335,6 +1337,7 @@ export interface ActionPayloads {
chatId: string;
title: string;
iconColor?: number;
iconEmojiId?: string;
};
loadTopics: {
chatId: string;
@ -1343,6 +1346,7 @@ export interface ActionPayloads {
loadTopicById: {
chatId: string;
topicId: number;
shouldCloseChatOnError?: boolean;
};
deleteTopic: {