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