Introduce Message Threads (#2546)
This commit is contained in:
parent
6d98d69e17
commit
962d3e321f
@ -235,7 +235,7 @@ export function buildApiMessageWithChatId(
|
||||
}),
|
||||
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons, isHideKeyboardSelective }),
|
||||
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
|
||||
...(replies?.comments && { repliesThreadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
|
||||
...(replies && { repliesThreadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
|
||||
...(postAuthor && { postAuthorTitle: postAuthor }),
|
||||
isProtected,
|
||||
isForwardingAllowed,
|
||||
@ -1591,20 +1591,18 @@ function buildThreadInfo(
|
||||
messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string,
|
||||
): ApiThreadInfo | undefined {
|
||||
const {
|
||||
channelId, replies, maxId, readMaxId, recentRepliers,
|
||||
channelId, replies, maxId, readMaxId, recentRepliers, comments,
|
||||
} = messageReplies;
|
||||
if (!channelId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const apiChannelId = buildApiPeerId(channelId, 'channel');
|
||||
const apiChannelId = channelId ? buildApiPeerId(channelId, 'channel') : undefined;
|
||||
if (apiChannelId === DELETED_COMMENTS_CHANNEL_ID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isPostThread = chatId !== apiChannelId;
|
||||
const isPostThread = apiChannelId && chatId !== apiChannelId;
|
||||
|
||||
return {
|
||||
isComments: comments,
|
||||
threadId: messageId,
|
||||
...(isPostThread ? {
|
||||
chatId: apiChannelId,
|
||||
|
||||
@ -850,9 +850,9 @@ export async function markMessagesRead({
|
||||
}
|
||||
|
||||
export async function requestThreadInfoUpdate({
|
||||
chat, threadId,
|
||||
chat, threadId, originChannelId,
|
||||
}: {
|
||||
chat: ApiChat; threadId: number;
|
||||
chat: ApiChat; threadId: number; originChannelId?: string;
|
||||
}) {
|
||||
if (threadId === MAIN_THREAD_ID) {
|
||||
return undefined;
|
||||
@ -881,15 +881,18 @@ export async function requestThreadInfoUpdate({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const topMessageId = topMessageResult.messages[topMessageResult.messages.length - 1].id;
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateThreadInfo',
|
||||
chatId: discussionChatId,
|
||||
threadId,
|
||||
threadId: topMessageId,
|
||||
threadInfo: {
|
||||
threadId,
|
||||
topMessageId: topMessageResult.messages[topMessageResult.messages.length - 1].id,
|
||||
threadId: topMessageId,
|
||||
topMessageId,
|
||||
lastReadInboxMessageId: topMessageResult.readInboxMaxId,
|
||||
messagesCount: (repliesResult instanceof GramJs.messages.ChannelMessages) ? repliesResult.count : undefined,
|
||||
...(originChannelId ? { originChannelId } : undefined),
|
||||
},
|
||||
firstMessageId: repliesResult && 'messages' in repliesResult && repliesResult.messages.length
|
||||
? repliesResult.messages[0].id
|
||||
@ -920,6 +923,7 @@ export async function requestThreadInfoUpdate({
|
||||
const users = topMessageResult.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
return {
|
||||
topMessageId,
|
||||
discussionChatId,
|
||||
users,
|
||||
};
|
||||
|
||||
@ -603,6 +603,14 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
lastReadInboxMessageId: update.readMaxId,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadChannelDiscussionOutbox) {
|
||||
onUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: buildApiPeerId(update.channelId, 'channel'),
|
||||
chat: {
|
||||
lastReadOutboxMessageId: update.readMaxId,
|
||||
},
|
||||
});
|
||||
} else if (
|
||||
update instanceof GramJs.UpdateDialogPinned
|
||||
&& update.peer instanceof GramJs.DialogPeer
|
||||
|
||||
@ -491,6 +491,7 @@ export type ApiReactionCustomEmoji = {
|
||||
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
|
||||
|
||||
export interface ApiThreadInfo {
|
||||
isComments?: boolean;
|
||||
threadId: number;
|
||||
chatId: string;
|
||||
topMessageId?: number;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -6,6 +6,7 @@
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
margin-inline-end: 0.5rem;
|
||||
flex-grow: 1;
|
||||
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectIsChatWithSelf, selectIsCurrentUserPremium,
|
||||
selectShouldSchedule,
|
||||
selectStickerSet,
|
||||
selectStickerSet, selectThreadInfo,
|
||||
} from '../../global/selectors';
|
||||
import renderText from './helpers/renderText';
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
@ -236,8 +236,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { chatId, threadId } = currentMessageList || {};
|
||||
const chat = chatId && selectChat(global, chatId);
|
||||
const sendOptions = chat ? getAllowedAttachmentOptions(chat) : undefined;
|
||||
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
|
||||
const isComments = Boolean(threadInfo?.originChannelId);
|
||||
const canSendStickers = Boolean(
|
||||
chat && threadId && getCanPostInChat(chat, threadId) && sendOptions?.canSendStickers,
|
||||
chat && threadId && getCanPostInChat(chat, threadId, isComments) && sendOptions?.canSendStickers,
|
||||
);
|
||||
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@ import {
|
||||
selectFirstMessageId,
|
||||
selectChatScheduledMessages,
|
||||
selectCurrentMessageIds,
|
||||
selectIsCurrentUserPremium, selectLastScrollOffset,
|
||||
selectIsCurrentUserPremium, selectLastScrollOffset, selectThreadInfo,
|
||||
} from '../../global/selectors';
|
||||
import {
|
||||
isChatChannel,
|
||||
@ -96,6 +96,7 @@ type StateProps = {
|
||||
messageIds?: number[];
|
||||
messagesById?: Record<number, ApiMessage>;
|
||||
firstUnreadId?: number;
|
||||
isComments?: boolean;
|
||||
isViewportNewest?: boolean;
|
||||
isRestricted?: boolean;
|
||||
restrictionReason?: ApiRestrictionReason;
|
||||
@ -144,6 +145,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
messageIds,
|
||||
messagesById,
|
||||
firstUnreadId,
|
||||
isComments,
|
||||
isViewportNewest,
|
||||
threadFirstMessageId,
|
||||
isRestricted,
|
||||
@ -604,6 +606,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
<MessageListContent
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
chatId={chatId}
|
||||
isComments={isComments}
|
||||
isChannelChat={isChannelChat}
|
||||
messageIds={messageIds || [lastMessage!.id]}
|
||||
messageGroups={messageGroups || groupMessages([lastMessage!])}
|
||||
isViewportNewest={Boolean(isViewportNewest)}
|
||||
@ -645,6 +649,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
? selectChatScheduledMessages(global, chatId)
|
||||
: selectChatMessages(global, chatId);
|
||||
const threadTopMessageId = selectThreadTopMessageId(global, chatId, threadId);
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
|
||||
if (
|
||||
threadId !== MAIN_THREAD_ID && !chat?.isForum
|
||||
@ -687,6 +692,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isBot: Boolean(chatBot),
|
||||
messageIds,
|
||||
messagesById,
|
||||
isComments: Boolean(threadInfo?.originChannelId),
|
||||
firstUnreadId: selectFirstUnreadId(global, chatId, threadId),
|
||||
isViewportNewest: type !== 'thread' || selectIsViewportNewest(global, chatId, threadId),
|
||||
threadFirstMessageId: selectFirstMessageId(global, chatId, threadId),
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { MessageListType } from '../../global/types';
|
||||
|
||||
import { SCHEDULED_WHEN_ONLINE } from '../../config';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { compact } from '../../util/iteratees';
|
||||
import { formatHumanDate } from '../../util/dateFormat';
|
||||
@ -21,8 +23,6 @@ import useMessageObservers from './hooks/useMessageObservers';
|
||||
import Message from './message/Message';
|
||||
import SponsoredMessage from './message/SponsoredMessage';
|
||||
import ActionMessage from './ActionMessage';
|
||||
import { getActions } from '../../global';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
interface OwnProps {
|
||||
isCurrentUserPremium?: boolean;
|
||||
@ -33,6 +33,8 @@ interface OwnProps {
|
||||
isViewportNewest: boolean;
|
||||
isUnread: boolean;
|
||||
withUsers: boolean;
|
||||
isChannelChat: boolean | undefined;
|
||||
isComments?: boolean;
|
||||
noAvatars: boolean;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
anchorIdRef: { current: string | undefined };
|
||||
@ -60,7 +62,9 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
messageGroups,
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
isComments,
|
||||
withUsers,
|
||||
isChannelChat,
|
||||
noAvatars,
|
||||
containerRef,
|
||||
anchorIdRef,
|
||||
@ -190,6 +194,10 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
// Service notifications saved in cache in previous versions may share the same `previousLocalId`
|
||||
const key = isServiceNotificationMessage(message) ? `${message.date}_${originalId}` : originalId;
|
||||
|
||||
const noComments = hasLinkedChat === false || !isChannelChat;
|
||||
|
||||
const isTopicTopMessage = message.id === threadTopMessageId;
|
||||
|
||||
return compact([
|
||||
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
|
||||
<Message
|
||||
@ -200,11 +208,12 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
album={album}
|
||||
noAvatars={noAvatars}
|
||||
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}
|
||||
withAvatar={position.isLastInGroup && withUsers && !isOwn && (!isTopicTopMessage || !isComments)}
|
||||
withSenderName={position.isFirstInGroup && withUsers && !isOwn}
|
||||
threadId={threadId}
|
||||
messageListType={type}
|
||||
noComments={hasLinkedChat === false}
|
||||
noComments={noComments}
|
||||
noReplies={!noComments || threadId !== MAIN_THREAD_ID}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isFirstInGroup={position.isFirstInGroup}
|
||||
isLastInGroup={position.isLastInGroup}
|
||||
|
||||
@ -40,7 +40,7 @@ import {
|
||||
selectIsUserBlocked,
|
||||
selectPinnedIds,
|
||||
selectReplyingToId,
|
||||
selectTheme,
|
||||
selectTheme, selectThreadInfo,
|
||||
} from '../../global/selectors';
|
||||
import {
|
||||
getCanPostInChat,
|
||||
@ -630,7 +630,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
const pinnedIds = selectPinnedIds(global, chatId, threadId);
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
|
||||
const canPost = chat && getCanPostInChat(chat, threadId);
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
const isComments = Boolean(threadInfo?.originChannelId);
|
||||
const canPost = chat && getCanPostInChat(chat, threadId, isComments);
|
||||
const isBotNotStarted = selectIsChatBotNotStarted(global, chatId);
|
||||
const isPinnedMessageList = messageListType === 'pinned';
|
||||
const isScheduledMessageList = messageListType === 'scheduled';
|
||||
|
||||
@ -89,6 +89,7 @@ type StateProps = {
|
||||
isRightColumnShown?: boolean;
|
||||
audioMessage?: ApiMessage;
|
||||
messagesCount?: number;
|
||||
isComments?: boolean;
|
||||
isChatWithSelf?: boolean;
|
||||
lastSyncTime?: number;
|
||||
hasButtonInHeader?: boolean;
|
||||
@ -116,6 +117,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
audioMessage,
|
||||
chat,
|
||||
messagesCount,
|
||||
isComments,
|
||||
isChatWithSelf,
|
||||
lastSyncTime,
|
||||
hasButtonInHeader,
|
||||
@ -340,7 +342,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
{renderBackButton()}
|
||||
<h3>
|
||||
{messagesCount !== undefined ? (
|
||||
messageListType === 'thread' ? (lang('CommentsCount', messagesCount, 'i'))
|
||||
messageListType === 'thread' ? (
|
||||
lang(isComments ? 'CommentsCount' : 'Replies', messagesCount, 'i'))
|
||||
: messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i'))
|
||||
: messageListType === 'scheduled' ? (
|
||||
isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')
|
||||
@ -422,13 +425,15 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
{renderInfo()}
|
||||
</Transition>
|
||||
|
||||
<GroupCallTopPane
|
||||
hasPinnedOffset={
|
||||
(shouldRenderPinnedMessage && Boolean(renderingPinnedMessage))
|
||||
{threadId === MAIN_THREAD_ID && !chat?.isForum && (
|
||||
<GroupCallTopPane
|
||||
hasPinnedOffset={
|
||||
(shouldRenderPinnedMessage && Boolean(renderingPinnedMessage))
|
||||
|| (shouldRenderAudioPlayer && Boolean(renderingAudioMessage))
|
||||
}
|
||||
chatId={chatId}
|
||||
/>
|
||||
}
|
||||
chatId={chatId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldRenderPinnedMessage && renderingPinnedMessage && (
|
||||
<HeaderPinnedMessage
|
||||
@ -540,12 +545,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
const pinnedMessageId = selectThreadTopMessageId(global, chatId, threadId);
|
||||
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
|
||||
const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
|
||||
return {
|
||||
...state,
|
||||
pinnedMessageIds: pinnedMessageId,
|
||||
canUnpin: false,
|
||||
topMessageSender,
|
||||
isComments: Boolean(threadInfo?.originChannelId),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -25,16 +25,16 @@ const CommentButton: FC<OwnProps> = ({
|
||||
threadInfo,
|
||||
disabled,
|
||||
}) => {
|
||||
const { openChat } = getActions();
|
||||
const { openComments } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const {
|
||||
threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds,
|
||||
threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
|
||||
} = threadInfo;
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
openChat({ id: chatId, threadId });
|
||||
}, [openChat, chatId, threadId]);
|
||||
openComments({ id: chatId, threadId, originChannelId });
|
||||
}, [openComments, chatId, threadId, originChannelId]);
|
||||
|
||||
const recentRepliers = useMemo(() => {
|
||||
if (!recentReplierIds?.length) {
|
||||
|
||||
@ -6,7 +6,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { MessageListType } from '../../../global/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, ApiChatReactions, ApiReaction,
|
||||
ApiAvailableReaction, ApiStickerSetInfo, ApiMessage, ApiStickerSet, ApiChatReactions, ApiReaction, ApiThreadInfo,
|
||||
} from '../../../api/types';
|
||||
import type { IAlbum, IAnchorPosition } from '../../../types';
|
||||
|
||||
@ -48,8 +48,10 @@ export type OwnProps = {
|
||||
album?: IAlbum;
|
||||
anchor: IAnchorPosition;
|
||||
messageListType: MessageListType;
|
||||
noReplies?: boolean;
|
||||
onClose: () => void;
|
||||
onCloseAnimationEnd: () => void;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -107,6 +109,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canReschedule,
|
||||
canReply,
|
||||
canPin,
|
||||
repliesThreadInfo,
|
||||
canUnpin,
|
||||
canDelete,
|
||||
canReport,
|
||||
@ -129,11 +132,13 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canRevote,
|
||||
canClosePoll,
|
||||
activeDownloads,
|
||||
noReplies,
|
||||
canShowSeenBy,
|
||||
canScheduleUntilOnline,
|
||||
threadId,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
setReplyingToId,
|
||||
setEditingId,
|
||||
pinMessage,
|
||||
@ -254,6 +259,14 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
}, [setReplyingToId, message.id, closeMenu]);
|
||||
|
||||
const handleOpenThread = useCallback(() => {
|
||||
openChat({
|
||||
id: message.chatId,
|
||||
threadId: message.id,
|
||||
});
|
||||
closeMenu();
|
||||
}, [closeMenu, message.chatId, message.id, openChat]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setEditingId({ messageId: message.id });
|
||||
closeMenu();
|
||||
@ -411,6 +424,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canDelete={canDelete}
|
||||
canReport={canReport}
|
||||
canPin={canPin}
|
||||
repliesThreadInfo={repliesThreadInfo}
|
||||
canUnpin={canUnpin}
|
||||
canEdit={canEdit}
|
||||
canForward={canForward}
|
||||
@ -428,6 +442,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
customEmojiSets={customEmojiSets}
|
||||
isDownloading={isDownloading}
|
||||
seenByRecentUsers={seenByRecentUsers}
|
||||
noReplies={noReplies}
|
||||
onOpenThread={handleOpenThread}
|
||||
onReply={handleReply}
|
||||
onEdit={handleEdit}
|
||||
onPin={handlePin}
|
||||
|
||||
@ -170,6 +170,7 @@ type OwnProps =
|
||||
threadId: number;
|
||||
messageListType: MessageListType;
|
||||
noComments: boolean;
|
||||
noReplies: boolean;
|
||||
appearanceOrder: number;
|
||||
memoFirstUnreadIdRef: { current: number | undefined };
|
||||
}
|
||||
@ -265,6 +266,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
withAvatar,
|
||||
withSenderName,
|
||||
noComments,
|
||||
noReplies,
|
||||
appearanceOrder,
|
||||
isFirstInGroup,
|
||||
isPremium,
|
||||
@ -466,6 +468,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
handleAudioPlay,
|
||||
handleAlbumMediaClick,
|
||||
handleMetaClick,
|
||||
handleOpenThread,
|
||||
handleReadMedia,
|
||||
handleCancelUpload,
|
||||
handleVoteSend,
|
||||
@ -523,7 +526,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
message.hasUnreadMention && 'has-unread-mention',
|
||||
isSelected && 'is-selected',
|
||||
isInSelectMode && 'is-in-selection-mode',
|
||||
isThreadTop && 'is-thread-top',
|
||||
isThreadTop && !withAvatar && 'is-thread-top',
|
||||
Boolean(message.inlineButtons) && 'has-inline-buttons',
|
||||
isSwiped && 'is-swiped',
|
||||
transitionClassNames,
|
||||
@ -546,7 +549,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
isCustomShape,
|
||||
isLastInGroup,
|
||||
asForwarded,
|
||||
hasThread,
|
||||
hasThread: hasThread && !noComments,
|
||||
forceSenderName,
|
||||
hasComments: repliesThreadInfo && repliesThreadInfo.messagesCount > 0,
|
||||
hasActionButton: canForward || canFocus,
|
||||
@ -708,11 +711,14 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
const meta = (
|
||||
<MessageMeta
|
||||
message={message}
|
||||
noReplies={noReplies}
|
||||
repliesThreadInfo={repliesThreadInfo}
|
||||
outgoingStatus={outgoingStatus}
|
||||
signature={signature}
|
||||
withReactionOffset={reactionsPosition === 'inside'}
|
||||
availableReactions={availableReactions}
|
||||
onClick={handleMetaClick}
|
||||
onOpenThread={handleOpenThread}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1200,6 +1206,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
messageListType={messageListType}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
repliesThreadInfo={repliesThreadInfo}
|
||||
noReplies={noReplies}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,14 @@ import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiMessage, ApiReaction, ApiSponsoredMessage, ApiStickerSet, ApiUser,
|
||||
ApiAvailableReaction,
|
||||
ApiChatReactions,
|
||||
ApiMessage,
|
||||
ApiReaction,
|
||||
ApiSponsoredMessage,
|
||||
ApiStickerSet,
|
||||
ApiThreadInfo,
|
||||
ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
@ -39,6 +46,7 @@ type OwnProps = {
|
||||
maxUniqueReactions?: number;
|
||||
canReschedule?: boolean;
|
||||
canReply?: boolean;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
canPin?: boolean;
|
||||
canUnpin?: boolean;
|
||||
canDelete?: boolean;
|
||||
@ -62,9 +70,11 @@ type OwnProps = {
|
||||
isDownloading?: boolean;
|
||||
canShowSeenBy?: boolean;
|
||||
seenByRecentUsers?: ApiUser[];
|
||||
noReplies?: boolean;
|
||||
hasCustomEmoji?: boolean;
|
||||
customEmojiSets?: ApiStickerSet[];
|
||||
onReply?: () => void;
|
||||
onOpenThread?: VoidFunction;
|
||||
onEdit?: () => void;
|
||||
onPin?: () => void;
|
||||
onUnpin?: () => void;
|
||||
@ -110,6 +120,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
canBuyPremium,
|
||||
canReply,
|
||||
canEdit,
|
||||
noReplies,
|
||||
canPin,
|
||||
canUnpin,
|
||||
canDelete,
|
||||
@ -125,6 +136,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
canRevote,
|
||||
canClosePoll,
|
||||
isDownloading,
|
||||
repliesThreadInfo,
|
||||
canShowSeenBy,
|
||||
canShowReactionsCount,
|
||||
canShowReactionList,
|
||||
@ -132,6 +144,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
hasCustomEmoji,
|
||||
customEmojiSets,
|
||||
onReply,
|
||||
onOpenThread,
|
||||
onEdit,
|
||||
onPin,
|
||||
onUnpin,
|
||||
@ -294,6 +307,11 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
<MenuItem icon="schedule" onClick={onReschedule}>{lang('MessageScheduleEditTime')}</MenuItem>
|
||||
)}
|
||||
{canReply && <MenuItem icon="reply" onClick={onReply}>{lang('Reply')}</MenuItem>}
|
||||
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
||||
<MenuItem icon="replies" onClick={onOpenThread}>
|
||||
{lang('Conversation.ContextViewReplies', repliesThreadInfo!.messagesCount, 'i')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canEdit && <MenuItem icon="edit" onClick={onEdit}>{lang('Edit')}</MenuItem>}
|
||||
{canFaveSticker && (
|
||||
<MenuItem icon="favorite" onClick={onFaveSticker}>{lang('AddToFavorites')}</MenuItem>
|
||||
|
||||
@ -16,7 +16,8 @@
|
||||
.message-time,
|
||||
.message-imported,
|
||||
.message-signature,
|
||||
.message-views {
|
||||
.message-views,
|
||||
.message-replies {
|
||||
font-size: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -55,6 +56,12 @@
|
||||
top: -0.0625rem;
|
||||
}
|
||||
|
||||
.icon-reply-filled {
|
||||
margin-left: 0.125rem;
|
||||
margin-right: 0.375rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.has-solid-background & {
|
||||
color: rgba(var(--color-text-meta-rgb), 0.75);
|
||||
background: none;
|
||||
|
||||
@ -2,7 +2,9 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiMessage, ApiMessageOutgoingStatus, ApiThreadInfo,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { formatDateTimeToString, formatTime } from '../../../util/dateFormat';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
@ -13,6 +15,7 @@ import useFlag from '../../../hooks/useFlag';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import MessageOutgoingStatus from '../../common/MessageOutgoingStatus';
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
|
||||
import './MessageMeta.scss';
|
||||
|
||||
@ -22,7 +25,10 @@ type OwnProps = {
|
||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||
signature?: string;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
noReplies?: boolean;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
onClick: (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
|
||||
onOpenThread: () => void;
|
||||
};
|
||||
|
||||
const MessageMeta: FC<OwnProps> = ({
|
||||
@ -30,7 +36,10 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
outgoingStatus,
|
||||
signature,
|
||||
withReactionOffset,
|
||||
repliesThreadInfo,
|
||||
noReplies,
|
||||
onClick,
|
||||
onOpenThread,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
const lang = useLang();
|
||||
@ -44,6 +53,11 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
function handleOpenThread(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
onOpenThread();
|
||||
}
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (!isActivated) return undefined;
|
||||
const createDateTime = formatDateTimeToString(message.date * 1000, lang.code);
|
||||
@ -84,6 +98,14 @@ const MessageMeta: FC<OwnProps> = ({
|
||||
<i className="icon-channelviews" />
|
||||
</>
|
||||
)}
|
||||
{!noReplies && Boolean(repliesThreadInfo?.messagesCount) && (
|
||||
<span onClick={handleOpenThread}>
|
||||
<span className="message-replies">
|
||||
<AnimatedCounter text={formatIntegerCompact(repliesThreadInfo!.messagesCount!)} />
|
||||
</span>
|
||||
<i className="icon-reply-filled" />
|
||||
</span>
|
||||
)}
|
||||
{signature && (
|
||||
<span className="message-signature">{renderText(signature)}</span>
|
||||
)}
|
||||
|
||||
@ -160,6 +160,13 @@ export default function useInnerHandlers(
|
||||
selectMessage(e, groupedId);
|
||||
}, [selectMessage, groupedId]);
|
||||
|
||||
const handleOpenThread = useCallback(() => {
|
||||
openChat({
|
||||
id: message.chatId,
|
||||
threadId: message.id,
|
||||
});
|
||||
}, [message.chatId, message.id, openChat]);
|
||||
|
||||
const handleTopicChipClick = useCallback(() => {
|
||||
if (!messageTopic) return;
|
||||
focusMessage({
|
||||
@ -178,6 +185,7 @@ export default function useInnerHandlers(
|
||||
handleAudioPlay,
|
||||
handleAlbumMediaClick,
|
||||
handleMetaClick: selectWithGroupedId,
|
||||
handleOpenThread,
|
||||
handleReadMedia,
|
||||
handleCancelUpload,
|
||||
handleVoteSend,
|
||||
|
||||
@ -11,7 +11,7 @@ import {
|
||||
selectIsChatWithBot,
|
||||
selectCurrentMessageList,
|
||||
selectCanScheduleUntilOnline,
|
||||
selectIsChatWithSelf,
|
||||
selectIsChatWithSelf, selectThreadInfo,
|
||||
} from '../../global/selectors';
|
||||
import { getAllowedAttachmentOptions, getCanPostInChat } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -155,7 +155,9 @@ export default memo(withGlobal(
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
|
||||
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
|
||||
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId);
|
||||
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
|
||||
const isComments = Boolean(threadInfo?.originChannelId);
|
||||
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId, isComments);
|
||||
|
||||
return {
|
||||
query,
|
||||
|
||||
@ -276,9 +276,9 @@ const ManageUser: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { userId }): StateProps => {
|
||||
const user = selectUser(global, userId);
|
||||
const chat = selectChat(global, userId)!;
|
||||
const chat = selectChat(global, userId);
|
||||
const { progress } = selectTabState(global).management;
|
||||
const isMuted = selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
||||
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
||||
|
||||
return {
|
||||
user, progress, isMuted,
|
||||
|
||||
@ -55,7 +55,7 @@ import {
|
||||
selectChatFolder, selectSupportChat, selectChatByUsername,
|
||||
selectCurrentMessageList, selectThreadInfo, selectCurrentChat, selectLastServiceNotification,
|
||||
selectVisibleUsers, selectUserByPhoneNumber, selectDraft, selectThreadTopMessageId,
|
||||
selectTabState, selectThread,
|
||||
selectTabState, selectThread, selectThreadOriginChat,
|
||||
} from '../../selectors';
|
||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||
import { debounce, pause, throttle } from '../../../util/schedulers';
|
||||
@ -142,10 +142,38 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||
actions.requestChatUpdate({ chatId: id });
|
||||
}
|
||||
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
actions.requestThreadInfoUpdate({ chatId: id, threadId });
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('openComments', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
id, threadId, originChannelId, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
const topMessageId = selectThreadTopMessageId(global, id, threadId);
|
||||
if (!topMessageId) {
|
||||
actions.requestThreadInfoUpdate({ chatId: id, threadId });
|
||||
const chat = selectThreadOriginChat(global, id, threadId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.openChat({ id: TMP_CHAT_ID, tabId });
|
||||
|
||||
const result = await callApi('requestThreadInfoUpdate', { chat, threadId, originChannelId });
|
||||
if (!result) {
|
||||
actions.openPreviousChat({ tabId });
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
setGlobal(global);
|
||||
|
||||
actions.openChat({ id, threadId: result.topMessageId, tabId });
|
||||
} else {
|
||||
actions.openChat({ id, threadId: topMessageId, tabId });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -68,7 +68,6 @@ import {
|
||||
selectReplyingToId,
|
||||
selectEditingId,
|
||||
selectDraft,
|
||||
selectThreadOriginChat,
|
||||
selectThreadTopMessageId,
|
||||
selectEditingScheduledId,
|
||||
selectEditingMessage,
|
||||
@ -239,12 +238,17 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
|
||||
}
|
||||
|
||||
const chat = selectChat(global, chatId)!;
|
||||
const replyingToTopId = chat.isForum ? selectThreadTopMessageId(global, chatId, threadId) : undefined;
|
||||
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
||||
const replyingToMessage = replyingToId ? selectChatMessage(global, chatId, replyingToId) : undefined;
|
||||
|
||||
const replyingToTopId = chat.isForum
|
||||
? selectThreadTopMessageId(global, chatId, threadId)
|
||||
: replyingToMessage?.replyToTopMessageId || replyingToMessage?.replyToMessageId;
|
||||
|
||||
const params = {
|
||||
...payload,
|
||||
chat,
|
||||
replyingTo: selectReplyingToId(global, chatId, threadId),
|
||||
replyingTo: replyingToId,
|
||||
replyingToTopId,
|
||||
noWebPage: selectNoWebPage(global, chatId, threadId),
|
||||
sendAs: selectSendAs(global, chatId),
|
||||
@ -549,7 +553,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
|
||||
}
|
||||
|
||||
const { chatId, threadId } = currentMessageList;
|
||||
const chat = selectThreadOriginChat(global, chatId, threadId);
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
@ -853,7 +857,7 @@ addActionHandler('rescheduleMessage', (global, actions, payload): ActionReturnTy
|
||||
|
||||
addActionHandler('requestThreadInfoUpdate', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, threadId } = payload;
|
||||
const chat = selectThreadOriginChat(global, chatId, threadId);
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
@ -939,7 +943,7 @@ async function loadViewportMessages<T extends GlobalState>(
|
||||
|
||||
global = getGlobal();
|
||||
const result = await callApi('fetchMessages', {
|
||||
chat: selectThreadOriginChat(global, chatId, threadId)!,
|
||||
chat: selectChat(global, chatId)!,
|
||||
offsetId,
|
||||
addOffset,
|
||||
limit: MESSAGE_LIST_SLICE,
|
||||
|
||||
@ -105,8 +105,6 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
const activeThreadId = currentThreadId || MAIN_THREAD_ID;
|
||||
const threadInfo = currentThreadId && currentChatId
|
||||
? selectThreadInfo(global, currentChatId, currentThreadId) : undefined;
|
||||
// TODO Fix comments chat id, or refetch chat thread here
|
||||
const activeCurrentChatId = threadInfo?.originChannelId || currentChatId;
|
||||
// Memoize drafts
|
||||
const draftChatIds = Object.keys(global.messages.byChatId);
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
@ -119,14 +117,14 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const currentChat = activeCurrentChatId ? global.chats.byId[activeCurrentChatId] : undefined;
|
||||
if (activeCurrentChatId && currentChat) {
|
||||
const currentChat = currentChatId ? global.chats.byId[currentChatId] : undefined;
|
||||
if (currentChatId && currentChat) {
|
||||
const result = await loadTopMessages(currentChat, activeThreadId, threadInfo?.lastReadInboxMessageId);
|
||||
global = getGlobal();
|
||||
const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {};
|
||||
|
||||
if (result && newCurrentChatId === currentChatId) {
|
||||
const currentChatMessages = selectChatMessages(global, activeCurrentChatId);
|
||||
const currentChatMessages = selectChatMessages(global, currentChatId);
|
||||
const localMessages = currentChatId === SERVICE_NOTIFICATIONS_USER_ID
|
||||
? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message)
|
||||
: [];
|
||||
@ -158,18 +156,20 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
wasReset = true;
|
||||
}
|
||||
|
||||
global = addChatMessagesById(global, activeCurrentChatId, byId);
|
||||
global = updateListedIds(global, activeCurrentChatId, activeThreadId, listedIds);
|
||||
global = addChatMessagesById(global, currentChatId, byId);
|
||||
global = updateListedIds(global, currentChatId, activeThreadId, listedIds);
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
||||
const { chatId: otherChatId, threadId: otherThreadId } = selectCurrentMessageList(global, otherTabId) || {};
|
||||
if (otherChatId === activeCurrentChatId && otherThreadId === activeThreadId) {
|
||||
global = safeReplaceViewportIds(global, activeCurrentChatId, activeThreadId, listedIds, otherTabId);
|
||||
if (otherChatId === currentChatId && otherThreadId === activeThreadId) {
|
||||
global = safeReplaceViewportIds(global, currentChatId, activeThreadId, listedIds, otherTabId);
|
||||
}
|
||||
});
|
||||
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = updateThreadInfos(global, activeCurrentChatId, result.repliesThreadInfos);
|
||||
if (result.repliesThreadInfos.length) {
|
||||
global = updateThreadInfos(global, currentChatId, result.repliesThreadInfos);
|
||||
}
|
||||
|
||||
areMessagesLoaded = true;
|
||||
}
|
||||
@ -184,10 +184,10 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
setGlobal(global);
|
||||
|
||||
if (currentChat?.isForum) {
|
||||
actions.loadTopics({ chatId: activeCurrentChatId!, force: true });
|
||||
actions.loadTopics({ chatId: currentChatId!, force: true });
|
||||
if (currentThreadId && currentThreadId !== MAIN_THREAD_ID) {
|
||||
actions.loadTopicById({
|
||||
chatId: activeCurrentChatId!, topicId: currentThreadId, shouldCloseChatOnError: true,
|
||||
chatId: currentChatId!, topicId: currentThreadId, shouldCloseChatOnError: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
|
||||
import type { ApiUpdateChat } from '../../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
||||
import { buildCollectionByKey, pick } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey, omit, pick } from '../../../util/iteratees';
|
||||
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
||||
import {
|
||||
updateChat,
|
||||
@ -28,7 +29,15 @@ const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
|
||||
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
switch (update['@type']) {
|
||||
case 'updateChat': {
|
||||
const { isForum: prevIsForum } = selectChat(global, update.id) || {};
|
||||
const { isForum: prevIsForum, lastReadOutboxMessageId } = selectChat(global, update.id) || {};
|
||||
|
||||
if (update.chat.lastReadOutboxMessageId && lastReadOutboxMessageId
|
||||
&& update.chat.lastReadOutboxMessageId < lastReadOutboxMessageId) {
|
||||
update = {
|
||||
...update,
|
||||
chat: omit(update.chat, ['lastReadInboxMessageId']),
|
||||
};
|
||||
}
|
||||
|
||||
global = updateChat(global, update.id, update.chat, update.newProfilePhoto);
|
||||
setGlobal(global);
|
||||
@ -47,8 +56,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
const { chatId: currentChatId } = selectCurrentMessageList(global, tabId) || {};
|
||||
const chatUpdate = update as ApiUpdateChat;
|
||||
// The property `isForum` was changed in another client
|
||||
if (currentChatId === update.id && 'isForum' in update.chat && prevIsForum !== update.chat.isForum) {
|
||||
if (currentChatId === chatUpdate.id
|
||||
&& 'isForum' in chatUpdate.chat && prevIsForum !== chatUpdate.chat.isForum) {
|
||||
if (prevIsForum) {
|
||||
actions.closeForumPanel({ tabId });
|
||||
}
|
||||
|
||||
@ -108,7 +108,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
const { threadInfo } = selectThreadByMessage(global, message as ApiMessage) || {};
|
||||
if (threadInfo) {
|
||||
if (threadInfo && !isLocal) {
|
||||
actions.requestThreadInfoUpdate({ chatId, threadId: threadInfo.threadId });
|
||||
}
|
||||
|
||||
@ -362,6 +362,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.loadTopicById({ chatId, topicId: threadId });
|
||||
}
|
||||
|
||||
// Update reply thread last read message id if already read in main thread
|
||||
if (threadInfo.topMessageId === threadId && !chat?.isForum) {
|
||||
const lastReadInboxMessageId = chat?.lastReadInboxMessageId;
|
||||
const lastReadInboxMessageIdInThread = newThreadInfo.lastReadInboxMessageId || lastReadInboxMessageId;
|
||||
if (lastReadInboxMessageId && lastReadInboxMessageIdInThread) {
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
lastReadInboxMessageId: Math.max(lastReadInboxMessageIdInThread, lastReadInboxMessageId),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
break;
|
||||
@ -758,8 +769,13 @@ function updateListedAndViewportIds<T extends GlobalState>(
|
||||
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', {
|
||||
...threadInfo,
|
||||
lastMessageId: message.id,
|
||||
messagesCount: (threadInfo.messagesCount || 0) + 1,
|
||||
});
|
||||
|
||||
if (!isMessageLocal(message)) {
|
||||
global = updateThreadInfo(global, chatId, threadInfo.threadId, {
|
||||
messagesCount: (threadInfo.messagesCount || 0) + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (isUnreadChatNotLoaded) {
|
||||
|
||||
@ -3,7 +3,7 @@ import { addCallback, removeCallback } from '../lib/teact/teactn';
|
||||
|
||||
import { addActionHandler, getGlobal } from './index';
|
||||
|
||||
import type { ActionReturnType, GlobalState } from './types';
|
||||
import type { ActionReturnType, GlobalState, MessageList } from './types';
|
||||
import type { ApiChat, ApiUser } from '../api/types';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
|
||||
@ -27,7 +27,7 @@ import {
|
||||
} from '../util/iteratees';
|
||||
import {
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentMessageList, selectThreadOriginChat,
|
||||
selectVisibleUsers,
|
||||
} from './selectors';
|
||||
import { hasStoredSession } from '../util/sessions';
|
||||
@ -459,8 +459,19 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
|
||||
const { chats: { byId }, currentUserId } = global;
|
||||
const currentChatIds = compact(
|
||||
Object.values(global.byTabId)
|
||||
.map(({ id: tabId }) => selectCurrentMessageList(global, tabId)),
|
||||
).map(({ chatId }) => chatId).filter((chatId) => isUserId(chatId));
|
||||
.flatMap(({ id: tabId }): MessageList[] | undefined => {
|
||||
const messageList = selectCurrentMessageList(global, tabId);
|
||||
if (!messageList) return undefined;
|
||||
|
||||
const { chatId, threadId } = messageList;
|
||||
const origin = selectThreadOriginChat(global, chatId, threadId);
|
||||
return origin ? [{
|
||||
chatId: origin.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
type: 'thread',
|
||||
}, messageList] : [messageList];
|
||||
}),
|
||||
).map(({ chatId }) => chatId);
|
||||
|
||||
const idsToSave = unique([
|
||||
...currentUserId ? [currentUserId] : [],
|
||||
|
||||
@ -177,7 +177,7 @@ export function isUserRightBanned(chat: ApiChat, key: keyof ApiChatBannedRights)
|
||||
);
|
||||
}
|
||||
|
||||
export function getCanPostInChat(chat: ApiChat, threadId: number) {
|
||||
export function getCanPostInChat(chat: ApiChat, threadId: number, isComments?: boolean) {
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
if (chat.isForum) {
|
||||
if (chat.isNotJoined) {
|
||||
@ -189,10 +189,10 @@ export function getCanPostInChat(chat: ApiChat, threadId: number) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true; // TODO[forums] legacy value, check that again
|
||||
}
|
||||
|
||||
if (chat.isRestricted || chat.isForbidden || chat.migratedTo || chat.isNotJoined || isChatWithRepliesBot(chat.id)) {
|
||||
if (chat.isRestricted || chat.isForbidden || chat.migratedTo
|
||||
|| (!isComments && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { cloneDeep } from '../util/iteratees';
|
||||
import { replaceTabThreadParam, replaceThreadParam, updatePasscodeSettings } from './reducers';
|
||||
import { clearStoredSession } from '../util/sessions';
|
||||
import { parseLocationHash } from '../util/routing';
|
||||
import { MAIN_THREAD_ID } from '../api/types';
|
||||
import { selectTabState, selectThreadParam } from './selectors';
|
||||
import { Bundles, loadBundle } from '../util/moduleLoader';
|
||||
import { getCurrentTabId, reestablishMasterToSelf } from '../util/establishMultitabRole';
|
||||
@ -68,20 +67,24 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
|
||||
}
|
||||
|
||||
Object.keys(global.messages.byChatId).forEach((chatId) => {
|
||||
const lastViewportIds = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'lastViewportIds');
|
||||
// Check if migration from previous version is faulty
|
||||
if (!lastViewportIds?.every((id) => isLocalMessageId(id) || global.messages.byChatId[chatId]?.byId[id])) {
|
||||
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'lastViewportIds', undefined);
|
||||
return;
|
||||
}
|
||||
global = replaceTabThreadParam(
|
||||
global,
|
||||
chatId,
|
||||
MAIN_THREAD_ID,
|
||||
'viewportIds',
|
||||
lastViewportIds,
|
||||
tabId,
|
||||
);
|
||||
const threadsById = global.messages.byChatId[chatId].threadsById;
|
||||
Object.keys(threadsById).forEach((thread) => {
|
||||
const threadId = Number(thread);
|
||||
const lastViewportIds = selectThreadParam(global, chatId, threadId, 'lastViewportIds');
|
||||
// Check if migration from previous version is faulty
|
||||
if (!lastViewportIds?.every((id) => isLocalMessageId(id) || global.messages.byChatId[chatId]?.byId[id])) {
|
||||
global = replaceThreadParam(global, chatId, threadId, 'lastViewportIds', undefined);
|
||||
return;
|
||||
}
|
||||
global = replaceTabThreadParam(
|
||||
global,
|
||||
chatId,
|
||||
threadId,
|
||||
'viewportIds',
|
||||
lastViewportIds,
|
||||
tabId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// Temporary state fix
|
||||
|
||||
@ -30,6 +30,7 @@ import {
|
||||
} from '../../util/iteratees';
|
||||
import { updateTabState } from './tabs';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { isLocalMessageId } from '../helpers';
|
||||
|
||||
type MessageStoreSections = {
|
||||
byId: Record<number, ApiMessage>;
|
||||
@ -254,7 +255,7 @@ export function deleteChatMessages<T extends GlobalState>(
|
||||
messageIds.forEach((messageId) => {
|
||||
if (listedIds?.includes(messageId)) {
|
||||
listedIds = listedIds.filter((id) => id !== messageId);
|
||||
if (newMessageCount !== undefined) newMessageCount -= 1;
|
||||
if (newMessageCount !== undefined && !isLocalMessageId(messageId)) newMessageCount -= 1;
|
||||
}
|
||||
|
||||
if (pinnedIds?.includes(messageId)) {
|
||||
|
||||
@ -482,8 +482,14 @@ export function selectThreadIdFromMessage<T extends GlobalState>(global: T, mess
|
||||
return message.id;
|
||||
}
|
||||
|
||||
// TODO ignore only basic group if reply threads are added
|
||||
if (!chat?.isForum) return MAIN_THREAD_ID;
|
||||
if (!chat?.isForum) {
|
||||
if (chat && isChatBasicGroup(chat)) return MAIN_THREAD_ID;
|
||||
|
||||
if (chat && isChatSuperGroup(chat)) {
|
||||
return replyToTopMessageId || replyToMessageId || MAIN_THREAD_ID;
|
||||
}
|
||||
return MAIN_THREAD_ID;
|
||||
}
|
||||
if (!isTopicReply) return GENERAL_TOPIC_ID;
|
||||
return replyToTopMessageId || replyToMessageId || GENERAL_TOPIC_ID;
|
||||
}
|
||||
@ -531,7 +537,10 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
|
||||
&& !chat.isForbidden
|
||||
);
|
||||
|
||||
const canReply = !isLocal && !isServiceNotification && !chat.isForbidden && getCanPostInChat(chat, threadId)
|
||||
const threadInfo = selectThreadInfo(global, message.chatId, threadId);
|
||||
const isComments = Boolean(threadInfo?.originChannelId);
|
||||
const canReply = !isLocal && !isServiceNotification && !chat.isForbidden
|
||||
&& getCanPostInChat(chat, threadId, isComments)
|
||||
&& (!messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics'));
|
||||
|
||||
const hasPinPermission = isPrivate || (
|
||||
|
||||
@ -1572,6 +1572,11 @@ export interface ActionPayloads {
|
||||
shouldReplaceHistory?: boolean;
|
||||
noForumTopicPanel?: boolean;
|
||||
} & WithTabId;
|
||||
openComments: {
|
||||
id: string;
|
||||
threadId: number;
|
||||
originChannelId?: string;
|
||||
} & WithTabId;
|
||||
loadFullChat: {
|
||||
chatId: string;
|
||||
force?: boolean;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,9 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-replies:before {
|
||||
content: "\e9b9";
|
||||
}
|
||||
.icon-forums:before {
|
||||
content: "\e9b4";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user