Topic management: Display icon picker (#2247)
This commit is contained in:
parent
9e4ec8286c
commit
b4f283d207
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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!),
|
||||
}),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
16
src/components/right/CreateTopic.async.tsx
Normal file
16
src/components/right/CreateTopic.async.tsx
Normal 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);
|
||||
@ -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));
|
||||
|
||||
16
src/components/right/EditTopic.async.tsx
Normal file
16
src/components/right/EditTopic.async.tsx
Normal 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);
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user