Introduce Forums (#2174)
@ -16,6 +16,7 @@ const config: PlaywrightTestConfig = {
|
|||||||
video: 'retain-on-failure',
|
video: 'retain-on-failure',
|
||||||
trace: 'on-first-retry',
|
trace: 'on-first-retry',
|
||||||
},
|
},
|
||||||
|
reporter: [['html', { outputFolder: 'playwright-report' }]],
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
name: 'chromium',
|
name: 'chromium',
|
||||||
|
|||||||
@ -14,7 +14,7 @@ type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit
|
|||||||
type LimitKey = `${Limit}_${LimitType}`;
|
type LimitKey = `${Limit}_${LimitType}`;
|
||||||
type LimitsConfig = Record<LimitKey, number>;
|
type LimitsConfig = Record<LimitKey, number>;
|
||||||
|
|
||||||
interface GramJsAppConfig extends LimitsConfig {
|
export interface GramJsAppConfig extends LimitsConfig {
|
||||||
emojies_sounds: Record<string, {
|
emojies_sounds: Record<string, {
|
||||||
id: string;
|
id: string;
|
||||||
access_hash: string;
|
access_hash: string;
|
||||||
@ -35,6 +35,8 @@ interface GramJsAppConfig extends LimitsConfig {
|
|||||||
premium_invoice_slug: string;
|
premium_invoice_slug: string;
|
||||||
premium_promo_order: string[];
|
premium_promo_order: string[];
|
||||||
default_emoji_statuses_stickerset_id: string;
|
default_emoji_statuses_stickerset_id: string;
|
||||||
|
// Forums
|
||||||
|
topics_pinned_limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
function buildEmojiSounds(appConfig: GramJsAppConfig) {
|
||||||
@ -79,6 +81,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue): ApiAppConfig {
|
|||||||
premiumPromoOrder: appConfig.premium_promo_order,
|
premiumPromoOrder: appConfig.premium_promo_order,
|
||||||
isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked,
|
isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked,
|
||||||
defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id,
|
defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id,
|
||||||
|
topicsPinnedLimit: appConfig.topics_pinned_limit,
|
||||||
maxUserReactionsDefault: appConfig.reactions_user_max_default,
|
maxUserReactionsDefault: appConfig.reactions_user_max_default,
|
||||||
maxUserReactionsPremium: appConfig.reactions_user_max_premium,
|
maxUserReactionsPremium: appConfig.reactions_user_max_premium,
|
||||||
limits: {
|
limits: {
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import type {
|
|||||||
ApiExportedInvite,
|
ApiExportedInvite,
|
||||||
ApiChatInviteImporter,
|
ApiChatInviteImporter,
|
||||||
ApiChatSettings,
|
ApiChatSettings,
|
||||||
|
ApiTopic,
|
||||||
ApiSendAsPeerId,
|
ApiSendAsPeerId,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
@ -45,6 +46,7 @@ function buildApiChatFieldsFromPeerEntity(
|
|||||||
const isJoinToSend = Boolean('joinToSend' in peerEntity && peerEntity.joinToSend);
|
const isJoinToSend = Boolean('joinToSend' in peerEntity && peerEntity.joinToSend);
|
||||||
const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest);
|
const isJoinRequest = Boolean('joinRequest' in peerEntity && peerEntity.joinRequest);
|
||||||
const usernames = buildApiUsernames(peerEntity);
|
const usernames = buildApiUsernames(peerEntity);
|
||||||
|
const isForum = Boolean('forum' in peerEntity && peerEntity.forum);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isMin,
|
isMin,
|
||||||
@ -70,6 +72,7 @@ function buildApiChatFieldsFromPeerEntity(
|
|||||||
fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined),
|
fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined),
|
||||||
isJoinToSend,
|
isJoinToSend,
|
||||||
isJoinRequest,
|
isJoinRequest,
|
||||||
|
isForum,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,7 +260,7 @@ export function getApiChatTitleFromMtpPeer(peer: GramJs.TypePeer, peerEntity: Gr
|
|||||||
function getUserName(user: GramJs.User) {
|
function getUserName(user: GramJs.User) {
|
||||||
return user.firstName
|
return user.firstName
|
||||||
? `${user.firstName}${user.lastName ? ` ${user.lastName}` : ''}`
|
? `${user.firstName}${user.lastName ? ` ${user.lastName}` : ''}`
|
||||||
: (user.lastName || undefined);
|
: (user.lastName || '');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAvatarHash(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) {
|
export function buildAvatarHash(photo: GramJs.TypeUserProfilePhoto | GramJs.TypeChatPhoto) {
|
||||||
@ -486,3 +489,50 @@ export function buildApiSendAsPeerId(sendAs: GramJs.SendAsPeer): ApiSendAsPeerId
|
|||||||
isPremium: sendAs.premiumRequired,
|
isPremium: sendAs.premiumRequired,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | undefined {
|
||||||
|
if (forumTopic instanceof GramJs.ForumTopicDeleted) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
my,
|
||||||
|
closed,
|
||||||
|
pinned,
|
||||||
|
hidden,
|
||||||
|
short,
|
||||||
|
date,
|
||||||
|
title,
|
||||||
|
iconColor,
|
||||||
|
iconEmojiId,
|
||||||
|
topMessage,
|
||||||
|
unreadCount,
|
||||||
|
unreadMentionsCount,
|
||||||
|
unreadReactionsCount,
|
||||||
|
fromId,
|
||||||
|
notifySettings: {
|
||||||
|
silent, muteUntil,
|
||||||
|
},
|
||||||
|
} = forumTopic;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
isClosed: closed,
|
||||||
|
isPinned: pinned,
|
||||||
|
isHidden: hidden,
|
||||||
|
isOwner: my,
|
||||||
|
isMin: short,
|
||||||
|
date,
|
||||||
|
title,
|
||||||
|
iconColor,
|
||||||
|
iconEmojiId: iconEmojiId?.toString(),
|
||||||
|
lastMessageId: topMessage,
|
||||||
|
unreadCount,
|
||||||
|
unreadMentionsCount,
|
||||||
|
unreadReactionsCount,
|
||||||
|
fromId: getApiChatIdFromMtpPeer(fromId),
|
||||||
|
// TODO[forums] `muteUntil` should not really be parsed here
|
||||||
|
isMuted: silent || (muteUntil !== undefined ? muteUntil > 0 : undefined),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -152,7 +152,10 @@ type UniversalMessage = (
|
|||||||
)>
|
)>
|
||||||
);
|
);
|
||||||
|
|
||||||
export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalMessage): ApiMessage {
|
export function buildApiMessageWithChatId(
|
||||||
|
chatId: string,
|
||||||
|
mtpMessage: UniversalMessage,
|
||||||
|
): ApiMessage {
|
||||||
const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined;
|
const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined;
|
||||||
const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined;
|
const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined;
|
||||||
const isChatWithSelf = !fromId && chatId === currentUserId;
|
const isChatWithSelf = !fromId && chatId === currentUserId;
|
||||||
@ -167,7 +170,9 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
|||||||
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
|
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
|
||||||
&& Boolean(mtpMessage.media.extendedMedia);
|
&& Boolean(mtpMessage.media.extendedMedia);
|
||||||
|
|
||||||
const { replyToMsgId, replyToTopId, replyToPeerId } = mtpMessage.replyTo || {};
|
const {
|
||||||
|
replyToMsgId, replyToTopId, forumTopic, replyToPeerId,
|
||||||
|
} = mtpMessage.replyTo || {};
|
||||||
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
|
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
|
||||||
const {
|
const {
|
||||||
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse,
|
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse,
|
||||||
@ -195,6 +200,7 @@ export function buildApiMessageWithChatId(chatId: string, mtpMessage: UniversalM
|
|||||||
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
|
||||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||||
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
...(replyToMsgId && { replyToMessageId: replyToMsgId }),
|
||||||
|
...(forumTopic && { isTopicReply: true }),
|
||||||
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
|
||||||
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
|
||||||
...(forwardInfo && { forwardInfo }),
|
...(forwardInfo && { forwardInfo }),
|
||||||
@ -1017,6 +1023,23 @@ function buildAction(
|
|||||||
currency = action.currency;
|
currency = action.currency;
|
||||||
amount = action.amount.toJSNumber();
|
amount = action.amount.toJSNumber();
|
||||||
months = action.months;
|
months = action.months;
|
||||||
|
} else if (action instanceof GramJs.MessageActionTopicCreate) {
|
||||||
|
text = 'TopicWasCreatedAction';
|
||||||
|
type = 'topicCreate';
|
||||||
|
translationValues.push(action.title);
|
||||||
|
} else if (action instanceof GramJs.MessageActionTopicEdit) {
|
||||||
|
if (action.closed !== undefined) {
|
||||||
|
text = action.closed ? 'TopicWasClosedAction' : 'TopicWasReopenedAction';
|
||||||
|
translationValues.push('%action_origin%', '%action_topic%');
|
||||||
|
} else if (action.hidden !== undefined) {
|
||||||
|
text = action.hidden ? 'TopicHidden2' : 'TopicWasUnhiddenAction';
|
||||||
|
} else if (action.title) {
|
||||||
|
text = 'TopicRenamedTo';
|
||||||
|
translationValues.push('%action_origin%', action.title);
|
||||||
|
} else {
|
||||||
|
// TODO[forums] Support icon changed action
|
||||||
|
text = 'ChatList.UnsupportedMessage';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
text = 'ChatList.UnsupportedMessage';
|
text = 'ChatList.UnsupportedMessage';
|
||||||
}
|
}
|
||||||
@ -1238,6 +1261,7 @@ export function buildLocalMessage(
|
|||||||
const localId = getNextLocalMessageId();
|
const localId = getNextLocalMessageId();
|
||||||
const media = attachment && buildUploadingMedia(attachment);
|
const media = attachment && buildUploadingMedia(attachment);
|
||||||
const isChannel = chat.type === 'chatTypeChannel';
|
const isChannel = chat.type === 'chatTypeChannel';
|
||||||
|
const isForum = chat.isForum;
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
id: localId,
|
id: localId,
|
||||||
@ -1260,13 +1284,14 @@ export function buildLocalMessage(
|
|||||||
senderId: sendAs?.id || currentUserId,
|
senderId: sendAs?.id || currentUserId,
|
||||||
...(replyingTo && { replyToMessageId: replyingTo }),
|
...(replyingTo && { replyToMessageId: replyingTo }),
|
||||||
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
|
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
|
||||||
|
...((replyingTo || replyingToTopId) && isForum && { isTopicReply: true }),
|
||||||
...(groupedId && {
|
...(groupedId && {
|
||||||
groupedId,
|
groupedId,
|
||||||
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
...(media && (media.photo || media.video) && { isInAlbum: true }),
|
||||||
}),
|
}),
|
||||||
...(scheduledAt && { isScheduled: true }),
|
...(scheduledAt && { isScheduled: true }),
|
||||||
isForwardingAllowed: true,
|
isForwardingAllowed: true,
|
||||||
};
|
} satisfies ApiMessage;
|
||||||
|
|
||||||
const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId);
|
const emojiOnlyCount = getEmojiOnlyCountForMessage(message.content, message.groupedId);
|
||||||
|
|
||||||
@ -1276,15 +1301,25 @@ export function buildLocalMessage(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildLocalForwardedMessage(
|
export function buildLocalForwardedMessage({
|
||||||
toChat: ApiChat,
|
toChat,
|
||||||
message: ApiMessage,
|
toThreadId,
|
||||||
serverTimeOffset: number,
|
message,
|
||||||
scheduledAt?: number,
|
serverTimeOffset,
|
||||||
noAuthors?: boolean,
|
scheduledAt,
|
||||||
noCaptions?: boolean,
|
noAuthors,
|
||||||
isCurrentUserPremium?: boolean,
|
noCaptions,
|
||||||
): ApiMessage {
|
isCurrentUserPremium,
|
||||||
|
}: {
|
||||||
|
toChat: ApiChat;
|
||||||
|
toThreadId?: number;
|
||||||
|
message: ApiMessage;
|
||||||
|
serverTimeOffset: number;
|
||||||
|
scheduledAt?: number;
|
||||||
|
noAuthors?: boolean;
|
||||||
|
noCaptions?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
}): ApiMessage {
|
||||||
const localId = getNextLocalMessageId();
|
const localId = getNextLocalMessageId();
|
||||||
const {
|
const {
|
||||||
content,
|
content,
|
||||||
@ -1322,6 +1357,8 @@ export function buildLocalForwardedMessage(
|
|||||||
sendingState: 'messageSendingStatePending',
|
sendingState: 'messageSendingStatePending',
|
||||||
groupedId,
|
groupedId,
|
||||||
isInAlbum,
|
isInAlbum,
|
||||||
|
isForwardingAllowed: true,
|
||||||
|
replyToTopMessageId: toThreadId,
|
||||||
...(emojiOnlyCount && { emojiOnlyCount }),
|
...(emojiOnlyCount && { emojiOnlyCount }),
|
||||||
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio
|
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio
|
||||||
...(message.chatId !== currentUserId && !isAudio && !noAuthors && {
|
...(message.chatId !== currentUserId && !isAudio && !noAuthors && {
|
||||||
@ -1335,7 +1372,6 @@ export function buildLocalForwardedMessage(
|
|||||||
}),
|
}),
|
||||||
...(message.chatId === currentUserId && !noAuthors && { forwardInfo: message.forwardInfo }),
|
...(message.chatId === currentUserId && !noAuthors && { forwardInfo: message.forwardInfo }),
|
||||||
...(scheduledAt && { isScheduled: true }),
|
...(scheduledAt && { isScheduled: true }),
|
||||||
isForwardingAllowed: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -139,6 +139,24 @@ export function buildApiNotifyException(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildApiNotifyExceptionTopic(
|
||||||
|
notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer, topicId: number, serverTimeOffset: number,
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
silent, muteUntil, showPreviews, otherSound,
|
||||||
|
} = notifySettings;
|
||||||
|
|
||||||
|
const hasSound = Boolean(otherSound && !(otherSound instanceof GramJs.NotificationSoundNone));
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatId: getApiChatIdFromMtpPeer(peer),
|
||||||
|
topicId,
|
||||||
|
isMuted: silent || (typeof muteUntil === 'number' && getServerTime(serverTimeOffset) < muteUntil),
|
||||||
|
...(!hasSound && { isSilent: true }),
|
||||||
|
...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function buildApiCountry(country: GramJs.help.Country, code: GramJs.help.CountryCode) {
|
function buildApiCountry(country: GramJs.help.Country, code: GramJs.help.CountryCode) {
|
||||||
const {
|
const {
|
||||||
hidden, iso2, defaultName, name,
|
hidden, iso2, defaultName, name,
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export function buildMessagePublicForwards(
|
|||||||
chat: {
|
chat: {
|
||||||
id: peerId,
|
id: peerId,
|
||||||
type: 'chatTypeChannel',
|
type: 'chatTypeChannel',
|
||||||
|
title: (channel as GramJs.Channel).title,
|
||||||
username: (channel as GramJs.Channel).username,
|
username: (channel as GramJs.Channel).username,
|
||||||
avatarHash: buildAvatarHash((channel as GramJs.Channel).photo),
|
avatarHash: buildAvatarHash((channel as GramJs.Channel).photo),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -108,9 +108,10 @@ export async function fetchInlineBotResults({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function sendInlineBotResult({
|
export async function sendInlineBotResult({
|
||||||
chat, resultId, queryId, replyingTo, sendAs, isSilent, scheduleDate,
|
chat, replyingToTopId, resultId, queryId, replyingTo, sendAs, isSilent, scheduleDate,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
|
replyingToTopId?: number;
|
||||||
resultId: string;
|
resultId: string;
|
||||||
queryId: string;
|
queryId: string;
|
||||||
replyingTo?: number;
|
replyingTo?: number;
|
||||||
@ -127,6 +128,7 @@ export async function sendInlineBotResult({
|
|||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
id: resultId,
|
id: resultId,
|
||||||
scheduleDate,
|
scheduleDate,
|
||||||
|
...(replyingToTopId && { topMsgId: replyingToTopId }),
|
||||||
...(isSilent && { silent: true }),
|
...(isSilent && { silent: true }),
|
||||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
@ -156,6 +158,7 @@ export async function requestWebView({
|
|||||||
url,
|
url,
|
||||||
startParam,
|
startParam,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
|
threadId,
|
||||||
theme,
|
theme,
|
||||||
sendAs,
|
sendAs,
|
||||||
isFromBotMenu,
|
isFromBotMenu,
|
||||||
@ -166,6 +169,7 @@ export async function requestWebView({
|
|||||||
url?: string;
|
url?: string;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
|
threadId?: number;
|
||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
sendAs?: ApiUser | ApiChat;
|
sendAs?: ApiUser | ApiChat;
|
||||||
isFromBotMenu?: boolean;
|
isFromBotMenu?: boolean;
|
||||||
@ -180,6 +184,7 @@ export async function requestWebView({
|
|||||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||||
fromBotMenu: isFromBotMenu || undefined,
|
fromBotMenu: isFromBotMenu || undefined,
|
||||||
platform: 'webz',
|
platform: 'webz',
|
||||||
|
...(threadId && { topMsgId: threadId }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -216,6 +221,7 @@ export function prolongWebView({
|
|||||||
bot,
|
bot,
|
||||||
queryId,
|
queryId,
|
||||||
replyToMessageId,
|
replyToMessageId,
|
||||||
|
threadId,
|
||||||
sendAs,
|
sendAs,
|
||||||
}: {
|
}: {
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
@ -223,6 +229,7 @@ export function prolongWebView({
|
|||||||
bot: ApiUser;
|
bot: ApiUser;
|
||||||
queryId: string;
|
queryId: string;
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
|
threadId?: number;
|
||||||
sendAs?: ApiUser | ApiChat;
|
sendAs?: ApiUser | ApiChat;
|
||||||
}) {
|
}) {
|
||||||
return invokeRequest(new GramJs.messages.ProlongWebView({
|
return invokeRequest(new GramJs.messages.ProlongWebView({
|
||||||
@ -231,6 +238,7 @@ export function prolongWebView({
|
|||||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||||
queryId: BigInt(queryId),
|
queryId: BigInt(queryId),
|
||||||
replyToMsgId: replyToMessageId,
|
replyToMsgId: replyToMessageId,
|
||||||
|
...(threadId && { topMsgId: threadId }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type BigInt from 'big-integer';
|
import BigInt from 'big-integer';
|
||||||
import { Api as GramJs } from '../../../lib/gramjs';
|
import { Api as GramJs } from '../../../lib/gramjs';
|
||||||
import type {
|
import type {
|
||||||
OnApiUpdate,
|
OnApiUpdate,
|
||||||
@ -12,11 +12,20 @@ import type {
|
|||||||
ApiChatBannedRights,
|
ApiChatBannedRights,
|
||||||
ApiChatAdminRights,
|
ApiChatAdminRights,
|
||||||
ApiGroupCall,
|
ApiGroupCall,
|
||||||
ApiUserStatus, ApiPhoto, ApiChatReactions,
|
ApiUserStatus,
|
||||||
|
ApiPhoto,
|
||||||
|
ApiTopic,
|
||||||
|
ApiChatReactions,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEBUG, ARCHIVED_FOLDER_ID, MEMBERS_LOAD_SLICE, SERVICE_NOTIFICATIONS_USER_ID, ALL_FOLDER_ID, MAX_INT_32,
|
DEBUG,
|
||||||
|
ARCHIVED_FOLDER_ID,
|
||||||
|
MEMBERS_LOAD_SLICE,
|
||||||
|
SERVICE_NOTIFICATIONS_USER_ID,
|
||||||
|
ALL_FOLDER_ID,
|
||||||
|
MAX_INT_32,
|
||||||
|
TOPICS_SLICE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { invokeRequest, uploadFile } from './client';
|
import { invokeRequest, uploadFile } from './client';
|
||||||
import {
|
import {
|
||||||
@ -29,6 +38,7 @@ import {
|
|||||||
buildApiChatBotCommands,
|
buildApiChatBotCommands,
|
||||||
buildApiChatSettings,
|
buildApiChatSettings,
|
||||||
buildApiChatReactions,
|
buildApiChatReactions,
|
||||||
|
buildApiTopic,
|
||||||
} from '../apiBuilders/chats';
|
} from '../apiBuilders/chats';
|
||||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||||
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||||
@ -323,11 +333,13 @@ export function saveDraft({
|
|||||||
chat,
|
chat,
|
||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
|
threadId,
|
||||||
replyToMsgId,
|
replyToMsgId,
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
text: string;
|
text: string;
|
||||||
entities?: ApiMessageEntity[];
|
entities?: ApiMessageEntity[];
|
||||||
|
threadId?: number;
|
||||||
replyToMsgId?: number;
|
replyToMsgId?: number;
|
||||||
}) {
|
}) {
|
||||||
return invokeRequest(new GramJs.messages.SaveDraft({
|
return invokeRequest(new GramJs.messages.SaveDraft({
|
||||||
@ -337,13 +349,15 @@ export function saveDraft({
|
|||||||
entities: entities.map(buildMtpMessageEntity),
|
entities: entities.map(buildMtpMessageEntity),
|
||||||
}),
|
}),
|
||||||
replyToMsgId,
|
replyToMsgId,
|
||||||
|
topMsgId: threadId,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearDraft(chat: ApiChat) {
|
export function clearDraft(chat: ApiChat, threadId?: number) {
|
||||||
return invokeRequest(new GramJs.messages.SaveDraft({
|
return invokeRequest(new GramJs.messages.SaveDraft({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
message: '',
|
message: '',
|
||||||
|
...(threadId && { topMsgId: threadId }),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,6 +571,30 @@ export async function updateChatMutedState({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateTopicMutedState({
|
||||||
|
chat, topicId, isMuted,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat; topicId: number; isMuted: boolean; serverTimeOffset: number;
|
||||||
|
|
||||||
|
}) {
|
||||||
|
await invokeRequest(new GramJs.account.UpdateNotifySettings({
|
||||||
|
peer: new GramJs.InputNotifyForumTopic({
|
||||||
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
topMsgId: topicId,
|
||||||
|
}),
|
||||||
|
settings: new GramJs.InputPeerNotifySettings({ muteUntil: isMuted ? MAX_INT_32 : 0 }),
|
||||||
|
}));
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateTopicNotifyExceptions',
|
||||||
|
chatId: chat.id,
|
||||||
|
topicId,
|
||||||
|
isMuted,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO[forums] Request forum topic thread update
|
||||||
|
}
|
||||||
|
|
||||||
export async function createChannel({
|
export async function createChannel({
|
||||||
title, about = '', users,
|
title, about = '', users,
|
||||||
}: {
|
}: {
|
||||||
@ -1234,7 +1272,7 @@ function updateLocalDb(result: (
|
|||||||
GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs |
|
GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs |
|
||||||
GramJs.messages.ChatFull | GramJs.contacts.Found |
|
GramJs.messages.ChatFull | GramJs.contacts.Found |
|
||||||
GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants |
|
GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants |
|
||||||
GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates
|
GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates | GramJs.messages.ForumTopics
|
||||||
)) {
|
)) {
|
||||||
if ('users' in result) {
|
if ('users' in result) {
|
||||||
addEntitiesWithPhotosToLocalDb(result.users);
|
addEntitiesWithPhotosToLocalDb(result.users);
|
||||||
@ -1283,3 +1321,168 @@ export function toggleIsProtected({
|
|||||||
enabled: isProtected,
|
enabled: isProtected,
|
||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toggleForum({
|
||||||
|
chat, isEnabled,
|
||||||
|
}: { chat: ApiChat; isEnabled: boolean }) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.channels.ToggleForum({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
enabled: isEnabled,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTopics({
|
||||||
|
chat, query, offsetTopicId, offsetId, offsetDate, limit = TOPICS_SLICE,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
query?: string;
|
||||||
|
offsetTopicId?: number;
|
||||||
|
offsetId?: number;
|
||||||
|
offsetDate?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<{
|
||||||
|
topics: ApiTopic[];
|
||||||
|
messages: ApiMessage[];
|
||||||
|
users: ApiUser[];
|
||||||
|
chats: ApiChat[];
|
||||||
|
count: number;
|
||||||
|
shouldOrderByCreateDate?: boolean;
|
||||||
|
draftsById: Record<number, ReturnType<typeof buildMessageDraft>>;
|
||||||
|
readInboxMessageIdByTopicId: Record<number, number>;
|
||||||
|
} | undefined> {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
const result = await invokeRequest(new GramJs.channels.GetForumTopics({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
limit,
|
||||||
|
q: query,
|
||||||
|
offsetTopic: offsetTopicId,
|
||||||
|
offsetId,
|
||||||
|
offsetDate,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result) return undefined;
|
||||||
|
|
||||||
|
updateLocalDb(result);
|
||||||
|
|
||||||
|
const { count, orderByCreateDate } = result;
|
||||||
|
|
||||||
|
const topics = result.topics.map(buildApiTopic).filter(Boolean);
|
||||||
|
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||||
|
const draftsById = result.topics.reduce((acc, topic) => {
|
||||||
|
if (topic instanceof GramJs.ForumTopic && topic.draft) {
|
||||||
|
acc[topic.id] = buildMessageDraft(topic.draft);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, ReturnType<typeof buildMessageDraft>>);
|
||||||
|
const readInboxMessageIdByTopicId = result.topics.reduce((acc, topic) => {
|
||||||
|
if (topic instanceof GramJs.ForumTopic && topic.readInboxMaxId) {
|
||||||
|
acc[topic.id] = topic.readInboxMaxId;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, number>);
|
||||||
|
|
||||||
|
return {
|
||||||
|
topics,
|
||||||
|
messages,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
// Include general topic
|
||||||
|
count: count + 1,
|
||||||
|
shouldOrderByCreateDate: orderByCreateDate,
|
||||||
|
draftsById,
|
||||||
|
readInboxMessageIdByTopicId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTopicById({
|
||||||
|
chat, topicId,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
topicId: number;
|
||||||
|
}): Promise<{
|
||||||
|
topic: ApiTopic;
|
||||||
|
messages: ApiMessage[];
|
||||||
|
users: ApiUser[];
|
||||||
|
chats: ApiChat[];
|
||||||
|
} | undefined> {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
const result = await invokeRequest(new GramJs.channels.GetForumTopicsByID({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
topics: [topicId],
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result?.topics.length || !(result.topics[0] instanceof GramJs.ForumTopic)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalDb(result);
|
||||||
|
|
||||||
|
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||||
|
|
||||||
|
return {
|
||||||
|
topic: buildApiTopic(result.topics[0])!,
|
||||||
|
messages,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteTopic({
|
||||||
|
chat, topicId,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
topicId: number;
|
||||||
|
}) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.channels.DeleteTopicHistory({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
topMsgId: topicId,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function togglePinnedTopic({
|
||||||
|
chat, topicId, isPinned,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
topicId: number;
|
||||||
|
isPinned: boolean;
|
||||||
|
}) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.channels.UpdatePinnedForumTopic({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
topicId,
|
||||||
|
pinned: isPinned,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function editTopic({
|
||||||
|
chat, topicId, title, iconEmojiId, isClosed, isHidden,
|
||||||
|
}: {
|
||||||
|
chat: ApiChat;
|
||||||
|
topicId: number;
|
||||||
|
title?: string;
|
||||||
|
iconEmojiId?: string;
|
||||||
|
isClosed?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
}) {
|
||||||
|
const { id, accessHash } = chat;
|
||||||
|
|
||||||
|
return invokeRequest(new GramJs.channels.EditForumTopic({
|
||||||
|
channel: buildInputPeer(id, accessHash),
|
||||||
|
topicId,
|
||||||
|
title,
|
||||||
|
iconEmojiId: iconEmojiId ? BigInt(iconEmojiId) : undefined,
|
||||||
|
closed: isClosed,
|
||||||
|
hidden: isHidden,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|||||||
@ -52,6 +52,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, maxBufferSize, webAuthToken, dcId,
|
userAgent, platform, sessionData, isTest, isMovSupported, isWebmSupported, maxBufferSize, webAuthToken, dcId,
|
||||||
|
mockScenario,
|
||||||
} = initialArgs;
|
} = initialArgs;
|
||||||
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
const session = new sessions.CallbackSession(sessionData, onSessionUpdate);
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ export async function init(_onUpdate: OnApiUpdate, initialArgs: ApiInitialArgs)
|
|||||||
shouldThrowIfUnauthorized: Boolean(sessionData),
|
shouldThrowIfUnauthorized: Boolean(sessionData),
|
||||||
webAuthToken,
|
webAuthToken,
|
||||||
webAuthTokenFailed: onWebAuthTokenFailed,
|
webAuthTokenFailed: onWebAuthTokenFailed,
|
||||||
|
mockScenario,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
|
|||||||
@ -12,14 +12,15 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
fetchChats, fetchFullChat, searchChats, requestChatUpdate, fetchChatSettings,
|
fetchChats, fetchFullChat, searchChats, requestChatUpdate, fetchChatSettings,
|
||||||
saveDraft, clearDraft, fetchChat, updateChatMutedState,
|
saveDraft, clearDraft, fetchChat, updateChatMutedState, updateTopicMutedState,
|
||||||
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
|
||||||
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
|
||||||
fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders,
|
fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders,
|
||||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||||
getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest,
|
getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest, fetchTopics, deleteTopic, togglePinnedTopic,
|
||||||
|
editTopic, toggleForum, fetchTopicById,
|
||||||
} from './chats';
|
} from './chats';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export async function fetchMessages({
|
|||||||
result = await invokeRequest(new RequestClass({
|
result = await invokeRequest(new RequestClass({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
...(threadId !== MAIN_THREAD_ID && {
|
...(threadId !== MAIN_THREAD_ID && {
|
||||||
msgId: threadId,
|
msgId: Number(threadId),
|
||||||
}),
|
}),
|
||||||
...(offsetId && {
|
...(offsetId && {
|
||||||
// Workaround for local message IDs overflowing some internal `Buffer` range check
|
// Workaround for local message IDs overflowing some internal `Buffer` range check
|
||||||
@ -255,6 +255,7 @@ export function sendMessage(
|
|||||||
sendAs,
|
sendAs,
|
||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
);
|
);
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
||||||
id: localMessage.id,
|
id: localMessage.id,
|
||||||
@ -280,7 +281,15 @@ export function sendMessage(
|
|||||||
|
|
||||||
if (groupedId) {
|
if (groupedId) {
|
||||||
return sendGroupedMedia({
|
return sendGroupedMedia({
|
||||||
chat, text, entities, replyingTo, attachment: attachment!, groupedId, isSilent, scheduledAt,
|
chat,
|
||||||
|
text,
|
||||||
|
entities,
|
||||||
|
replyingTo,
|
||||||
|
replyingToTopId,
|
||||||
|
attachment: attachment!,
|
||||||
|
groupedId,
|
||||||
|
isSilent,
|
||||||
|
scheduledAt,
|
||||||
}, randomId, localMessage, onProgress);
|
}, randomId, localMessage, onProgress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -328,6 +337,7 @@ export function sendMessage(
|
|||||||
...(isSilent && { silent: isSilent }),
|
...(isSilent && { silent: isSilent }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||||
|
...(replyingToTopId && { topMsgId: replyingToTopId }),
|
||||||
...(media && { media }),
|
...(media && { media }),
|
||||||
...(noWebPage && { noWebpage: noWebPage }),
|
...(noWebPage && { noWebpage: noWebPage }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
@ -349,6 +359,7 @@ function sendGroupedMedia(
|
|||||||
text,
|
text,
|
||||||
entities,
|
entities,
|
||||||
replyingTo,
|
replyingTo,
|
||||||
|
replyingToTopId,
|
||||||
attachment,
|
attachment,
|
||||||
groupedId,
|
groupedId,
|
||||||
isSilent,
|
isSilent,
|
||||||
@ -359,6 +370,7 @@ function sendGroupedMedia(
|
|||||||
text?: string;
|
text?: string;
|
||||||
entities?: ApiMessageEntity[];
|
entities?: ApiMessageEntity[];
|
||||||
replyingTo?: number;
|
replyingTo?: number;
|
||||||
|
replyingToTopId?: number;
|
||||||
attachment: ApiAttachment;
|
attachment: ApiAttachment;
|
||||||
groupedId: string;
|
groupedId: string;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
@ -434,6 +446,7 @@ function sendGroupedMedia(
|
|||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
|
||||||
replyToMsgId: replyingTo,
|
replyToMsgId: replyingTo,
|
||||||
|
...(replyingToTopId && { topMsgId: replyingToTopId }),
|
||||||
...(isSilent && { silent: isSilent }),
|
...(isSilent && { silent: isSilent }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
@ -628,9 +641,10 @@ export async function pinMessage({
|
|||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unpinAllMessages({ chat }: { chat: ApiChat }) {
|
export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; threadId?: number }) {
|
||||||
await invokeRequest(new GramJs.messages.UnpinAllMessages({
|
await invokeRequest(new GramJs.messages.UnpinAllMessages({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
...(threadId && { topMsgId: threadId }),
|
||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -834,14 +848,18 @@ export async function requestThreadInfoUpdate({
|
|||||||
}: {
|
}: {
|
||||||
chat: ApiChat; threadId: number;
|
chat: ApiChat; threadId: number;
|
||||||
}) {
|
}) {
|
||||||
|
if (threadId === MAIN_THREAD_ID) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const [topMessageResult, repliesResult] = await Promise.all([
|
const [topMessageResult, repliesResult] = await Promise.all([
|
||||||
invokeRequest(new GramJs.messages.GetDiscussionMessage({
|
invokeRequest(new GramJs.messages.GetDiscussionMessage({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
msgId: threadId,
|
msgId: Number(threadId),
|
||||||
})),
|
})),
|
||||||
invokeRequest(new GramJs.messages.GetReplies({
|
invokeRequest(new GramJs.messages.GetReplies({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
msgId: threadId,
|
msgId: Number(threadId),
|
||||||
offsetId: 1,
|
offsetId: 1,
|
||||||
addOffset: -1,
|
addOffset: -1,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
@ -882,6 +900,14 @@ export async function requestThreadInfoUpdate({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (chat.isForum) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateTopic',
|
||||||
|
chatId: chat.id,
|
||||||
|
topicId: threadId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
discussionChatId,
|
discussionChatId,
|
||||||
};
|
};
|
||||||
@ -928,9 +954,9 @@ export async function searchMessagesLocal({
|
|||||||
|
|
||||||
const result = await invokeRequest(new GramJs.messages.Search({
|
const result = await invokeRequest(new GramJs.messages.Search({
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
|
topMsgId: topMessageId,
|
||||||
filter,
|
filter,
|
||||||
q: query || '',
|
q: query || '',
|
||||||
topMsgId: topMessageId,
|
|
||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -1154,6 +1180,7 @@ export async function fetchExtendedMedia({
|
|||||||
export async function forwardMessages({
|
export async function forwardMessages({
|
||||||
fromChat,
|
fromChat,
|
||||||
toChat,
|
toChat,
|
||||||
|
toThreadId,
|
||||||
messages,
|
messages,
|
||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
isSilent,
|
isSilent,
|
||||||
@ -1166,6 +1193,7 @@ export async function forwardMessages({
|
|||||||
}: {
|
}: {
|
||||||
fromChat: ApiChat;
|
fromChat: ApiChat;
|
||||||
toChat: ApiChat;
|
toChat: ApiChat;
|
||||||
|
toThreadId?: number;
|
||||||
messages: ApiMessage[];
|
messages: ApiMessage[];
|
||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
@ -1180,9 +1208,16 @@ export async function forwardMessages({
|
|||||||
const randomIds = messages.map(generateRandomBigInt);
|
const randomIds = messages.map(generateRandomBigInt);
|
||||||
|
|
||||||
messages.forEach((message, index) => {
|
messages.forEach((message, index) => {
|
||||||
const localMessage = buildLocalForwardedMessage(
|
const localMessage = buildLocalForwardedMessage({
|
||||||
toChat, message, serverTimeOffset, scheduledAt, noAuthors, noCaptions, isCurrentUserPremium,
|
toChat,
|
||||||
);
|
toThreadId,
|
||||||
|
message,
|
||||||
|
serverTimeOffset,
|
||||||
|
scheduledAt,
|
||||||
|
noAuthors,
|
||||||
|
noCaptions,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
});
|
||||||
localDb.localMessages[String(randomIds[index])] = localMessage;
|
localDb.localMessages[String(randomIds[index])] = localMessage;
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
@ -1202,6 +1237,7 @@ export async function forwardMessages({
|
|||||||
silent: isSilent || undefined,
|
silent: isSilent || undefined,
|
||||||
dropAuthor: noAuthors || undefined,
|
dropAuthor: noAuthors || undefined,
|
||||||
dropMediaCaptions: noCaptions || undefined,
|
dropMediaCaptions: noCaptions || undefined,
|
||||||
|
...(toThreadId && { topMsgId: toThreadId }),
|
||||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}), true);
|
}), true);
|
||||||
@ -1281,13 +1317,14 @@ function updateLocalDb(result: (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchPinnedMessages({ chat }: { chat: ApiChat }) {
|
export async function fetchPinnedMessages({ chat, threadId }: { chat: ApiChat; threadId: number }) {
|
||||||
const result = await invokeRequest(new GramJs.messages.Search(
|
const result = await invokeRequest(new GramJs.messages.Search(
|
||||||
{
|
{
|
||||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||||
filter: new GramJs.InputMessagesFilterPinned(),
|
filter: new GramJs.InputMessagesFilterPinned(),
|
||||||
q: '',
|
q: '',
|
||||||
limit: PINNED_MESSAGES_LIMIT,
|
limit: PINNED_MESSAGES_LIMIT,
|
||||||
|
topMsgId: threadId,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|||||||
@ -39,7 +39,7 @@ import {
|
|||||||
} from './gramjsBuilders';
|
} from './gramjsBuilders';
|
||||||
import localDb from './localDb';
|
import localDb from './localDb';
|
||||||
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
import { omitVirtualClassFields } from './apiBuilders/helpers';
|
||||||
import { DEBUG } from '../../config';
|
import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
|
||||||
import {
|
import {
|
||||||
addMessageToLocalDb,
|
addMessageToLocalDb,
|
||||||
addEntitiesWithPhotosToLocalDb,
|
addEntitiesWithPhotosToLocalDb,
|
||||||
@ -49,7 +49,12 @@ import {
|
|||||||
log,
|
log,
|
||||||
swapLocalInvoiceMedia,
|
swapLocalInvoiceMedia,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './apiBuilders/misc';
|
import {
|
||||||
|
buildApiNotifyException,
|
||||||
|
buildApiNotifyExceptionTopic,
|
||||||
|
buildPrivacyKey,
|
||||||
|
buildPrivacyRules,
|
||||||
|
} from './apiBuilders/misc';
|
||||||
import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common';
|
import { buildApiPhoto, buildApiUsernames } from './apiBuilders/common';
|
||||||
import {
|
import {
|
||||||
buildApiGroupCall,
|
buildApiGroupCall,
|
||||||
@ -149,6 +154,13 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
let message: ApiMessage | undefined;
|
let message: ApiMessage | undefined;
|
||||||
let shouldForceReply: boolean | undefined;
|
let shouldForceReply: boolean | undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-underscore-dangle
|
||||||
|
const entities = update._entities;
|
||||||
|
if (entities) {
|
||||||
|
addEntitiesWithPhotosToLocalDb(entities);
|
||||||
|
dispatchUserAndChatUpdates(entities);
|
||||||
|
}
|
||||||
|
|
||||||
if (update instanceof GramJs.UpdateShortChatMessage) {
|
if (update instanceof GramJs.UpdateShortChatMessage) {
|
||||||
message = buildApiMessageFromShortChat(update);
|
message = buildApiMessageFromShortChat(update);
|
||||||
} else if (update instanceof GramJs.UpdateShortMessage) {
|
} else if (update instanceof GramJs.UpdateShortMessage) {
|
||||||
@ -174,13 +186,6 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
&& (!update.message.replyMarkup.selective || message.isMentioned);
|
&& (!update.message.replyMarkup.selective || message.isMentioned);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-underscore-dangle
|
|
||||||
const entities = update._entities;
|
|
||||||
if (entities) {
|
|
||||||
addEntitiesWithPhotosToLocalDb(entities);
|
|
||||||
dispatchUserAndChatUpdates(entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (update instanceof GramJs.UpdateNewScheduledMessage) {
|
if (update instanceof GramJs.UpdateNewScheduledMessage) {
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': sentMessageIds.has(message.id) ? 'updateScheduledMessage' : 'newScheduledMessage',
|
'@type': sentMessageIds.has(message.id) ? 'updateScheduledMessage' : 'newScheduledMessage',
|
||||||
@ -282,6 +287,23 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (action instanceof GramJs.MessageActionTopicEdit) {
|
||||||
|
const { replyTo } = update.message;
|
||||||
|
const {
|
||||||
|
replyToMsgId, replyToTopId, forumTopic: isTopicReply,
|
||||||
|
} = replyTo || {};
|
||||||
|
const topicId = !isTopicReply ? GENERAL_TOPIC_ID : replyToTopId || replyToMsgId || GENERAL_TOPIC_ID;
|
||||||
|
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateTopic',
|
||||||
|
chatId: getApiChatIdFromMtpPeer(update.message.peerId!),
|
||||||
|
topicId,
|
||||||
|
});
|
||||||
|
} else if (action instanceof GramJs.MessageActionTopicCreate) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateTopics',
|
||||||
|
chatId: getApiChatIdFromMtpPeer(update.message.peerId!),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
@ -655,6 +677,16 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
'@type': 'updateNotifyExceptions',
|
'@type': 'updateNotifyExceptions',
|
||||||
...buildApiNotifyException(update.notifySettings, update.peer.peer, serverTimeOffset),
|
...buildApiNotifyException(update.notifySettings, update.peer.peer, serverTimeOffset),
|
||||||
});
|
});
|
||||||
|
} else if (
|
||||||
|
update instanceof GramJs.UpdateNotifySettings
|
||||||
|
&& update.peer instanceof GramJs.NotifyForumTopic
|
||||||
|
) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateTopicNotifyExceptions',
|
||||||
|
...buildApiNotifyExceptionTopic(
|
||||||
|
update.notifySettings, update.peer.peer, update.peer.topMsgId, serverTimeOffset,
|
||||||
|
),
|
||||||
|
});
|
||||||
} else if (
|
} else if (
|
||||||
update instanceof GramJs.UpdateUserTyping
|
update instanceof GramJs.UpdateUserTyping
|
||||||
|| update instanceof GramJs.UpdateChatUserTyping
|
|| update instanceof GramJs.UpdateChatUserTyping
|
||||||
@ -684,6 +716,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateChatTypingStatus',
|
'@type': 'updateChatTypingStatus',
|
||||||
id,
|
id,
|
||||||
|
threadId: update.topMsgId,
|
||||||
typingStatus: buildChatTypingStatus(update, serverTimeOffset),
|
typingStatus: buildChatTypingStatus(update, serverTimeOffset),
|
||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateChannel) {
|
} else if (update instanceof GramJs.UpdateChannel) {
|
||||||
@ -907,6 +940,7 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'draftMessage',
|
'@type': 'draftMessage',
|
||||||
chatId: getApiChatIdFromMtpPeer(update.peer),
|
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||||
|
threadId: update.topMsgId,
|
||||||
...buildMessageDraft(update.draft),
|
...buildMessageDraft(update.draft),
|
||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateContactsReset) {
|
} else if (update instanceof GramJs.UpdateContactsReset) {
|
||||||
@ -1042,6 +1076,19 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
dispatchUserAndChatUpdates(entities);
|
dispatchUserAndChatUpdates(entities);
|
||||||
}
|
}
|
||||||
onUpdate({ '@type': 'updateConfig' });
|
onUpdate({ '@type': 'updateConfig' });
|
||||||
|
} else if (update instanceof GramJs.UpdateChannelPinnedTopic) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePinnedTopic',
|
||||||
|
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||||
|
topicId: update.topicId,
|
||||||
|
isPinned: Boolean(update.pinned),
|
||||||
|
});
|
||||||
|
} else if (update instanceof GramJs.UpdateChannelPinnedTopics) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updatePinnedTopicsOrder',
|
||||||
|
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||||
|
order: update.order || [],
|
||||||
|
});
|
||||||
} else if (DEBUG) {
|
} else if (DEBUG) {
|
||||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||||
log('UNEXPECTED UPDATE', params);
|
log('UNEXPECTED UPDATE', params);
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export interface ApiChat {
|
|||||||
id: string;
|
id: string;
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
type: ApiChatType;
|
type: ApiChatType;
|
||||||
title?: string;
|
title: string;
|
||||||
hasUnreadMark?: boolean;
|
hasUnreadMark?: boolean;
|
||||||
lastMessage?: ApiMessage;
|
lastMessage?: ApiMessage;
|
||||||
lastReadOutboxMessageId?: number;
|
lastReadOutboxMessageId?: number;
|
||||||
@ -39,6 +39,10 @@ export interface ApiChat {
|
|||||||
draftDate?: number;
|
draftDate?: number;
|
||||||
isProtected?: boolean;
|
isProtected?: boolean;
|
||||||
fakeType?: ApiFakeType;
|
fakeType?: ApiFakeType;
|
||||||
|
isForum?: boolean;
|
||||||
|
topics?: Record<number, ApiTopic>;
|
||||||
|
topicsCount?: number;
|
||||||
|
orderedPinnedTopicIds?: number[];
|
||||||
|
|
||||||
// Calls
|
// Calls
|
||||||
isCallActive?: boolean;
|
isCallActive?: boolean;
|
||||||
@ -64,8 +68,6 @@ export interface ApiChat {
|
|||||||
settings?: ApiChatSettings;
|
settings?: ApiChatSettings;
|
||||||
// Obtained from GetFullChat / GetFullChannel
|
// Obtained from GetFullChat / GetFullChannel
|
||||||
fullInfo?: ApiChatFullInfo;
|
fullInfo?: ApiChatFullInfo;
|
||||||
// Obtained with UpdateUserTyping or UpdateChatUserTyping updates
|
|
||||||
typingStatus?: ApiTypingStatus;
|
|
||||||
|
|
||||||
joinRequests?: ApiChatInviteImporter[];
|
joinRequests?: ApiChatInviteImporter[];
|
||||||
isJoinToSend?: boolean;
|
isJoinToSend?: boolean;
|
||||||
@ -137,6 +139,7 @@ export interface ApiChatAdminRights {
|
|||||||
addAdmins?: true;
|
addAdmins?: true;
|
||||||
anonymous?: true;
|
anonymous?: true;
|
||||||
manageCall?: true;
|
manageCall?: true;
|
||||||
|
manageTopics?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiChatBannedRights {
|
export interface ApiChatBannedRights {
|
||||||
@ -153,6 +156,7 @@ export interface ApiChatBannedRights {
|
|||||||
inviteUsers?: true;
|
inviteUsers?: true;
|
||||||
pinMessages?: true;
|
pinMessages?: true;
|
||||||
untilDate?: number;
|
untilDate?: number;
|
||||||
|
manageTopics?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiRestrictionReason {
|
export interface ApiRestrictionReason {
|
||||||
@ -189,3 +193,25 @@ export interface ApiSendAsPeerId {
|
|||||||
id: string;
|
id: string;
|
||||||
isPremium?: boolean;
|
isPremium?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiTopic {
|
||||||
|
id: number;
|
||||||
|
isClosed?: boolean;
|
||||||
|
isPinned?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
isOwner?: boolean;
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
// TODO[forums] https://github.com/telegramdesktop/tdesktop/blob/1aece79a471d99a8b63d826b1bce1f36a04d7293/Telegram/SourceFiles/data/data_forum_topic.cpp#L318
|
||||||
|
isMin?: boolean;
|
||||||
|
date: number;
|
||||||
|
title: string;
|
||||||
|
iconColor: number;
|
||||||
|
iconEmojiId?: string;
|
||||||
|
lastMessageId: number;
|
||||||
|
unreadCount: number;
|
||||||
|
unreadMentionsCount: number;
|
||||||
|
unreadReactionsCount: number;
|
||||||
|
fromId: string;
|
||||||
|
|
||||||
|
isMuted?: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@ -259,7 +259,7 @@ export interface ApiAction {
|
|||||||
text: string;
|
text: string;
|
||||||
targetUserIds?: string[];
|
targetUserIds?: string[];
|
||||||
targetChatId?: string;
|
targetChatId?: string;
|
||||||
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'other';
|
type: 'historyClear' | 'contactSignUp' | 'chatCreate' | 'topicCreate' | 'other';
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
amount?: number;
|
amount?: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
@ -390,6 +390,7 @@ export interface ApiMessage {
|
|||||||
replyToChatId?: string;
|
replyToChatId?: string;
|
||||||
replyToMessageId?: number;
|
replyToMessageId?: number;
|
||||||
replyToTopMessageId?: number;
|
replyToTopMessageId?: number;
|
||||||
|
isTopicReply?: true;
|
||||||
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
|
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
|
||||||
forwardInfo?: ApiMessageForwardInfo;
|
forwardInfo?: ApiMessageForwardInfo;
|
||||||
isDeleting?: boolean;
|
isDeleting?: boolean;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export interface ApiInitialArgs {
|
|||||||
maxBufferSize?: number;
|
maxBufferSize?: number;
|
||||||
webAuthToken?: string;
|
webAuthToken?: string;
|
||||||
dcId?: number;
|
dcId?: number;
|
||||||
|
mockScenario?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiOnProgress {
|
export interface ApiOnProgress {
|
||||||
@ -174,6 +175,7 @@ export interface ApiAppConfig {
|
|||||||
premiumPromoOrder: string[];
|
premiumPromoOrder: string[];
|
||||||
defaultEmojiStatusesStickerSetId: string;
|
defaultEmojiStatusesStickerSetId: string;
|
||||||
maxUniqueReactions: number;
|
maxUniqueReactions: number;
|
||||||
|
topicsPinnedLimit: number;
|
||||||
maxUserReactionsDefault: number;
|
maxUserReactionsDefault: number;
|
||||||
maxUserReactionsPremium: number;
|
maxUserReactionsPremium: number;
|
||||||
limits: Record<ApiLimitType, readonly [number, number]>;
|
limits: Record<ApiLimitType, readonly [number, number]>;
|
||||||
|
|||||||
@ -121,6 +121,7 @@ export type ApiUpdateChatInbox = {
|
|||||||
export type ApiUpdateChatTypingStatus = {
|
export type ApiUpdateChatTypingStatus = {
|
||||||
'@type': 'updateChatTypingStatus';
|
'@type': 'updateChatTypingStatus';
|
||||||
id: string;
|
id: string;
|
||||||
|
threadId?: number;
|
||||||
typingStatus: ApiTypingStatus | undefined;
|
typingStatus: ApiTypingStatus | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -184,7 +185,7 @@ export type ApiUpdateNewScheduledMessage = {
|
|||||||
'@type': 'newScheduledMessage';
|
'@type': 'newScheduledMessage';
|
||||||
chatId: string;
|
chatId: string;
|
||||||
id: number;
|
id: number;
|
||||||
message: Partial<ApiMessage>;
|
message: ApiMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ApiUpdateNewMessage = {
|
export type ApiUpdateNewMessage = {
|
||||||
@ -309,6 +310,7 @@ export type ApiUpdateResetMessages = {
|
|||||||
export type ApiUpdateDraftMessage = {
|
export type ApiUpdateDraftMessage = {
|
||||||
'@type': 'draftMessage';
|
'@type': 'draftMessage';
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
formattedText?: ApiFormattedText;
|
formattedText?: ApiFormattedText;
|
||||||
date?: number;
|
date?: number;
|
||||||
replyingToId?: number;
|
replyingToId?: number;
|
||||||
@ -433,6 +435,11 @@ export type ApiUpdateNotifyExceptions = {
|
|||||||
'@type': 'updateNotifyExceptions';
|
'@type': 'updateNotifyExceptions';
|
||||||
} & ApiNotifyException;
|
} & ApiNotifyException;
|
||||||
|
|
||||||
|
export type ApiUpdateTopicNotifyExceptions = {
|
||||||
|
'@type': 'updateTopicNotifyExceptions';
|
||||||
|
topicId: number;
|
||||||
|
} & ApiNotifyException;
|
||||||
|
|
||||||
export type ApiUpdateTwoFaStateWaitCode = {
|
export type ApiUpdateTwoFaStateWaitCode = {
|
||||||
'@type': 'updateTwoFaStateWaitCode';
|
'@type': 'updateTwoFaStateWaitCode';
|
||||||
length: number;
|
length: number;
|
||||||
@ -563,6 +570,30 @@ export type ApiUpdateTranscribedAudio = {
|
|||||||
isPending?: boolean;
|
isPending?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePinnedTopic = {
|
||||||
|
'@type': 'updatePinnedTopic';
|
||||||
|
topicId: number;
|
||||||
|
chatId: string;
|
||||||
|
isPinned: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdatePinnedTopicsOrder = {
|
||||||
|
'@type': 'updatePinnedTopicsOrder';
|
||||||
|
chatId: string;
|
||||||
|
order: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateTopic = {
|
||||||
|
'@type': 'updateTopic';
|
||||||
|
chatId: string;
|
||||||
|
topicId: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ApiUpdateTopics = {
|
||||||
|
'@type': 'updateTopics';
|
||||||
|
chatId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiUpdate = (
|
export type ApiUpdate = (
|
||||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed |
|
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed |
|
||||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||||
@ -587,7 +618,8 @@ export type ApiUpdate = (
|
|||||||
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
|
ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted |
|
||||||
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState |
|
||||||
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus |
|
||||||
ApiUpdateMessageExtendedMedia | ApiUpdateConfig
|
ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic |
|
||||||
|
ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics
|
||||||
);
|
);
|
||||||
|
|
||||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||||
|
|||||||
1
src/assets/icons/forumTopic/blue.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#4BB7FF"/><stop offset="100%" stop-color="#015EC1"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#0888DF"/><stop offset="100%" stop-color="#0042AC"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.947" d="M42 4.474c10.654 0 20.303 3.968 27.286 10.398 6.937 6.387 11.24 15.208 11.24 24.958 0 9.751-4.303 18.572-11.24 24.96C62.303 71.22 52.654 75.186 42 75.186a41.582 41.582 0 0 1-13.414-2.202c-.191.122-.386.244-.584.368-1.822 1.138-3.425 2.065-4.812 2.78-3.227 1.664-7.306 2.762-12.221 3.324l-.636.07c.515-.989.983-1.9 1.403-2.737l.35-.699c1.069-2.154 1.789-3.757 2.143-4.842.586-1.799.88-3.579 1.007-5.216.019-.24.034-.485.045-.734a37.536 37.536 0 0 1-.518-.466c-6.966-6.39-11.29-15.23-11.29-25.003 0-9.75 4.304-18.57 11.24-24.958C21.698 8.442 31.347 4.474 42 4.474Z"/><path fill="#71D0FF" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/green.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#97E334"/><stop offset="100%" stop-color="#11B411"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#48AF18"/><stop offset="100%" stop-color="#05951A"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.842" d="M42 4.421c10.668 0 20.33 3.974 27.322 10.412C76.27 21.23 80.579 30.065 80.579 39.83c0 9.766-4.31 18.6-11.257 24.998C62.329 71.266 52.668 75.24 42 75.24a41.635 41.635 0 0 1-13.407-2.197l-.563.355c-1.823 1.139-3.428 2.067-4.816 2.782-3.231 1.666-7.317 2.767-12.24 3.33l-.636.07c2.004-4.03 3.32-6.75 3.84-8.348.586-1.794.88-3.57 1.006-5.203.018-.232.033-.467.044-.706a37.29 37.29 0 0 1-.5-.451C7.75 58.472 3.42 49.619 3.42 39.83c0-9.765 4.31-18.6 11.257-24.997C21.671 8.395 31.332 4.42 42 4.42Z"/><path fill="#C2FF71" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/grey.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#A5A5A5"/><stop offset="100%" stop-color="#616161"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="99.396%"><stop offset="0%" stop-color="#737373"/><stop offset="100%" stop-color="#565656"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.947" d="M42 4.474c10.654 0 20.303 3.968 27.286 10.398 6.937 6.387 11.24 15.208 11.24 24.958 0 9.751-4.303 18.572-11.24 24.96C62.303 71.22 52.654 75.186 42 75.186a41.582 41.582 0 0 1-13.414-2.202c-.191.122-.386.244-.584.368-1.822 1.138-3.425 2.065-4.812 2.78-3.227 1.664-7.306 2.762-12.221 3.324l-.636.07c.515-.989.983-1.9 1.403-2.737l.35-.699c1.069-2.154 1.789-3.757 2.143-4.842.586-1.799.88-3.579 1.007-5.216.019-.24.034-.485.045-.734a37.536 37.536 0 0 1-.518-.466c-6.966-6.39-11.29-15.23-11.29-25.003 0-9.75 4.304-18.57 11.24-24.958C21.698 8.442 31.347 4.474 42 4.474Z"/><path fill="#B8B8B8" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/red.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FF714C"/><stop offset="100%" stop-color="#C61505"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="98.606%"><stop offset="0%" stop-color="#E12F1F"/><stop offset="100%" stop-color="#B40101"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.947" d="M42 4.474c10.654 0 20.303 3.968 27.286 10.398 6.937 6.387 11.24 15.208 11.24 24.958 0 9.751-4.303 18.572-11.24 24.96C62.303 71.22 52.654 75.186 42 75.186a41.582 41.582 0 0 1-13.414-2.202c-.191.122-.386.244-.584.368-1.822 1.138-3.425 2.065-4.812 2.78-3.227 1.664-7.306 2.762-12.221 3.324l-.636.07c.515-.989.983-1.9 1.403-2.737l.35-.699c1.069-2.154 1.789-3.757 2.143-4.842.586-1.799.88-3.579 1.007-5.216.019-.24.034-.485.045-.734a37.536 37.536 0 0 1-.518-.466c-6.966-6.39-11.29-15.23-11.29-25.003 0-9.75 4.304-18.57 11.24-24.958C21.698 8.442 31.347 4.474 42 4.474Z"/><path fill="#FFB47D" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/rose.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="4.314%" y2="99.602%"><stop offset="0%" stop-color="#FF7999"/><stop offset="98.597%" stop-color="#E4215A"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="96.402%"><stop offset="0%" stop-color="#F83B72"/><stop offset="100%" stop-color="#BA0940"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.842" d="M42 4.421c10.668 0 20.33 3.974 27.322 10.412C76.27 21.23 80.579 30.065 80.579 39.83c0 9.766-4.31 18.6-11.257 24.998C62.329 71.266 52.668 75.24 42 75.24a41.635 41.635 0 0 1-13.407-2.197l-.563.355c-1.823 1.139-3.428 2.067-4.816 2.782-3.231 1.666-7.317 2.767-12.24 3.33l-.636.07c2.004-4.03 3.32-6.75 3.84-8.348.586-1.794.88-3.57 1.006-5.203.018-.232.033-.467.044-.706a37.29 37.29 0 0 1-.5-.451C7.75 58.472 3.42 49.619 3.42 39.83c0-9.765 4.31-18.6 11.257-24.997C21.671 8.395 31.332 4.42 42 4.42Z"/><path fill="#FFC7D6" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/violet.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="99.764%"><stop offset="0%" stop-color="#E57AFF"/><stop offset="99.855%" stop-color="#A438BB"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="99.396%"><stop offset="0%" stop-color="#B239D1"/><stop offset="100%" stop-color="#7C279A"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.842" d="M42 4.421c10.668 0 20.33 3.974 27.322 10.412C76.27 21.23 80.579 30.065 80.579 39.83c0 9.766-4.31 18.6-11.257 24.998C62.329 71.266 52.668 75.24 42 75.24a41.635 41.635 0 0 1-13.407-2.197l-.563.355c-1.823 1.139-3.428 2.067-4.816 2.782-3.231 1.666-7.317 2.767-12.24 3.33l-.636.07c2.004-4.03 3.32-6.75 3.84-8.348.586-1.794.88-3.57 1.006-5.203.018-.232.033-.467.044-.706a37.29 37.29 0 0 1-.5-.451C7.75 58.472 3.42 49.619 3.42 39.83c0-9.765 4.31-18.6 11.257-24.997C21.671 8.395 31.332 4.42 42 4.42Z"/><path fill="#F5BDFF" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
1
src/assets/icons/forumTopic/yellow.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><defs><linearGradient id="a" x1="50%" x2="50%" y1="0%" y2="100%"><stop offset="0%" stop-color="#FFDB5C"/><stop offset="100%" stop-color="#EA5800"/></linearGradient><linearGradient id="b" x1="50%" x2="50%" y1="0%" y2="99.014%"><stop offset="0%" stop-color="#F2A807"/><stop offset="100%" stop-color="#D93A00"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><path fill="url(#a)" stroke="url(#b)" stroke-width="2.842" d="M42 4.421c10.668 0 20.33 3.974 27.322 10.412C76.27 21.23 80.579 30.065 80.579 39.83c0 9.766-4.31 18.6-11.257 24.998C62.329 71.266 52.668 75.24 42 75.24a41.635 41.635 0 0 1-13.407-2.197l-.563.355c-1.823 1.139-3.428 2.067-4.816 2.782-3.231 1.666-7.317 2.767-12.24 3.33l-.636.07c2.004-4.03 3.32-6.75 3.84-8.348.586-1.794.88-3.57 1.006-5.203.018-.232.033-.467.044-.706a37.29 37.29 0 0 1-.5-.451C7.75 58.472 3.42 49.619 3.42 39.83c0-9.765 4.31-18.6 11.257-24.997C21.671 8.395 31.332 4.42 42 4.42Z"/><path fill="#F9FF71" d="M9.68 24.614a.501.501 0 0 0 .73-.006C20.518 13.728 31.314 8.501 42.8 8.93c11.46.427 22.125 6.44 31.995 18.037a.693.693 0 0 0 .941.106.705.705 0 0 0 .174-.944C67.782 13.338 56.745 6.691 42.8 6.188 28.822 5.686 17.759 11.55 9.607 23.78a.669.669 0 0 0 .074.834Z" opacity=".375"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@ -20,6 +20,7 @@ import './GroupCallTopPane.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
hasPinnedOffset: boolean;
|
hasPinnedOffset: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -33,6 +34,7 @@ type StateProps = {
|
|||||||
const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
isActive,
|
isActive,
|
||||||
|
className,
|
||||||
groupCall,
|
groupCall,
|
||||||
hasPinnedOffset,
|
hasPinnedOffset,
|
||||||
usersById,
|
usersById,
|
||||||
@ -97,6 +99,7 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
|||||||
'GroupCallTopPane',
|
'GroupCallTopPane',
|
||||||
hasPinnedOffset && 'has-pinned-offset',
|
hasPinnedOffset && 'has-pinned-offset',
|
||||||
!isActive && 'is-hidden',
|
!isActive && 'is-hidden',
|
||||||
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={handleJoinGroupCall}
|
onClick={handleJoinGroupCall}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -6,6 +6,10 @@ $animation-time: 0.15s;
|
|||||||
.root {
|
.root {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
white-space: pre;
|
white-space: pre;
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.character-container {
|
.character-container {
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { ANIMATION_LEVEL_MAX } from '../../config';
|
|||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||||
import useTimeout from '../../hooks/useTimeout';
|
import useTimeout from '../../hooks/useTimeout';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import styles from './AnimatedCounter.module.scss';
|
import styles from './AnimatedCounter.module.scss';
|
||||||
|
|
||||||
@ -18,6 +19,8 @@ const ANIMATION_TIME = 150;
|
|||||||
const AnimatedCounter: FC<OwnProps> = ({
|
const AnimatedCounter: FC<OwnProps> = ({
|
||||||
text,
|
text,
|
||||||
}) => {
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
const prevText = usePrevious(text);
|
const prevText = usePrevious(text);
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
|
|
||||||
@ -54,7 +57,7 @@ const AnimatedCounter: FC<OwnProps> = ({
|
|||||||
}, shouldAnimate && isAnimatingRef.current ? ANIMATION_TIME : undefined);
|
}, shouldAnimate && isAnimatingRef.current ? ANIMATION_TIME : undefined);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={styles.root}>
|
<span className={styles.root} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{textElement}
|
{textElement}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
.Avatar {
|
.Avatar {
|
||||||
--color-user: var(--color-primary);
|
--color-user: var(--color-primary);
|
||||||
|
--radius: 50%;
|
||||||
flex: none;
|
flex: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 3.375rem;
|
width: 3.375rem;
|
||||||
height: 3.375rem;
|
height: 3.375rem;
|
||||||
border-radius: 50%;
|
border-radius: var(--radius);
|
||||||
background: linear-gradient(var(--color-white) -125%, var(--color-user));
|
background: linear-gradient(var(--color-white) -125%, var(--color-user));
|
||||||
color: white;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -15,7 +16,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&__media {
|
&__media {
|
||||||
border-radius: 50%;
|
border-radius: var(--radius);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
@ -26,11 +27,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
font-size: 2.5rem;
|
font-size: 1.25rem;
|
||||||
|
|
||||||
&.icon-reply-filled {
|
|
||||||
transform: scale(0.7);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.size-micro {
|
&.size-micro {
|
||||||
@ -81,7 +78,7 @@
|
|||||||
font-size: 1.3125rem;
|
font-size: 1.3125rem;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
font-size: 3.5rem;
|
font-size: 1.625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
.emoji {
|
||||||
@ -131,4 +128,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.forum {
|
||||||
|
--radius: var(--border-radius-forum-avatar);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,6 +83,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
const isDeleted = user && isDeletedUser(user);
|
const isDeleted = user && isDeletedUser(user);
|
||||||
const isReplies = user && isChatWithRepliesBot(user.id);
|
const isReplies = user && isChatWithRepliesBot(user.id);
|
||||||
|
const isForum = chat?.isForum;
|
||||||
let imageHash: string | undefined;
|
let imageHash: string | undefined;
|
||||||
let videoHash: string | undefined;
|
let videoHash: string | undefined;
|
||||||
|
|
||||||
@ -184,6 +185,7 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
isSavedMessages && 'saved-messages',
|
isSavedMessages && 'saved-messages',
|
||||||
isDeleted && 'deleted-account',
|
isDeleted && 'deleted-account',
|
||||||
isReplies && 'replies-bot-account',
|
isReplies && 'replies-bot-account',
|
||||||
|
isForum && 'forum',
|
||||||
isOnline && 'online',
|
isOnline && 'online',
|
||||||
onClick && 'interactive',
|
onClick && 'interactive',
|
||||||
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
(!isSavedMessages && !imgBlobUrl) && 'no-photo',
|
||||||
|
|||||||
@ -11,10 +11,17 @@ import type {
|
|||||||
|
|
||||||
import { TME_LINK_PREFIX } from '../../config';
|
import { TME_LINK_PREFIX } from '../../config';
|
||||||
import {
|
import {
|
||||||
selectChat, selectNotifyExceptions, selectNotifySettings, selectUser,
|
selectChat, selectCurrentMessageList, selectNotifyExceptions, selectNotifySettings, selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import {
|
import {
|
||||||
getChatDescription, getChatLink, getHasAdminRight, isChatChannel, isUserId, isUserRightBanned, selectIsChatMuted,
|
getChatDescription,
|
||||||
|
getChatLink,
|
||||||
|
getTopicLink,
|
||||||
|
getHasAdminRight,
|
||||||
|
isChatChannel,
|
||||||
|
isUserId,
|
||||||
|
isUserRightBanned,
|
||||||
|
selectIsChatMuted,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
import { copyTextToClipboard } from '../../util/clipboard';
|
import { copyTextToClipboard } from '../../util/clipboard';
|
||||||
@ -38,6 +45,8 @@ type StateProps =
|
|||||||
canInviteUsers?: boolean;
|
canInviteUsers?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
phoneCodeList: ApiCountryCode[];
|
phoneCodeList: ApiCountryCode[];
|
||||||
|
isForum?: boolean;
|
||||||
|
topicId?: number;
|
||||||
}
|
}
|
||||||
& Pick<GlobalState, 'lastSyncTime'>;
|
& Pick<GlobalState, 'lastSyncTime'>;
|
||||||
|
|
||||||
@ -51,11 +60,14 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
canInviteUsers,
|
canInviteUsers,
|
||||||
isMuted,
|
isMuted,
|
||||||
phoneCodeList,
|
phoneCodeList,
|
||||||
|
isForum,
|
||||||
|
topicId,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadFullUser,
|
loadFullUser,
|
||||||
showNotification,
|
showNotification,
|
||||||
updateChatMutedState,
|
updateChatMutedState,
|
||||||
|
updateTopicMutedState,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -84,19 +96,35 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return result?.length ? result : undefined;
|
return result?.length ? result : undefined;
|
||||||
}, [chatUsernames, user]);
|
}, [chatUsernames, user]);
|
||||||
const link = useMemo(() => (chat ? getChatLink(chat) : undefined), [chat]);
|
const link = useMemo(() => {
|
||||||
|
if (!chat) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isForum
|
||||||
|
? getTopicLink(chat.id, activeChatUsernames?.[0].username, topicId)
|
||||||
|
: getChatLink(chat);
|
||||||
|
}, [chat, isForum, activeChatUsernames, topicId]);
|
||||||
|
|
||||||
const handleNotificationChange = useCallback(() => {
|
const handleNotificationChange = useCallback(() => {
|
||||||
setAreNotificationsEnabled((current) => {
|
setAreNotificationsEnabled((current) => {
|
||||||
const newAreNotificationsEnabled = !current;
|
const newAreNotificationsEnabled = !current;
|
||||||
|
|
||||||
runDebounced(() => {
|
runDebounced(() => {
|
||||||
updateChatMutedState({ chatId, isMuted: !newAreNotificationsEnabled });
|
if (topicId) {
|
||||||
|
updateTopicMutedState({
|
||||||
|
chatId: chatId!,
|
||||||
|
topicId,
|
||||||
|
isMuted: !newAreNotificationsEnabled,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateChatMutedState({ chatId, isMuted: !newAreNotificationsEnabled });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return newAreNotificationsEnabled;
|
return newAreNotificationsEnabled;
|
||||||
});
|
});
|
||||||
}, [chatId, updateChatMutedState]);
|
}, [chatId, topicId, updateChatMutedState, updateTopicMutedState]);
|
||||||
|
|
||||||
if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) {
|
if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -112,6 +140,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) {
|
function renderUsernames(usernameList: ApiUsername[], isChat?: boolean) {
|
||||||
const [mainUsername, ...otherUsernames] = usernameList;
|
const [mainUsername, ...otherUsernames] = usernameList;
|
||||||
|
|
||||||
const usernameLinks = otherUsernames.length
|
const usernameLinks = otherUsernames.length
|
||||||
? (lang('UsernameAlso', '%USERNAMES%') as string)
|
? (lang('UsernameAlso', '%USERNAMES%') as string)
|
||||||
.split('%')
|
.split('%')
|
||||||
@ -139,6 +168,10 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
})
|
})
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const publicLink = isForum
|
||||||
|
? getTopicLink('', mainUsername.username, topicId)
|
||||||
|
: `@${mainUsername.username}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
icon="mention"
|
icon="mention"
|
||||||
@ -146,9 +179,9 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
|
|||||||
narrow
|
narrow
|
||||||
ripple
|
ripple
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => copy(`@${mainUsername.username}`, lang(isChat ? 'Link' : 'Username'))}
|
onClick={() => copy(publicLink, lang(isChat ? 'Link' : 'Username'))}
|
||||||
>
|
>
|
||||||
<span className="title" dir="auto">{renderText(mainUsername.username)}</span>
|
<span className="title" dir="auto">{publicLink}</span>
|
||||||
<span className="subtitle">
|
<span className="subtitle">
|
||||||
{usernameLinks && <span className="other-usernames">{usernameLinks}</span>}
|
{usernameLinks && <span className="other-usernames">{usernameLinks}</span>}
|
||||||
{lang(isChat ? 'Link' : 'Username')}
|
{lang(isChat ? 'Link' : 'Username')}
|
||||||
@ -215,7 +248,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
|
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
|
||||||
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
|
const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined;
|
||||||
|
const isForum = chat?.isForum;
|
||||||
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
|
||||||
|
const { threadId } = selectCurrentMessageList(global) || {};
|
||||||
|
const topicId = isForum ? threadId : undefined;
|
||||||
|
|
||||||
const canInviteUsers = chat && !user && (
|
const canInviteUsers = chat && !user && (
|
||||||
(!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers'))
|
(!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers'))
|
||||||
@ -223,7 +259,14 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastSyncTime, phoneCodeList, chat, user, canInviteUsers, isMuted,
|
lastSyncTime,
|
||||||
|
phoneCodeList,
|
||||||
|
chat,
|
||||||
|
user,
|
||||||
|
canInviteUsers,
|
||||||
|
isMuted,
|
||||||
|
isForum,
|
||||||
|
topicId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ChatExtra));
|
)(ChatExtra));
|
||||||
|
|||||||
158
src/components/common/ChatForumLastMessage.module.scss
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
$radius: 0.5rem;
|
||||||
|
|
||||||
|
.root {
|
||||||
|
--first-column-background-color: var(--color-item-active);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
z-index: 3;
|
||||||
|
|
||||||
|
transition: 0.25s ease-out background-color;
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
--first-column-background-color: var(--color-borders);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-column, .main-column {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.unread {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--color-text);
|
||||||
|
|
||||||
|
&.main-column, &.last-message {
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.main-column, &.last-message, .after-wrapper {
|
||||||
|
transition: background-color 0.15s ease-in-out;
|
||||||
|
background: var(--first-column-background-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-column {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-column {
|
||||||
|
border-start-start-radius: $radius;
|
||||||
|
border-start-end-radius: $radius;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
pointer-events: initial;
|
||||||
|
|
||||||
|
border-end-end-radius: $radius;
|
||||||
|
|
||||||
|
.after-wrapper {
|
||||||
|
width: $radius;
|
||||||
|
height: $radius;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: -$radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after {
|
||||||
|
border-end-start-radius: $radius;
|
||||||
|
background: var(--background-color);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-column-title {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-columns {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ellipsis {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message {
|
||||||
|
border-end-start-radius: $radius;
|
||||||
|
border-end-end-radius: $radius;
|
||||||
|
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
pointer-events: initial;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.after-wrapper {
|
||||||
|
width: $radius;
|
||||||
|
height: $radius;
|
||||||
|
top: 0;
|
||||||
|
position: absolute;
|
||||||
|
inset-inline-end: -$radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.after {
|
||||||
|
border-start-start-radius: $radius;
|
||||||
|
background: var(--background-color);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.reverse-corner {
|
||||||
|
.main-column {
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message {
|
||||||
|
border-start-end-radius: $radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.overwritten-width {
|
||||||
|
.last-message, .main-column {
|
||||||
|
min-width: var(--overwritten-width);
|
||||||
|
}
|
||||||
|
|
||||||
|
.last-message {
|
||||||
|
border-start-end-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-column {
|
||||||
|
border-end-end-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
src/components/common/ChatForumLastMessage.tsx
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
import React, {
|
||||||
|
memo,
|
||||||
|
useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ApiChat } from '../../api/types';
|
||||||
|
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import { getOrderedTopics } from '../../global/helpers';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import TopicIcon from './TopicIcon';
|
||||||
|
|
||||||
|
import styles from './ChatForumLastMessage.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chat: ApiChat;
|
||||||
|
renderLastMessage: () => React.ReactNode;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NO_CORNER_THRESHOLD = Number(REM);
|
||||||
|
|
||||||
|
const ChatForumLastMessage: FC<OwnProps> = ({
|
||||||
|
chat,
|
||||||
|
renderLastMessage,
|
||||||
|
observeIntersection,
|
||||||
|
}) => {
|
||||||
|
const { openChat } = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const lastMessageRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const mainColumnRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const lastMessage = renderLastMessage();
|
||||||
|
|
||||||
|
const [lastActiveTopic, ...otherTopics] = useMemo(() => {
|
||||||
|
return chat.topics ? getOrderedTopics(Object.values(chat.topics), undefined, true) : [];
|
||||||
|
}, [chat.topics]);
|
||||||
|
|
||||||
|
const [isReversedCorner, setIsReversedCorner] = useState(false);
|
||||||
|
const [overwrittenWidth, setOverwrittenWidth] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
function handleOpenTopic(e: React.MouseEvent<HTMLDivElement>) {
|
||||||
|
if (lastActiveTopic.unreadCount === 0) return;
|
||||||
|
e.stopPropagation();
|
||||||
|
openChat({ id: chat.id, threadId: lastActiveTopic.id, shouldReplaceHistory: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const lastMessageElement = lastMessageRef.current;
|
||||||
|
const mainColumnElement = mainColumnRef.current;
|
||||||
|
if (!lastMessageElement || !mainColumnElement) return;
|
||||||
|
|
||||||
|
const lastMessageWidth = lastMessageElement.offsetWidth;
|
||||||
|
const mainColumnWidth = mainColumnElement.offsetWidth;
|
||||||
|
|
||||||
|
if (Math.abs(lastMessageWidth - mainColumnWidth) < NO_CORNER_THRESHOLD) {
|
||||||
|
setOverwrittenWidth(Math.max(lastMessageWidth, mainColumnWidth));
|
||||||
|
} else {
|
||||||
|
setOverwrittenWidth(undefined);
|
||||||
|
}
|
||||||
|
setIsReversedCorner(lastMessageWidth > mainColumnWidth);
|
||||||
|
}, [lastActiveTopic, lastMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
isReversedCorner && styles.reverseCorner,
|
||||||
|
overwrittenWidth && styles.overwrittenWidth,
|
||||||
|
)}
|
||||||
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
|
style={overwrittenWidth ? `--overwritten-width: ${overwrittenWidth}px` : undefined}
|
||||||
|
>
|
||||||
|
{lastActiveTopic && (
|
||||||
|
<span className={styles.titleRow}>
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.mainColumn,
|
||||||
|
lastActiveTopic.unreadCount && styles.unread,
|
||||||
|
)}
|
||||||
|
ref={mainColumnRef}
|
||||||
|
onMouseDown={handleOpenTopic}
|
||||||
|
>
|
||||||
|
<TopicIcon
|
||||||
|
topic={lastActiveTopic}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
/>
|
||||||
|
<div className={styles.title}>{renderText(lastActiveTopic.title)}</div>
|
||||||
|
{!overwrittenWidth && isReversedCorner && (
|
||||||
|
<div className={styles.afterWrapper}>
|
||||||
|
<div className={styles.after} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.otherColumns}>
|
||||||
|
{otherTopics.map((topic) => (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.otherColumn, topic.unreadCount && styles.unread,
|
||||||
|
)}
|
||||||
|
key={topic.id}
|
||||||
|
>
|
||||||
|
<TopicIcon
|
||||||
|
topic={topic}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
/>
|
||||||
|
<span className={styles.otherColumnTitle}>{renderText(topic.title)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.ellipsis} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{!lastActiveTopic && <div className={buildClassName(styles.titleRow, styles.loading)}>{lang('Loading')}</div>}
|
||||||
|
<span
|
||||||
|
className={buildClassName(styles.lastMessage, lastActiveTopic?.unreadCount && styles.unread)}
|
||||||
|
ref={lastMessageRef}
|
||||||
|
onMouseDown={handleOpenTopic}
|
||||||
|
>
|
||||||
|
{lastMessage}
|
||||||
|
{!overwrittenWidth && !isReversedCorner && (
|
||||||
|
<div className={styles.afterWrapper}>
|
||||||
|
<div className={styles.after} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(ChatForumLastMessage);
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||||
import React, { useCallback } from '../../lib/teact/teact';
|
|
||||||
import { getActions } from '../../global';
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import Link from '../ui/Link';
|
import Link from '../ui/Link';
|
||||||
@ -32,4 +33,4 @@ const ChatLink: FC<OwnProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ChatLink;
|
export default memo(ChatLink);
|
||||||
|
|||||||
@ -42,6 +42,11 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
> .Transition {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.picker-list {
|
.picker-list {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@ -85,4 +90,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-icon {
|
||||||
|
--custom-emoji-size: 2.75rem;
|
||||||
|
|
||||||
|
margin-inline-end: 0.25rem !important;
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
font-size: 2.75rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-icon-letter {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-item {
|
||||||
|
.ListItem-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullName {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.emoji-small {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { memo, useRef, useCallback } from '../../lib/teact/teact';
|
import React, {
|
||||||
|
memo, useRef, useCallback, useState, useMemo,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiTopic } from '../../api/types';
|
||||||
|
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
import { CHAT_HEIGHT_PX } from '../../config';
|
import { CHAT_HEIGHT_PX } from '../../config';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
import { getCanPostInChat, isUserId } from '../../global/helpers';
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||||
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
||||||
import { isUserId } from '../../global/helpers';
|
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
@ -16,26 +23,34 @@ import InfiniteScroll from '../ui/InfiniteScroll';
|
|||||||
import ListItem from '../ui/ListItem';
|
import ListItem from '../ui/ListItem';
|
||||||
import GroupChatInfo from './GroupChatInfo';
|
import GroupChatInfo from './GroupChatInfo';
|
||||||
import PrivateChatInfo from './PrivateChatInfo';
|
import PrivateChatInfo from './PrivateChatInfo';
|
||||||
|
import Transition from '../ui/Transition';
|
||||||
|
import TopicIcon from './TopicIcon';
|
||||||
|
|
||||||
import './ChatOrUserPicker.scss';
|
import './ChatOrUserPicker.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
chatOrUserIds: string[];
|
chatOrUserIds: string[];
|
||||||
|
chatsById?: Record<string, ApiChat>;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
search: string;
|
search: string;
|
||||||
loadMore?: NoneToVoidFunction;
|
loadMore?: NoneToVoidFunction;
|
||||||
onSearchChange: (search: string) => void;
|
onSearchChange: (search: string) => void;
|
||||||
onSelectChatOrUser: (chatOrUserId: string) => void;
|
onSelectChatOrUser: (chatOrUserId: string, threadId?: number) => void;
|
||||||
onClose: NoneToVoidFunction;
|
onClose: NoneToVoidFunction;
|
||||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CHAT_LIST_SLIDE = 0;
|
||||||
|
const TOPIC_LIST_SLIDE = 1;
|
||||||
|
const TOPIC_ICON_SIZE = 2.75 * REM;
|
||||||
|
|
||||||
const ChatOrUserPicker: FC<OwnProps> = ({
|
const ChatOrUserPicker: FC<OwnProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
chatOrUserIds,
|
chatOrUserIds,
|
||||||
|
chatsById,
|
||||||
search,
|
search,
|
||||||
searchPlaceholder,
|
searchPlaceholder,
|
||||||
loadMore,
|
loadMore,
|
||||||
@ -44,89 +59,218 @@ const ChatOrUserPicker: FC<OwnProps> = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { loadTopics } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(search));
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const topicContainerRef = useRef<HTMLDivElement>(null);
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const searchRef = useRef<HTMLInputElement>(null);
|
const searchRef = useRef<HTMLInputElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const topicSearchRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(search));
|
||||||
|
const [forumId, setForumId] = useState<string | undefined>(undefined);
|
||||||
|
const [topicSearch, setTopicSearch] = useState<string>('');
|
||||||
|
const activeKey = forumId ? TOPIC_LIST_SLIDE : CHAT_LIST_SLIDE;
|
||||||
|
const viewportOffset = chatOrUserIds!.indexOf(viewportIds![0]);
|
||||||
|
|
||||||
const resetSearch = useCallback(() => {
|
const resetSearch = useCallback(() => {
|
||||||
onSearchChange('');
|
onSearchChange('');
|
||||||
}, [onSearchChange]);
|
}, [onSearchChange]);
|
||||||
useInputFocusOnOpen(searchRef, isOpen, resetSearch);
|
useInputFocusOnOpen(searchRef, isOpen && activeKey === CHAT_LIST_SLIDE, resetSearch);
|
||||||
|
useInputFocusOnOpen(topicSearchRef, isOpen && activeKey === TOPIC_LIST_SLIDE);
|
||||||
|
|
||||||
|
const [topicIds, topics] = useMemo(() => {
|
||||||
|
const topicsResult = forumId ? chatsById?.[forumId].topics : undefined;
|
||||||
|
if (!topicsResult) {
|
||||||
|
return [undefined, undefined];
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTitle = topicSearch.toLowerCase();
|
||||||
|
|
||||||
|
const result = topicsResult
|
||||||
|
? Object.values(topicsResult).reduce((acc, topic) => {
|
||||||
|
if (
|
||||||
|
getCanPostInChat(chatsById![forumId!], topic.id)
|
||||||
|
&& (!searchTitle || topic.title.toLowerCase().includes(searchTitle))
|
||||||
|
) {
|
||||||
|
acc[topic.id] = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, ApiTopic>)
|
||||||
|
: topicsResult;
|
||||||
|
|
||||||
|
return [Object.keys(result).map(Number), result];
|
||||||
|
}, [chatsById, forumId, topicSearch]);
|
||||||
|
|
||||||
|
const handleHeaderBackClick = useCallback(() => {
|
||||||
|
setForumId(undefined);
|
||||||
|
setTopicSearch('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onSearchChange(e.currentTarget.value);
|
onSearchChange(e.currentTarget.value);
|
||||||
}, [onSearchChange]);
|
}, [onSearchChange]);
|
||||||
|
|
||||||
|
const handleTopicSearchChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTopicSearch(e.currentTarget.value);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
||||||
if (viewportIds && viewportIds.length > 0) {
|
if (viewportIds && viewportIds.length > 0) {
|
||||||
onSelectChatOrUser(viewportIds[index === -1 ? 0 : index]);
|
const chatId = viewportIds[index === -1 ? 0 : index];
|
||||||
|
const chat = chatsById?.[chatId];
|
||||||
|
if (chat?.isForum) {
|
||||||
|
if (!chat.topics) loadTopics({ chatId });
|
||||||
|
setForumId(chatId);
|
||||||
|
} else {
|
||||||
|
onSelectChatOrUser(chatId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, '.ListItem-button', true);
|
}, '.ListItem-button', true);
|
||||||
|
|
||||||
const modalHeader = (
|
const handleTopicKeyDown = useKeyboardListNavigation(topicContainerRef, isOpen, (index) => {
|
||||||
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
if (topicIds?.length) {
|
||||||
<Button
|
onSelectChatOrUser(forumId!, topicIds[index === -1 ? 0 : index]);
|
||||||
round
|
}
|
||||||
color="translucent"
|
}, '.ListItem-button', true);
|
||||||
size="smaller"
|
|
||||||
ariaLabel={lang('Close')}
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
<i className="icon-close" />
|
|
||||||
</Button>
|
|
||||||
<InputText
|
|
||||||
ref={searchRef}
|
|
||||||
value={search}
|
|
||||||
onChange={handleSearchChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const viewportOffset = chatOrUserIds!.indexOf(viewportIds![0]);
|
const handleClick = useCallback((e: React.MouseEvent, chatId: string) => {
|
||||||
|
const chat = chatsById?.[chatId];
|
||||||
|
if (chat?.isForum) {
|
||||||
|
if (!chat.topics) loadTopics({ chatId });
|
||||||
|
setForumId(chatId);
|
||||||
|
resetSearch();
|
||||||
|
} else {
|
||||||
|
onSelectChatOrUser(chatId);
|
||||||
|
}
|
||||||
|
}, [chatsById, loadTopics, onSelectChatOrUser, resetSearch]);
|
||||||
|
|
||||||
|
const handleTopicClick = useCallback((e: React.MouseEvent, topicId: number) => {
|
||||||
|
onSelectChatOrUser(forumId!, topicId);
|
||||||
|
}, [forumId, onSelectChatOrUser]);
|
||||||
|
|
||||||
|
function renderTopicList() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
<Button round color="translucent" size="smaller" ariaLabel={lang('Back')} onClick={handleHeaderBackClick}>
|
||||||
|
<i className="icon-arrow-left" />
|
||||||
|
</Button>
|
||||||
|
<InputText
|
||||||
|
ref={topicSearchRef}
|
||||||
|
value={topicSearch}
|
||||||
|
onChange={handleTopicSearchChange}
|
||||||
|
onKeyDown={handleTopicKeyDown}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<InfiniteScroll
|
||||||
|
ref={topicContainerRef}
|
||||||
|
className="picker-list custom-scroll"
|
||||||
|
items={topicIds}
|
||||||
|
withAbsolutePositioning
|
||||||
|
maxHeight={!topicIds ? 0 : topicIds.length * CHAT_HEIGHT_PX}
|
||||||
|
onKeyDown={handleTopicKeyDown}
|
||||||
|
>
|
||||||
|
{topicIds
|
||||||
|
? topicIds.map((topicId, i) => (
|
||||||
|
<ListItem
|
||||||
|
key={`${forumId}_${topicId}`}
|
||||||
|
className="chat-item-clickable force-rounded-corners small-icon topic-item"
|
||||||
|
style={`top: ${i * CHAT_HEIGHT_PX}px;`}
|
||||||
|
onClick={handleTopicClick}
|
||||||
|
clickArg={topicId}
|
||||||
|
>
|
||||||
|
<TopicIcon
|
||||||
|
size={TOPIC_ICON_SIZE}
|
||||||
|
topic={topics[topicId]}
|
||||||
|
className="topic-icon"
|
||||||
|
letterClassName="topic-icon-letter"
|
||||||
|
/>
|
||||||
|
<div dir="auto" className="fullName">{renderText(topics[topicId].title)}</div>
|
||||||
|
</ListItem>
|
||||||
|
))
|
||||||
|
: <Loading />}
|
||||||
|
</InfiniteScroll>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChatList() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<i className="icon-close" />
|
||||||
|
</Button>
|
||||||
|
<InputText
|
||||||
|
ref={searchRef}
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearchChange}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{viewportIds?.length ? (
|
||||||
|
<InfiniteScroll
|
||||||
|
ref={containerRef}
|
||||||
|
className="picker-list custom-scroll"
|
||||||
|
items={viewportIds}
|
||||||
|
onLoadMore={getMore}
|
||||||
|
withAbsolutePositioning
|
||||||
|
maxHeight={chatOrUserIds!.length * CHAT_HEIGHT_PX}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{viewportIds.map((id, i) => (
|
||||||
|
<ListItem
|
||||||
|
key={id}
|
||||||
|
className="chat-item-clickable force-rounded-corners small-icon"
|
||||||
|
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
||||||
|
onClick={handleClick}
|
||||||
|
clickArg={id}
|
||||||
|
>
|
||||||
|
{isUserId(id) ? (
|
||||||
|
<PrivateChatInfo
|
||||||
|
status={id === currentUserId ? lang('SavedMessagesInfo') : undefined}
|
||||||
|
userId={id}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GroupChatInfo chatId={id} />
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
|
) : viewportIds && !viewportIds.length ? (
|
||||||
|
<p className="no-results">{lang('lng_blocked_list_not_found')}</p>
|
||||||
|
) : (
|
||||||
|
<Loading />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
className="ChatOrUserPicker"
|
className="ChatOrUserPicker"
|
||||||
header={modalHeader}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||||
>
|
>
|
||||||
{viewportIds?.length ? (
|
<Transition activeKey={activeKey} name="slide-fade">
|
||||||
<InfiniteScroll
|
{() => {
|
||||||
ref={containerRef}
|
return activeKey === TOPIC_LIST_SLIDE ? renderTopicList() : renderChatList();
|
||||||
className="picker-list custom-scroll"
|
}}
|
||||||
items={viewportIds}
|
</Transition>
|
||||||
onLoadMore={getMore}
|
|
||||||
withAbsolutePositioning
|
|
||||||
maxHeight={chatOrUserIds!.length * CHAT_HEIGHT_PX}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
{viewportIds.map((id, i) => (
|
|
||||||
<ListItem
|
|
||||||
key={id}
|
|
||||||
className="chat-item-clickable force-rounded-corners small-icon"
|
|
||||||
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => onSelectChatOrUser(id)}
|
|
||||||
>
|
|
||||||
{isUserId(id) ? (
|
|
||||||
<PrivateChatInfo status={id === currentUserId ? lang('SavedMessagesInfo') : undefined} userId={id} />
|
|
||||||
) : (
|
|
||||||
<GroupChatInfo chatId={id} />
|
|
||||||
)}
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</InfiniteScroll>
|
|
||||||
) : viewportIds && !viewportIds.length ? (
|
|
||||||
<p className="no-results">{lang('lng_blocked_list_not_found')}</p>
|
|
||||||
) : (
|
|
||||||
<Loading />
|
|
||||||
)}
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
width: var(--custom-emoji-size);
|
width: var(--custom-emoji-size);
|
||||||
height: var(--custom-emoji-size);
|
height: var(--custom-emoji-size);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
flex: 0 0 var(--custom-emoji-size);
|
||||||
|
|
||||||
&.with-grid-fix .media, &.with-grid-fix .thumb {
|
&.with-grid-fix .media, &.with-grid-fix .thumb {
|
||||||
width: calc(100% + 1px) !important;
|
width: calc(100% + 1px) !important;
|
||||||
@ -13,6 +14,10 @@
|
|||||||
&:global(.custom-color) {
|
&:global(.custom-color) {
|
||||||
--emoji-status-color: var(--color-primary);
|
--emoji-status-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, { useRef } from '../../lib/teact/teact';
|
import React, { useRef } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiUser, ApiMessage, ApiChat } from '../../api/types';
|
import type {
|
||||||
|
ApiUser, ApiMessage, ApiChat,
|
||||||
|
} from '../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getMessageMediaHash,
|
getMessageMediaHash,
|
||||||
|
|||||||
@ -5,27 +5,39 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiChat, ApiTypingStatus } from '../../api/types';
|
import type {
|
||||||
|
ApiChat, ApiTopic, ApiThreadInfo, ApiTypingStatus,
|
||||||
|
} from '../../api/types';
|
||||||
import type { GlobalState } from '../../global/types';
|
import type { GlobalState } from '../../global/types';
|
||||||
import type { AnimationLevel } from '../../types';
|
import type { AnimationLevel } from '../../types';
|
||||||
|
import type { LangFn } from '../../hooks/useLang';
|
||||||
import { MediaViewerOrigin } from '../../types';
|
import { MediaViewerOrigin } from '../../types';
|
||||||
|
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
import {
|
import {
|
||||||
getChatTypeString,
|
getChatTypeString,
|
||||||
getMainUsername,
|
getMainUsername,
|
||||||
isChatSuperGroup,
|
isChatSuperGroup,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import { selectChat, selectChatMessages, selectChatOnlineCount } from '../../global/selectors';
|
import {
|
||||||
import type { LangFn } from '../../hooks/useLang';
|
selectChat, selectChatMessages, selectChatOnlineCount, selectThreadInfo,
|
||||||
|
} from '../../global/selectors';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import Avatar from './Avatar';
|
import Avatar from './Avatar';
|
||||||
import TypingStatus from './TypingStatus';
|
import TypingStatus from './TypingStatus';
|
||||||
import DotAnimation from './DotAnimation';
|
import DotAnimation from './DotAnimation';
|
||||||
import FullNameTitle from './FullNameTitle';
|
import FullNameTitle from './FullNameTitle';
|
||||||
|
import TopicIcon from './TopicIcon';
|
||||||
|
|
||||||
|
const TOPIC_ICON_SIZE = 2.5 * REM;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
|
className?: string;
|
||||||
typingStatus?: ApiTypingStatus;
|
typingStatus?: ApiTypingStatus;
|
||||||
avatarSize?: 'small' | 'medium' | 'large' | 'jumbo';
|
avatarSize?: 'small' | 'medium' | 'large' | 'jumbo';
|
||||||
status?: string;
|
status?: string;
|
||||||
@ -37,11 +49,15 @@ type OwnProps = {
|
|||||||
withChatType?: boolean;
|
withChatType?: boolean;
|
||||||
withVideoAvatar?: boolean;
|
withVideoAvatar?: boolean;
|
||||||
noRtl?: boolean;
|
noRtl?: boolean;
|
||||||
|
noAvatar?: boolean;
|
||||||
|
onClick?: VoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps =
|
type StateProps =
|
||||||
{
|
{
|
||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
|
threadInfo?: ApiThreadInfo;
|
||||||
|
topic?: ApiTopic;
|
||||||
onlineCount?: number;
|
onlineCount?: number;
|
||||||
areMessagesLoaded: boolean;
|
areMessagesLoaded: boolean;
|
||||||
animationLevel: AnimationLevel;
|
animationLevel: AnimationLevel;
|
||||||
@ -50,7 +66,9 @@ type StateProps =
|
|||||||
|
|
||||||
const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||||
typingStatus,
|
typingStatus,
|
||||||
|
className,
|
||||||
avatarSize = 'medium',
|
avatarSize = 'medium',
|
||||||
|
noAvatar,
|
||||||
status,
|
status,
|
||||||
withDots,
|
withDots,
|
||||||
withMediaViewer,
|
withMediaViewer,
|
||||||
@ -59,12 +77,15 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
withUpdatingStatus,
|
withUpdatingStatus,
|
||||||
withChatType,
|
withChatType,
|
||||||
withVideoAvatar,
|
withVideoAvatar,
|
||||||
|
threadInfo,
|
||||||
noRtl,
|
noRtl,
|
||||||
chat,
|
chat,
|
||||||
onlineCount,
|
onlineCount,
|
||||||
areMessagesLoaded,
|
areMessagesLoaded,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
topic,
|
||||||
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadFullChat,
|
loadFullChat,
|
||||||
@ -73,6 +94,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const isSuperGroup = chat && isChatSuperGroup(chat);
|
const isSuperGroup = chat && isChatSuperGroup(chat);
|
||||||
|
const isTopic = Boolean(chat?.isForum && threadInfo && topic);
|
||||||
const { id: chatId, isMin, isRestricted } = chat || {};
|
const { id: chatId, isMin, isRestricted } = chat || {};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -123,6 +145,14 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
return <TypingStatus typingStatus={typingStatus} />;
|
return <TypingStatus typingStatus={typingStatus} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isTopic) {
|
||||||
|
return (
|
||||||
|
<span className="status" dir="auto">
|
||||||
|
{threadInfo?.messagesCount ? lang('messages', threadInfo.messagesCount, 'i') : renderText(chat.title)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (withChatType) {
|
if (withChatType) {
|
||||||
return (
|
return (
|
||||||
<span className="status" dir="auto">{lang(getChatTypeString(chat))}</span>
|
<span className="status" dir="auto">{lang(getChatTypeString(chat))}</span>
|
||||||
@ -142,17 +172,30 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ChatInfo" dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
|
<div
|
||||||
<Avatar
|
className={
|
||||||
key={chat.id}
|
buildClassName('ChatInfo', className)
|
||||||
size={avatarSize}
|
}
|
||||||
chat={chat}
|
dir={!noRtl && lang.isRtl ? 'rtl' : undefined}
|
||||||
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
|
onClick={onClick}
|
||||||
withVideo={withVideoAvatar}
|
>
|
||||||
animationLevel={animationLevel}
|
{!noAvatar && !isTopic && (
|
||||||
/>
|
<Avatar
|
||||||
|
key={chat.id}
|
||||||
|
size={avatarSize}
|
||||||
|
chat={chat}
|
||||||
|
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
|
||||||
|
withVideo={withVideoAvatar}
|
||||||
|
animationLevel={animationLevel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isTopic && (
|
||||||
|
<TopicIcon topic={topic!} className="topic-header-icon" size={TOPIC_ICON_SIZE} />
|
||||||
|
)}
|
||||||
<div className="info">
|
<div className="info">
|
||||||
<FullNameTitle peer={chat} />
|
{topic
|
||||||
|
? <h3 dir="auto" className="fullName">{renderText(topic.title)}</h3>
|
||||||
|
: <FullNameTitle peer={chat} />}
|
||||||
{renderStatusOrTyping()}
|
{renderStatusOrTyping()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -177,16 +220,20 @@ function getGroupStatus(lang: LangFn, chat: ApiChat) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId }): StateProps => {
|
(global, { chatId, threadId }): StateProps => {
|
||||||
const { lastSyncTime } = global;
|
const { lastSyncTime } = global;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
|
const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
|
||||||
const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined;
|
const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined;
|
||||||
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
|
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
|
||||||
|
const topic = threadId ? chat?.topics?.[threadId] : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
chat,
|
chat,
|
||||||
|
threadInfo,
|
||||||
onlineCount,
|
onlineCount,
|
||||||
|
topic,
|
||||||
areMessagesLoaded,
|
areMessagesLoaded,
|
||||||
animationLevel: global.settings.byKey.animationLevel,
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
|
|
||||||
.Avatar__icon, i {
|
.Avatar__icon, i {
|
||||||
font-size: 2rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +120,16 @@
|
|||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
&.forum-avatar {
|
||||||
|
border-start-start-radius: 0.625rem;
|
||||||
|
border-end-start-radius: 0.625rem;
|
||||||
|
|
||||||
|
.item-remove {
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&[dir="rtl"] {
|
&[dir="rtl"] {
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
|
|||||||
@ -74,6 +74,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
'PickerSelectedItem',
|
'PickerSelectedItem',
|
||||||
className,
|
className,
|
||||||
|
chat?.isForum && 'forum-avatar',
|
||||||
isMinimized && 'minimized',
|
isMinimized && 'minimized',
|
||||||
canClose && 'closeable',
|
canClose && 'closeable',
|
||||||
);
|
);
|
||||||
|
|||||||
179
src/components/common/ProfileInfo.module.scss
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
.self {
|
||||||
|
margin: 0 -0.5rem 0.75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:global(.ghost) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.photoWrapper {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
> :global(.Transition) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.photoDashes {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 0.125rem;
|
||||||
|
padding: 0 0.375rem;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
top: 0.5rem;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photoDash {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
opacity: 0.25;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
margin: 0 0.125rem;
|
||||||
|
transition: opacity 300ms ease;
|
||||||
|
|
||||||
|
&_current {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 25%;
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
appearance: none;
|
||||||
|
background: transparent no-repeat;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
opacity: 0.25;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:global(:hover),
|
||||||
|
:global(.is-touch-env) & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&_prev {
|
||||||
|
left: 0;
|
||||||
|
background-image: url("../../assets/media_navigation_previous.svg");
|
||||||
|
background-position: 1.25rem 50%;
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&_next {
|
||||||
|
right: 0;
|
||||||
|
background-image: url("../../assets/media_navigation_next.svg");
|
||||||
|
background-position: calc(100% - 1.25rem) 50%;
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
left: 0;
|
||||||
|
right: auto;
|
||||||
|
transform: scaleX(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 100px;
|
||||||
|
padding: 0 1.5rem 0.5rem;
|
||||||
|
background: linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
||||||
|
color: var(--color-white);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&:dir(rtl) {
|
||||||
|
.status {
|
||||||
|
text-align: right;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[dir="rtl"] {
|
||||||
|
.status {
|
||||||
|
text-align: right;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicContainer {
|
||||||
|
--custom-emoji-size: 7.5rem;
|
||||||
|
|
||||||
|
padding: 1rem 1rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicTitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicIcon {
|
||||||
|
margin: auto;
|
||||||
|
width: 7.5rem !important;
|
||||||
|
height: 7.5rem !important;
|
||||||
|
display: flex !important;
|
||||||
|
|
||||||
|
&:global(.general-forum-icon) {
|
||||||
|
font-size: 7.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicIconTitle {
|
||||||
|
font-size: 3rem !important;
|
||||||
|
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
:global(.emoji-small) {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.topicMessagesCounter {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// This class is used in `ghostAnimation`, so we need to keep it global
|
||||||
.ProfileInfo {
|
.ProfileInfo {
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -16,176 +17,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-wrapper {
|
.fullName {
|
||||||
width: 100%;
|
font-weight: 500;
|
||||||
position: absolute;
|
font-size: 1.25rem;
|
||||||
left: 0;
|
line-height: 1.375rem;
|
||||||
top: 0;
|
white-space: pre-wrap;
|
||||||
bottom: 0;
|
word-break: break-word;
|
||||||
|
margin-bottom: 0;
|
||||||
> .Transition {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-dashes {
|
.VerifiedIcon,
|
||||||
position: absolute;
|
.PremiumIcon {
|
||||||
width: 100%;
|
z-index: 2;
|
||||||
height: 0.125rem;
|
--color-fill: var(--color-white);
|
||||||
padding: 0 0.375rem;
|
--color-checkmark: var(--color-primary);
|
||||||
z-index: 1;
|
opacity: 0.8;
|
||||||
|
|
||||||
display: flex;
|
|
||||||
top: 0.5rem;
|
|
||||||
left: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-dash {
|
.emoji:not(.custom-emoji) {
|
||||||
flex: 1 1 auto;
|
width: 1.5rem;
|
||||||
background-color: var(--color-white);
|
height: 1.5rem;
|
||||||
opacity: 0.25;
|
background-size: 1.5rem;
|
||||||
border-radius: 0.125rem;
|
|
||||||
margin: 0 0.125rem;
|
|
||||||
transition: opacity 300ms ease;
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
opacity: 0.75;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation {
|
.custom-emoji {
|
||||||
position: absolute;
|
pointer-events: auto;
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 25%;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
appearance: none;
|
|
||||||
background: transparent no-repeat;
|
|
||||||
background-size: 1.25rem;
|
|
||||||
opacity: 0.25;
|
|
||||||
transition: opacity 0.15s;
|
|
||||||
outline: none;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&:hover,
|
--custom-emoji-size: 1.5rem;
|
||||||
.is-touch-env & {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.prev {
|
.custom-color {
|
||||||
left: 0;
|
--emoji-status-color: var(--color-white);
|
||||||
background-image: url("../../assets/media_navigation_previous.svg");
|
|
||||||
background-position: 1.25rem 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.next {
|
|
||||||
right: 0;
|
|
||||||
background-image: url("../../assets/media_navigation_next.svg");
|
|
||||||
background-position: calc(100% - 1.25rem) 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 100px;
|
|
||||||
padding: 0 1.5rem 0.5rem;
|
|
||||||
background: linear-gradient(0deg, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0) 100%);
|
|
||||||
color: var(--color-white);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-end;
|
|
||||||
pointer-events: none;
|
|
||||||
|
|
||||||
&:dir(rtl) {
|
|
||||||
.status {
|
|
||||||
text-align: right;
|
|
||||||
unicode-bidi: plaintext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[dir="rtl"] {
|
|
||||||
.status {
|
|
||||||
text-align: right;
|
|
||||||
unicode-bidi: plaintext;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
.fullName {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.375rem;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-word;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.VerifiedIcon, .PremiumIcon {
|
|
||||||
z-index: 2;
|
|
||||||
--color-fill: var(--color-white);
|
|
||||||
--color-checkmark: var(--color-primary);
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emoji:not(.custom-emoji) {
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
background-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.custom-emoji {
|
|
||||||
pointer-events: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
--custom-emoji-size: 1.5rem;
|
|
||||||
|
|
||||||
&.custom-color {
|
|
||||||
--emoji-status-color: var(--color-white);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[dir="rtl"] {
|
|
||||||
.navigation.prev {
|
|
||||||
left: auto;
|
|
||||||
right: 0;
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.navigation.next {
|
|
||||||
left: 0;
|
|
||||||
right: auto;
|
|
||||||
transform: scaleX(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.self {
|
|
||||||
margin: 0 -0.5rem 0.75rem;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&.ghost {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
line-height: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,19 +4,23 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiUser, ApiChat, ApiUserStatus } from '../../api/types';
|
import type {
|
||||||
|
ApiUser, ApiChat, ApiUserStatus, ApiTopic,
|
||||||
|
} from '../../api/types';
|
||||||
import type { GlobalState } from '../../global/types';
|
import type { GlobalState } from '../../global/types';
|
||||||
import type { AnimationLevel } from '../../types';
|
import type { AnimationLevel } from '../../types';
|
||||||
import { MediaViewerOrigin } from '../../types';
|
import { MediaViewerOrigin } from '../../types';
|
||||||
|
|
||||||
|
import { GENERAL_TOPIC_ID } from '../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
import { selectChat, selectUser, selectUserStatus } from '../../global/selectors';
|
|
||||||
import {
|
import {
|
||||||
getUserStatus, isChatChannel, isUserOnline,
|
selectChat, selectCurrentMessageList, selectThreadInfo, selectUser, selectUserStatus,
|
||||||
} from '../../global/helpers';
|
} from '../../global/selectors';
|
||||||
|
import { getUserStatus, isChatChannel, isUserOnline } from '../../global/helpers';
|
||||||
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
import { captureEvents, SwipeDirection } from '../../util/captureEvents';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
import usePhotosPreload from './hooks/usePhotosPreload';
|
import usePhotosPreload from './hooks/usePhotosPreload';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
@ -25,8 +29,10 @@ import usePrevious from '../../hooks/usePrevious';
|
|||||||
import FullNameTitle from './FullNameTitle';
|
import FullNameTitle from './FullNameTitle';
|
||||||
import ProfilePhoto from './ProfilePhoto';
|
import ProfilePhoto from './ProfilePhoto';
|
||||||
import Transition from '../ui/Transition';
|
import Transition from '../ui/Transition';
|
||||||
|
import TopicIcon from './TopicIcon';
|
||||||
|
|
||||||
import './ProfileInfo.scss';
|
import './ProfileInfo.scss';
|
||||||
|
import styles from './ProfileInfo.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
userId: string;
|
userId: string;
|
||||||
@ -44,10 +50,13 @@ type StateProps =
|
|||||||
serverTimeOffset: number;
|
serverTimeOffset: number;
|
||||||
mediaId?: number;
|
mediaId?: number;
|
||||||
avatarOwnerId?: string;
|
avatarOwnerId?: string;
|
||||||
|
topic?: ApiTopic;
|
||||||
|
messagesCount?: number;
|
||||||
}
|
}
|
||||||
& Pick<GlobalState, 'connectionState'>;
|
& Pick<GlobalState, 'connectionState'>;
|
||||||
|
|
||||||
const EMOJI_STATUS_SIZE = 24;
|
const EMOJI_STATUS_SIZE = 24;
|
||||||
|
const EMOJI_TOPIC_SIZE = 120;
|
||||||
|
|
||||||
const ProfileInfo: FC<OwnProps & StateProps> = ({
|
const ProfileInfo: FC<OwnProps & StateProps> = ({
|
||||||
forceShowSelf,
|
forceShowSelf,
|
||||||
@ -61,6 +70,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
mediaId,
|
mediaId,
|
||||||
avatarOwnerId,
|
avatarOwnerId,
|
||||||
|
topic,
|
||||||
|
messagesCount,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadFullUser,
|
loadFullUser,
|
||||||
@ -139,7 +150,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
// Swipe gestures
|
// Swipe gestures
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const element = document.querySelector<HTMLDivElement>('.photo-wrapper');
|
const element = document.querySelector<HTMLDivElement>(`.${styles.photoWrapper}`);
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -164,15 +175,35 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTopic() {
|
||||||
|
return (
|
||||||
|
<div className={styles.topicContainer}>
|
||||||
|
<TopicIcon
|
||||||
|
topic={topic!}
|
||||||
|
size={EMOJI_TOPIC_SIZE}
|
||||||
|
className={styles.topicIcon}
|
||||||
|
letterClassName={styles.topicIconTitle}
|
||||||
|
noLoopLimit
|
||||||
|
/>
|
||||||
|
<h3 className={styles.topicTitle} dir={lang.isRtl ? 'rtl' : undefined}>{renderText(topic!.title)}</h3>
|
||||||
|
<p className={styles.topicMessagesCounter}>
|
||||||
|
{messagesCount && messagesCount > 1
|
||||||
|
? lang('Chat.Title.Topic', messagesCount + (topic!.id === GENERAL_TOPIC_ID ? 1 : -1), 'i')
|
||||||
|
: lang('lng_forum_no_messages')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPhotoTabs() {
|
function renderPhotoTabs() {
|
||||||
if (isSavedMessages || !photos || photos.length <= 1) {
|
if (isSavedMessages || !photos || photos.length <= 1) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="photo-dashes">
|
<div className={styles.photoDashes}>
|
||||||
{photos.map((_, i) => (
|
{photos.map((_, i) => (
|
||||||
<span className={`photo-dash ${i === currentPhotoIndex ? 'current' : ''}`} />
|
<span className={buildClassName(styles.photoDash, i === currentPhotoIndex && styles.photoDash_current)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -198,14 +229,14 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
function renderStatus() {
|
function renderStatus() {
|
||||||
if (user) {
|
if (user) {
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('status', isUserOnline(user, userStatus) && 'online')}>
|
<div className={buildClassName(styles.status, 'status', isUserOnline(user, userStatus) && 'online')}>
|
||||||
<span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
|
<span className="user-status" dir="auto">{getUserStatus(lang, user, userStatus, serverTimeOffset)}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="status" dir="auto">{
|
<span className={buildClassName(styles.status, 'status')} dir="auto">{
|
||||||
isChatChannel(chat!)
|
isChatChannel(chat!)
|
||||||
? lang('Subscribers', chat!.membersCount ?? 0, 'i')
|
? lang('Subscribers', chat!.membersCount ?? 0, 'i')
|
||||||
: lang('Members', chat!.membersCount ?? 0, 'i')
|
: lang('Members', chat!.membersCount ?? 0, 'i')
|
||||||
@ -214,18 +245,26 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (topic) {
|
||||||
|
return renderTopic();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={buildClassName('ProfileInfo', forceShowSelf && 'self')} dir={lang.isRtl ? 'rtl' : undefined}>
|
<div
|
||||||
<div className="photo-wrapper">
|
className={buildClassName('ProfileInfo', forceShowSelf && styles.self)}
|
||||||
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
|
>
|
||||||
|
<div className={styles.photoWrapper}>
|
||||||
{renderPhotoTabs()}
|
{renderPhotoTabs()}
|
||||||
<Transition activeKey={currentPhotoIndex} name={slideAnimation} className="profile-slide-container">
|
<Transition activeKey={currentPhotoIndex} name={slideAnimation}>
|
||||||
{renderPhoto}
|
{renderPhoto}
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
{!isFirst && (
|
{!isFirst && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="navigation prev"
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
|
className={buildClassName(styles.navigation, styles.navigation_prev)}
|
||||||
aria-label={lang('AccDescrPrevious')}
|
aria-label={lang('AccDescrPrevious')}
|
||||||
onClick={selectPreviousMedia}
|
onClick={selectPreviousMedia}
|
||||||
/>
|
/>
|
||||||
@ -233,14 +272,15 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
|
|||||||
{!isLast && (
|
{!isLast && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="navigation next"
|
dir={lang.isRtl ? 'rtl' : undefined}
|
||||||
|
className={buildClassName(styles.navigation, styles.navigation_next)}
|
||||||
aria-label={lang('Next')}
|
aria-label={lang('Next')}
|
||||||
onClick={selectNextMedia}
|
onClick={selectNextMedia}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="info" dir={lang.isRtl ? 'rtl' : 'auto'}>
|
<div className={styles.info} dir={lang.isRtl ? 'rtl' : 'auto'}>
|
||||||
{(user || chat) && (
|
{(user || chat) && (
|
||||||
<FullNameTitle
|
<FullNameTitle
|
||||||
peer={(user || chat)!}
|
peer={(user || chat)!}
|
||||||
@ -266,6 +306,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
const isSavedMessages = !forceShowSelf && user && user.isSelf;
|
||||||
const { animationLevel } = global.settings.byKey;
|
const { animationLevel } = global.settings.byKey;
|
||||||
const { mediaId, avatarOwnerId } = global.mediaViewer;
|
const { mediaId, avatarOwnerId } = global.mediaViewer;
|
||||||
|
const isForum = chat?.isForum;
|
||||||
|
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
|
||||||
|
const threadInfo = currentTopicId ? selectThreadInfo(global, userId, currentTopicId) : undefined;
|
||||||
|
const topic = isForum && currentTopicId ? chat?.topics?.[currentTopicId] : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connectionState,
|
connectionState,
|
||||||
@ -277,6 +321,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
mediaId,
|
mediaId,
|
||||||
avatarOwnerId,
|
avatarOwnerId,
|
||||||
|
...(topic && {
|
||||||
|
topic,
|
||||||
|
messagesCount: threadInfo?.messagesCount,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ProfileInfo));
|
)(ProfileInfo));
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
&.replies-bot-account,
|
&.replies-bot-account,
|
||||||
&.deleted-account,
|
&.deleted-account,
|
||||||
&.saved-messages {
|
&.saved-messages {
|
||||||
font-size: 20rem;
|
font-size: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export type OwnProps = {
|
|||||||
searchPlaceholder: string;
|
searchPlaceholder: string;
|
||||||
filter?: ApiChatType[];
|
filter?: ApiChatType[];
|
||||||
loadMore?: NoneToVoidFunction;
|
loadMore?: NoneToVoidFunction;
|
||||||
onSelectRecipient: (peerId: string) => void;
|
onSelectRecipient: (peerId: string, threadId?: number) => void;
|
||||||
onClose: NoneToVoidFunction;
|
onClose: NoneToVoidFunction;
|
||||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
@ -94,6 +94,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
<ChatOrUserPicker
|
<ChatOrUserPicker
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
chatOrUserIds={renderingIds}
|
chatOrUserIds={renderingIds}
|
||||||
|
chatsById={chatsById}
|
||||||
searchPlaceholder={searchPlaceholder}
|
searchPlaceholder={searchPlaceholder}
|
||||||
search={search}
|
search={search}
|
||||||
onSearchChange={setSearch}
|
onSearchChange={setSearch}
|
||||||
|
|||||||
33
src/components/common/TopicChip.module.scss
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
.root {
|
||||||
|
--custom-emoji-size: 1.125rem;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: var(--border-radius-messages);
|
||||||
|
color: var(--topic-button-accent-color);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--topic-button-accent-color);
|
||||||
|
opacity: 0.15;
|
||||||
|
transition: opacity 0.15s ease-in-out;
|
||||||
|
border-radius: var(--border-radius-messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::before {
|
||||||
|
opacity: 0.10;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/components/common/TopicChip.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiTopic } from '../../api/types';
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { getTopicColorCssVariable } from '../../util/forumColors';
|
||||||
|
import { REM } from './helpers/mediaDimensions';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import TopicIcon from './TopicIcon';
|
||||||
|
|
||||||
|
import styles from './TopicChip.module.scss';
|
||||||
|
import blankSrc from '../../assets/blank.png';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
topic?: ApiTopic;
|
||||||
|
className?: string;
|
||||||
|
onClick?: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOPIC_ICON_SIZE = 1.125 * REM;
|
||||||
|
|
||||||
|
const TopicChip: FC<OwnProps> = ({
|
||||||
|
topic,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.root, className)}
|
||||||
|
style={`--topic-button-accent-color: var(${getTopicColorCssVariable(topic?.iconColor)})`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
{topic
|
||||||
|
? <TopicIcon topic={topic} size={TOPIC_ICON_SIZE} />
|
||||||
|
: <img src={blankSrc} alt="" />}
|
||||||
|
{topic?.title ? renderText(topic.title) : lang('Loading')}
|
||||||
|
{topic?.isClosed && <i className="icon-lock" />}
|
||||||
|
<i className="icon-next" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TopicChip);
|
||||||
30
src/components/common/TopicDefaultIcon.module.scss
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
.root {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
z-index: 1;
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
position: relative;
|
||||||
|
bottom: 0.0625rem;
|
||||||
|
|
||||||
|
:global(.emoji) {
|
||||||
|
width: 0.75rem;
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/components/common/TopicDefaultIcon.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import { GENERAL_TOPIC_ID } from '../../config';
|
||||||
|
import { getFirstLetters } from '../../util/textFormat';
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
import { getTopicDefaultIcon } from '../../util/forumColors';
|
||||||
|
|
||||||
|
import styles from './TopicDefaultIcon.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
className?: string;
|
||||||
|
letterClassName?: string;
|
||||||
|
topicId: number;
|
||||||
|
iconColor?: number;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TopicDefaultIcon: FC<OwnProps> = ({
|
||||||
|
className,
|
||||||
|
letterClassName,
|
||||||
|
topicId,
|
||||||
|
iconColor,
|
||||||
|
title,
|
||||||
|
}) => {
|
||||||
|
const iconSrc = getTopicDefaultIcon(iconColor);
|
||||||
|
|
||||||
|
if (topicId === GENERAL_TOPIC_ID) {
|
||||||
|
return <i className={buildClassName(styles.root, className, 'icon-hashtag', 'general-forum-icon')} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.root, className)}>
|
||||||
|
<img className={styles.icon} src={iconSrc} alt="" />
|
||||||
|
<div className={buildClassName(styles.title, letterClassName, 'topic-icon-letter')}>
|
||||||
|
{renderText(getFirstLetters(title, 1))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TopicDefaultIcon);
|
||||||
52
src/components/common/TopicIcon.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
|
import type { ApiTopic } from '../../api/types';
|
||||||
|
|
||||||
|
import CustomEmoji from './CustomEmoji';
|
||||||
|
import TopicDefaultIcon from './TopicDefaultIcon';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
topic: ApiTopic;
|
||||||
|
className?: string;
|
||||||
|
letterClassName?: string;
|
||||||
|
size?: number;
|
||||||
|
noLoopLimit?: true;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOOP_LIMIT = 2;
|
||||||
|
|
||||||
|
const TopicIcon: FC<OwnProps> = ({
|
||||||
|
topic,
|
||||||
|
className,
|
||||||
|
letterClassName,
|
||||||
|
size,
|
||||||
|
noLoopLimit,
|
||||||
|
observeIntersection,
|
||||||
|
}) => {
|
||||||
|
if (topic.iconEmojiId) {
|
||||||
|
return (
|
||||||
|
<CustomEmoji
|
||||||
|
documentId={topic.iconEmojiId}
|
||||||
|
className={className}
|
||||||
|
size={size}
|
||||||
|
observeIntersectionForPlaying={observeIntersection}
|
||||||
|
loopLimit={!noLoopLimit ? LOOP_LIMIT : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TopicDefaultIcon
|
||||||
|
iconColor={topic.iconColor}
|
||||||
|
title={topic.title}
|
||||||
|
topicId={topic.id}
|
||||||
|
className={className}
|
||||||
|
letterClassName={letterClassName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(TopicIcon);
|
||||||
@ -64,7 +64,7 @@
|
|||||||
.left {
|
.left {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
min-width: 12rem;
|
min-width: 16rem;
|
||||||
width: 33vw;
|
width: 33vw;
|
||||||
max-width: 26.5rem;
|
max-width: 26.5rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from '../../../lib/teact/teact';
|
import React from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiMessage, ApiUser, ApiGroupCall,
|
ApiChat, ApiMessage, ApiUser, ApiGroupCall, ApiTopic,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { TextPart } from '../../../types';
|
import type { TextPart } from '../../../types';
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
@ -38,6 +38,7 @@ export function renderActionMessageText(
|
|||||||
targetUsers?: ApiUser[],
|
targetUsers?: ApiUser[],
|
||||||
targetMessage?: ApiMessage,
|
targetMessage?: ApiMessage,
|
||||||
targetChatId?: string,
|
targetChatId?: string,
|
||||||
|
topic?: ApiTopic,
|
||||||
options: RenderOptions = {},
|
options: RenderOptions = {},
|
||||||
observeIntersectionForLoading?: ObserveFn,
|
observeIntersectionForLoading?: ObserveFn,
|
||||||
observeIntersectionForPlaying?: ObserveFn,
|
observeIntersectionForPlaying?: ObserveFn,
|
||||||
@ -54,6 +55,7 @@ export function renderActionMessageText(
|
|||||||
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
||||||
? 'Message.PinnedGenericMessage'
|
? 'Message.PinnedGenericMessage'
|
||||||
: text;
|
: text;
|
||||||
|
|
||||||
let unprocessed = lang(translationKey, translationValues?.length ? translationValues : undefined);
|
let unprocessed = lang(translationKey, translationValues?.length ? translationValues : undefined);
|
||||||
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
|
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
|
||||||
unprocessed = unprocessed.replace('un1', '%action_origin%').replace('un2', '%message%');
|
unprocessed = unprocessed.replace('un1', '%action_origin%').replace('un2', '%message%');
|
||||||
@ -92,6 +94,16 @@ export function renderActionMessageText(
|
|||||||
unprocessed = processed.pop() as string;
|
unprocessed = processed.pop() as string;
|
||||||
content.push(...processed);
|
content.push(...processed);
|
||||||
|
|
||||||
|
if (unprocessed.includes('%action_topic%')) {
|
||||||
|
processed = processPlaceholder(
|
||||||
|
unprocessed,
|
||||||
|
'%action_topic%',
|
||||||
|
topic ? topic.title : 'a topic',
|
||||||
|
);
|
||||||
|
unprocessed = processed.pop() as string;
|
||||||
|
content.push(...processed);
|
||||||
|
}
|
||||||
|
|
||||||
if (unprocessed.includes('%gift_payment_amount%')) {
|
if (unprocessed.includes('%gift_payment_amount%')) {
|
||||||
processed = processPlaceholder(
|
processed = processPlaceholder(
|
||||||
unprocessed,
|
unprocessed,
|
||||||
|
|||||||
@ -5,4 +5,27 @@
|
|||||||
.chat-list {
|
.chat-list {
|
||||||
height: calc(100% - var(--header-height));
|
height: calc(100% - var(--header-height));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.DropdownMenuFiller {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Button.rtl {
|
||||||
|
transition: var(--slide-transition) transform;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&.right-aligned {
|
||||||
|
transform: translateX(calc(clamp(
|
||||||
|
var(--left-column-min-width),
|
||||||
|
var(--left-column-width),
|
||||||
|
var(--left-column-max-width)
|
||||||
|
) - 4.375rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disable-transition {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,22 +1,33 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
import useLeftHeaderButtonRtlForumTransition from './main/hooks/useLeftHeaderButtonRtlForumTransition';
|
||||||
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
import useForumPanelRender from '../../hooks/useForumPanelRender';
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
import ChatList from './main/ChatList';
|
import ChatList from './main/ChatList';
|
||||||
import type { LeftColumnContent } from '../../types';
|
import ForumPanel from './main/ForumPanel';
|
||||||
|
|
||||||
import './ArchivedChats.scss';
|
import './ArchivedChats.scss';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onContentChange: (content: LeftColumnContent) => void;
|
onTopicSearch: NoneToVoidFunction;
|
||||||
|
isForumPanelOpen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ArchivedChats: FC<OwnProps> = ({ isActive, onReset }) => {
|
const ArchivedChats: FC<OwnProps> = ({
|
||||||
|
isActive,
|
||||||
|
isForumPanelOpen,
|
||||||
|
onReset,
|
||||||
|
onTopicSearch,
|
||||||
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
@ -24,21 +35,47 @@ const ArchivedChats: FC<OwnProps> = ({ isActive, onReset }) => {
|
|||||||
onBack: onReset,
|
onBack: onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldDisableDropdownMenuTransitionRef,
|
||||||
|
handleDropdownMenuTransitionEnd,
|
||||||
|
} = useLeftHeaderButtonRtlForumTransition(isForumPanelOpen);
|
||||||
|
|
||||||
|
const {
|
||||||
|
shouldRender: shouldRenderTitle,
|
||||||
|
transitionClassNames: titleClassNames,
|
||||||
|
} = useShowTransition(!isForumPanelOpen);
|
||||||
|
|
||||||
|
const { shouldRenderForumPanel, handleForumPanelAnimationEnd } = useForumPanelRender(isForumPanelOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ArchivedChats">
|
<div className="ArchivedChats">
|
||||||
<div className="left-header">
|
<div className="left-header">
|
||||||
|
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="smaller"
|
size="smaller"
|
||||||
color="translucent"
|
color="translucent"
|
||||||
onClick={onReset}
|
onClick={onReset}
|
||||||
ariaLabel="Return to chat list"
|
ariaLabel="Return to chat list"
|
||||||
|
className={buildClassName(
|
||||||
|
lang.isRtl && 'rtl',
|
||||||
|
isForumPanelOpen && lang.isRtl && 'right-aligned',
|
||||||
|
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
|
||||||
|
)}
|
||||||
|
onTransitionEnd={handleDropdownMenuTransitionEnd}
|
||||||
>
|
>
|
||||||
<i className="icon-arrow-left" />
|
<i className="icon-arrow-left" />
|
||||||
</Button>
|
</Button>
|
||||||
<h3>{lang('ArchivedChats')}</h3>
|
{shouldRenderTitle && <h3 className={titleClassNames}>{lang('ArchivedChats')}</h3>}
|
||||||
</div>
|
</div>
|
||||||
<ChatList folderType="archived" isActive={isActive} />
|
<ChatList folderType="archived" isActive={isActive} />
|
||||||
|
{shouldRenderForumPanel && (
|
||||||
|
<ForumPanel
|
||||||
|
isOpen={isForumPanelOpen}
|
||||||
|
onTopicSearch={onTopicSearch}
|
||||||
|
onCloseAnimationEnd={handleForumPanelAnimationEnd}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { LeftColumnContent, SettingsScreens } from '../../types';
|
|||||||
|
|
||||||
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
|
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/environment';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
import { selectCurrentChat } from '../../global/selectors';
|
import { selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
|
||||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||||
import { useResize } from '../../hooks/useResize';
|
import { useResize } from '../../hooks/useResize';
|
||||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||||
@ -33,6 +33,9 @@ type StateProps = {
|
|||||||
nextSettingsScreen?: SettingsScreens;
|
nextSettingsScreen?: SettingsScreens;
|
||||||
isChatOpen: boolean;
|
isChatOpen: boolean;
|
||||||
isUpdateAvailable?: boolean;
|
isUpdateAvailable?: boolean;
|
||||||
|
isForumPanelOpen?: boolean;
|
||||||
|
forumPanelChatId?: string;
|
||||||
|
isClosingSearch?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ContentType {
|
enum ContentType {
|
||||||
@ -60,9 +63,13 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
isChatOpen,
|
isChatOpen,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
|
isForumPanelOpen,
|
||||||
|
forumPanelChatId,
|
||||||
|
isClosingSearch,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setGlobalSearchQuery,
|
setGlobalSearchQuery,
|
||||||
|
setGlobalSearchClosing,
|
||||||
setGlobalSearchChatId,
|
setGlobalSearchChatId,
|
||||||
resetChatCreation,
|
resetChatCreation,
|
||||||
setGlobalSearchDate,
|
setGlobalSearchDate,
|
||||||
@ -106,11 +113,13 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
function fullReset() {
|
function fullReset() {
|
||||||
setContent(LeftColumnContent.ChatList);
|
setContent(LeftColumnContent.ChatList);
|
||||||
setContactsFilter('');
|
setContactsFilter('');
|
||||||
setGlobalSearchQuery({ query: '' });
|
setGlobalSearchClosing(true);
|
||||||
setGlobalSearchDate({ date: undefined });
|
|
||||||
setGlobalSearchChatId({ id: undefined });
|
|
||||||
resetChatCreation();
|
resetChatCreation();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
setGlobalSearchQuery({ query: '' });
|
||||||
|
setGlobalSearchDate({ date: undefined });
|
||||||
|
setGlobalSearchChatId({ id: undefined });
|
||||||
|
setGlobalSearchClosing(false);
|
||||||
setLastResetTime(Date.now());
|
setLastResetTime(Date.now());
|
||||||
}, RESET_TRANSITION_DELAY_MS);
|
}, RESET_TRANSITION_DELAY_MS);
|
||||||
}
|
}
|
||||||
@ -299,8 +308,8 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
|
|
||||||
fullReset();
|
fullReset();
|
||||||
}, [
|
}, [
|
||||||
content, isFirstChatFolderActive, settingsScreen, setGlobalSearchQuery, setGlobalSearchDate, setGlobalSearchChatId,
|
content, isFirstChatFolderActive, setGlobalSearchClosing, resetChatCreation, setGlobalSearchQuery,
|
||||||
resetChatCreation, hasPasscode,
|
setGlobalSearchDate, setGlobalSearchChatId, settingsScreen, hasPasscode,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const handleSearchQuery = useCallback((query: string) => {
|
const handleSearchQuery = useCallback((query: string) => {
|
||||||
@ -316,6 +325,12 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [content, searchQuery, setGlobalSearchQuery]);
|
}, [content, searchQuery, setGlobalSearchQuery]);
|
||||||
|
|
||||||
|
const handleTopicSearch = useCallback(() => {
|
||||||
|
setContent(LeftColumnContent.GlobalSearch);
|
||||||
|
setGlobalSearchQuery({ query: '' });
|
||||||
|
setGlobalSearchChatId({ id: forumPanelChatId });
|
||||||
|
}, [forumPanelChatId, setGlobalSearchChatId, setGlobalSearchQuery]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => (content !== LeftColumnContent.ChatList || (isFirstChatFolderActive && !isChatOpen)
|
() => (content !== LeftColumnContent.ChatList || (isFirstChatFolderActive && !isChatOpen)
|
||||||
? captureEscKeyListener(() => handleReset())
|
? captureEscKeyListener(() => handleReset())
|
||||||
@ -367,7 +382,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
initResize, resetResize, handleMouseUp,
|
initResize, resetResize, handleMouseUp,
|
||||||
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth);
|
} = useResize(resizeRef, setLeftColumnWidth, resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
|
||||||
|
|
||||||
const handleSettingsScreenSelect = useCallback((screen: SettingsScreens) => {
|
const handleSettingsScreenSelect = useCallback((screen: SettingsScreens) => {
|
||||||
setContent(LeftColumnContent.Settings);
|
setContent(LeftColumnContent.Settings);
|
||||||
@ -393,7 +408,8 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
<ArchivedChats
|
<ArchivedChats
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
onContentChange={setContent}
|
onTopicSearch={handleTopicSearch}
|
||||||
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case ContentType.Settings:
|
case ContentType.Settings:
|
||||||
@ -433,6 +449,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<LeftMain
|
<LeftMain
|
||||||
content={content}
|
content={content}
|
||||||
|
isClosingSearch={isClosingSearch}
|
||||||
searchQuery={searchQuery}
|
searchQuery={searchQuery}
|
||||||
searchDate={searchDate}
|
searchDate={searchDate}
|
||||||
contactsFilter={contactsFilter}
|
contactsFilter={contactsFilter}
|
||||||
@ -443,6 +460,8 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
shouldSkipTransition={shouldSkipHistoryAnimations}
|
shouldSkipTransition={shouldSkipHistoryAnimations}
|
||||||
isUpdateAvailable={isUpdateAvailable}
|
isUpdateAvailable={isUpdateAvailable}
|
||||||
|
isForumPanelOpen={isForumPanelOpen}
|
||||||
|
onTopicSearch={handleTopicSearch}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -480,7 +499,10 @@ export default memo(withGlobal(
|
|||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
const isChatOpen = Boolean(selectCurrentChat(global)?.id);
|
const currentChat = selectCurrentChat(global);
|
||||||
|
const isChatOpen = Boolean(currentChat?.id);
|
||||||
|
const isForumPanelOpen = selectIsForumPanelOpen(global);
|
||||||
|
const forumPanelChatId = global.forumPanelChatId;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
searchQuery: query,
|
searchQuery: query,
|
||||||
@ -493,6 +515,9 @@ export default memo(withGlobal(
|
|||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
isChatOpen,
|
isChatOpen,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
|
isForumPanelOpen,
|
||||||
|
forumPanelChatId,
|
||||||
|
isClosingSearch: global.globalSearch.isClosing,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(LeftColumn));
|
)(LeftColumn));
|
||||||
|
|||||||
@ -49,11 +49,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.mention,
|
&.mention,
|
||||||
&.unread:not(.muted) {
|
&.unread:not(.muted),
|
||||||
|
&.unopened:not(.muted) {
|
||||||
background: var(--color-green);
|
background: var(--color-green);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.unopened {
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
min-width: auto;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
&.pinned {
|
&.pinned {
|
||||||
color: var(--color-pinned);
|
color: var(--color-pinned);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||||
import React, { memo } from '../../../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { ApiChat } from '../../../api/types';
|
import type { ApiChat, ApiTopic } from '../../../api/types';
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
@ -13,38 +13,76 @@ import './Badge.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
|
topic?: ApiTopic;
|
||||||
|
wasTopicOpened?: boolean;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
|
shouldShowOnlyMostImportant?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
const Badge: FC<OwnProps> = ({
|
||||||
|
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
unreadMentionsCount = 0, unreadReactionsCount = 0,
|
||||||
|
} = !chat.isForum ? chat : {}; // TODO[forums] Unread mentions and reactions temporarily disabled for forums
|
||||||
|
|
||||||
|
const isTopicUnopened = !isPinned && topic && !wasTopicOpened;
|
||||||
|
const isForum = chat.isForum && !topic;
|
||||||
|
const topicsWithUnread = useMemo(() => (
|
||||||
|
isForum && chat?.topics ? Object.values(chat.topics).filter(({ unreadCount }) => unreadCount) : undefined
|
||||||
|
), [chat, isForum]);
|
||||||
|
|
||||||
|
const unreadCount = useMemo(() => (
|
||||||
|
isForum
|
||||||
|
// If we have unmuted topics, display the count of those. Otherwise, display the count of all topics.
|
||||||
|
? ((isMuted && topicsWithUnread?.filter((acc) => acc.isMuted === false).length)
|
||||||
|
|| topicsWithUnread?.length)
|
||||||
|
: (topic || chat).unreadCount
|
||||||
|
), [chat, topic, topicsWithUnread, isForum, isMuted]);
|
||||||
|
|
||||||
|
const shouldBeMuted = useMemo(() => {
|
||||||
|
const hasUnmutedUnreadTopics = chat.topics
|
||||||
|
&& Object.values(chat.topics).some((acc) => acc.isMuted && acc.unreadCount);
|
||||||
|
|
||||||
|
return isMuted || (chat.topics && !hasUnmutedUnreadTopics);
|
||||||
|
}, [chat, isMuted]);
|
||||||
|
|
||||||
|
const hasUnreadMark = topic ? false : chat.hasUnreadMark;
|
||||||
|
|
||||||
const isShown = Boolean(
|
const isShown = Boolean(
|
||||||
chat.unreadCount || chat.unreadMentionsCount || chat.hasUnreadMark || isPinned || chat.unreadReactionsCount,
|
unreadCount || unreadMentionsCount || hasUnreadMark || isPinned || unreadReactionsCount
|
||||||
|
|| isTopicUnopened,
|
||||||
);
|
);
|
||||||
const isUnread = Boolean(chat.unreadCount || chat.hasUnreadMark);
|
|
||||||
|
const isUnread = Boolean(unreadCount || hasUnreadMark);
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'Badge',
|
'Badge',
|
||||||
isMuted && 'muted',
|
shouldBeMuted && 'muted',
|
||||||
!isUnread && isPinned && 'pinned',
|
!isUnread && isPinned && 'pinned',
|
||||||
isUnread && 'unread',
|
isUnread && 'unread',
|
||||||
);
|
);
|
||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
const unreadReactionsElement = chat.unreadReactionsCount && (
|
const unreadReactionsElement = unreadReactionsCount && (
|
||||||
<div className={buildClassName('Badge reaction', isMuted && 'muted')}>
|
<div className={buildClassName('Badge reaction', shouldBeMuted && 'muted')}>
|
||||||
<i className="icon-heart" />
|
<i className="icon-heart" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const unreadMentionsElement = chat.unreadMentionsCount && (
|
const unreadMentionsElement = unreadMentionsCount && (
|
||||||
<div className="Badge mention">
|
<div className="Badge mention">
|
||||||
<i className="icon-mention" />
|
<i className="icon-mention" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const unreadCountElement = (chat.hasUnreadMark || chat.unreadCount) ? (
|
const unopenedTopicElement = isTopicUnopened && (
|
||||||
|
<div className={buildClassName('Badge unopened', shouldBeMuted && 'muted')} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const unreadCountElement = (hasUnreadMark || unreadCount) ? (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{!chat.hasUnreadMark && <AnimatedCounter text={formatIntegerCompact(chat.unreadCount!)} />}
|
{!hasUnreadMark && <AnimatedCounter text={formatIntegerCompact(unreadCount!)} />}
|
||||||
</div>
|
</div>
|
||||||
) : undefined;
|
) : undefined;
|
||||||
|
|
||||||
@ -54,12 +92,21 @@ const Badge: FC<OwnProps> = ({ chat, isPinned, isMuted }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const elements = [unreadReactionsElement, unreadMentionsElement, unreadCountElement, pinnedElement].filter(Boolean);
|
const elements = [
|
||||||
|
unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, pinnedElement,
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
if (elements.length === 0) return undefined;
|
if (elements.length === 0) return undefined;
|
||||||
|
|
||||||
if (elements.length === 1) return elements[0];
|
if (elements.length === 1) return elements[0];
|
||||||
|
|
||||||
|
if (shouldShowOnlyMostImportant) {
|
||||||
|
const importanceOrderedElements = [
|
||||||
|
unreadMentionsElement, unreadCountElement, unreadReactionsElement, pinnedElement,
|
||||||
|
].filter(Boolean);
|
||||||
|
return importanceOrderedElements[0];
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Badge-wrapper">
|
<div className="Badge-wrapper">
|
||||||
{elements}
|
{elements}
|
||||||
|
|||||||
@ -28,6 +28,14 @@
|
|||||||
.Avatar.online::after {
|
.Avatar.online::after {
|
||||||
border-color: var(--color-chat-hover);
|
border-color: var(--color-chat-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-badge-wrapper {
|
||||||
|
--outline-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ChatCallStatus {
|
||||||
|
border-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
@ -41,12 +49,29 @@
|
|||||||
.Avatar.online::after {
|
.Avatar.online::after {
|
||||||
border-color: var(--color-chat-hover);
|
border-color: var(--color-chat-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ChatCallStatus {
|
||||||
|
border-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active-forum {
|
||||||
|
.status-badge-wrapper {
|
||||||
|
--outline-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 600px) {
|
@media (min-width: 600px) {
|
||||||
&.selected,
|
&.active-forum.forum,
|
||||||
&.selected:hover {
|
&.active-forum.forum:hover {
|
||||||
|
.status-badge-wrapper {
|
||||||
|
--outline-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected:not(.forum),
|
||||||
|
&.selected:not(.forum):hover {
|
||||||
--background-color: var(--color-chat-active) !important;
|
--background-color: var(--color-chat-active) !important;
|
||||||
|
|
||||||
.custom-emoji.custom-color {
|
.custom-emoji.custom-color {
|
||||||
@ -63,6 +88,10 @@
|
|||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ChatCallStatus {
|
||||||
|
border-color: var(--color-chat-active) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.ListItem-button {
|
.ListItem-button {
|
||||||
--background-color: var(--color-chat-active) !important;
|
--background-color: var(--color-chat-active) !important;
|
||||||
--color-text: var(--color-white);
|
--color-text: var(--color-white);
|
||||||
@ -77,25 +106,98 @@
|
|||||||
color: var(--color-white) !important;
|
color: var(--color-white) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.general-forum-icon {
|
||||||
|
color: var(--color-white) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.Badge:not(.pinned) {
|
.Badge:not(.pinned) {
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
color: var(--color-chat-active);
|
color: var(--color-chat-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Badge:not(.pinned).muted {
|
||||||
|
color: var(--color-white);
|
||||||
|
background: #FFFFFF33;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-wrapper-visible .Badge:not(.pinned).muted {
|
||||||
|
background: var(--color-chat-active-greyed);
|
||||||
|
--outline-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-wrapper-visible .Badge:not(.pinned):not(.muted) {
|
||||||
|
--outline-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.smaller .ListItem-button {
|
||||||
|
height: 4.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active-forum::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: -0.5rem;
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 75%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
|
||||||
|
background: var(--color-primary);
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
border-start-end-radius: var(--border-radius-default);
|
||||||
|
border-end-end-radius: var(--border-radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.ListItem-button {
|
.ListItem-button {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ripple-container {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
|
height: 3.375rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0.5rem;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transition-timing-function: cubic-bezier(0.16,1.25,0.64,1);
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
|
||||||
|
--outline-color: var(--color-background);
|
||||||
|
|
||||||
|
.Badge {
|
||||||
|
box-shadow: 0 0 0 2px var(--outline-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge-wrapper-visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transition-property: ease-in-out;
|
||||||
|
transition-property: opacity, transform;
|
||||||
|
|
||||||
.subtitle {
|
.subtitle {
|
||||||
margin-top: -0.125rem;
|
margin-top: -0.125rem;
|
||||||
}
|
}
|
||||||
@ -112,6 +214,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.general-forum-icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.LastMessageMeta {
|
.LastMessageMeta {
|
||||||
body.is-ios & {
|
body.is-ios & {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
@ -218,5 +325,19 @@
|
|||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.active-forum::before {
|
||||||
|
left: auto;
|
||||||
|
right: 0.0625rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.smaller .info {
|
||||||
|
transform: translateX(-25%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[dir="rtl"].smaller .info {
|
||||||
|
transform: translateX(25%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,52 +1,58 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useLayoutEffect, useMemo, useRef,
|
memo, useCallback, useEffect, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { LangFn } from '../../../hooks/useLang';
|
|
||||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiUser, ApiMessage, ApiMessageOutgoingStatus, ApiFormattedText, ApiUserStatus,
|
ApiChat,
|
||||||
|
ApiUser,
|
||||||
|
ApiMessage,
|
||||||
|
ApiMessageOutgoingStatus,
|
||||||
|
ApiFormattedText,
|
||||||
|
ApiUserStatus,
|
||||||
|
ApiTopic,
|
||||||
|
ApiTypingStatus,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { AnimationLevel } from '../../../types';
|
import type { AnimationLevel } from '../../../types';
|
||||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
import type { ChatAnimationTypes } from './hooks';
|
||||||
|
|
||||||
import { ANIMATION_END_DELAY } from '../../../config';
|
import { ANIMATION_END_DELAY } from '../../../config';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
import {
|
import {
|
||||||
isUserId,
|
isUserId,
|
||||||
isActionMessage,
|
|
||||||
getPrivateChatUserId,
|
getPrivateChatUserId,
|
||||||
getMessageAction,
|
getMessageAction,
|
||||||
getMessageSenderName,
|
|
||||||
isChatChannel,
|
|
||||||
getMessageMediaHash,
|
|
||||||
getMessageMediaThumbDataUri,
|
|
||||||
getMessageVideo,
|
|
||||||
getMessageSticker,
|
|
||||||
selectIsChatMuted,
|
selectIsChatMuted,
|
||||||
getMessageRoundVideo,
|
|
||||||
} from '../../../global/helpers';
|
} from '../../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectChat, selectUser, selectChatMessage, selectOutgoingStatus, selectDraft, selectCurrentMessageList,
|
selectChat,
|
||||||
selectNotifySettings, selectNotifyExceptions, selectUserStatus, selectIsDefaultEmojiStatusPack,
|
selectUser,
|
||||||
|
selectChatMessage,
|
||||||
|
selectOutgoingStatus,
|
||||||
|
selectDraft,
|
||||||
|
selectCurrentMessageList,
|
||||||
|
selectNotifySettings,
|
||||||
|
selectNotifyExceptions,
|
||||||
|
selectUserStatus,
|
||||||
|
selectIsDefaultEmojiStatusPack,
|
||||||
|
selectTopicFromMessage,
|
||||||
|
selectThreadParam,
|
||||||
|
selectIsForumPanelOpen,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import { renderActionMessageText } from '../../common/helpers/renderActionMessageText';
|
|
||||||
import renderText from '../../common/helpers/renderText';
|
|
||||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
|
||||||
import { fastRaf } from '../../../util/schedulers';
|
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { fastRaf } from '../../../util/schedulers';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
|
|
||||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
|
||||||
import useChatContextActions from '../../../hooks/useChatContextActions';
|
import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useMedia from '../../../hooks/useMedia';
|
import useChatListEntry from './hooks/useChatListEntry';
|
||||||
import { ChatAnimationTypes } from './hooks';
|
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||||
import useLang from '../../../hooks/useLang';
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import Avatar from '../../common/Avatar';
|
||||||
import TypingStatus from '../../common/TypingStatus';
|
|
||||||
import LastMessageMeta from '../../common/LastMessageMeta';
|
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||||
import DeleteChatModal from '../../common/DeleteChatModal';
|
import DeleteChatModal from '../../common/DeleteChatModal';
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
@ -55,17 +61,19 @@ import ChatFolderModal from '../ChatFolderModal.async';
|
|||||||
import ChatCallStatus from './ChatCallStatus';
|
import ChatCallStatus from './ChatCallStatus';
|
||||||
import ReportModal from '../../common/ReportModal';
|
import ReportModal from '../../common/ReportModal';
|
||||||
import FullNameTitle from '../../common/FullNameTitle';
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
import MessageSummary from '../../common/MessageSummary';
|
|
||||||
|
|
||||||
import './Chat.scss';
|
import './Chat.scss';
|
||||||
|
|
||||||
|
const TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY = 300;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
style?: string;
|
|
||||||
chatId: string;
|
chatId: string;
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
orderDiff: number;
|
orderDiff: number;
|
||||||
animationType: ChatAnimationTypes;
|
animationType: ChatAnimationTypes;
|
||||||
isPinned?: boolean;
|
isPinned?: boolean;
|
||||||
|
offsetTopInSmallerMode: number;
|
||||||
|
offsetTop: number;
|
||||||
observeIntersection?: ObserveFn;
|
observeIntersection?: ObserveFn;
|
||||||
onDragEnter?: (chatId: string) => void;
|
onDragEnter?: (chatId: string) => void;
|
||||||
};
|
};
|
||||||
@ -79,20 +87,21 @@ type StateProps = {
|
|||||||
actionTargetUserIds?: string[];
|
actionTargetUserIds?: string[];
|
||||||
actionTargetMessage?: ApiMessage;
|
actionTargetMessage?: ApiMessage;
|
||||||
actionTargetChatId?: string;
|
actionTargetChatId?: string;
|
||||||
lastMessageSender?: ApiUser;
|
lastMessageSender?: ApiUser | ApiChat;
|
||||||
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
draft?: ApiFormattedText;
|
draft?: ApiFormattedText;
|
||||||
animationLevel?: AnimationLevel;
|
animationLevel?: AnimationLevel;
|
||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
|
isForumPanelActive?: boolean;
|
||||||
canScrollDown?: boolean;
|
canScrollDown?: boolean;
|
||||||
canChangeFolder?: boolean;
|
canChangeFolder?: boolean;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
|
lastMessageTopic?: ApiTopic;
|
||||||
|
typingStatus?: ApiTypingStatus;
|
||||||
|
forumPanelChatId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ANIMATION_DURATION = 200;
|
|
||||||
|
|
||||||
const Chat: FC<OwnProps & StateProps> = ({
|
const Chat: FC<OwnProps & StateProps> = ({
|
||||||
style,
|
|
||||||
chatId,
|
chatId,
|
||||||
folderId,
|
folderId,
|
||||||
orderDiff,
|
orderDiff,
|
||||||
@ -109,22 +118,28 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
lastMessageOutgoingStatus,
|
lastMessageOutgoingStatus,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
|
offsetTopInSmallerMode,
|
||||||
|
offsetTop,
|
||||||
draft,
|
draft,
|
||||||
animationLevel,
|
animationLevel,
|
||||||
isSelected,
|
isSelected,
|
||||||
|
isForumPanelActive,
|
||||||
canScrollDown,
|
canScrollDown,
|
||||||
canChangeFolder,
|
canChangeFolder,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
lastMessageTopic,
|
||||||
|
typingStatus,
|
||||||
|
forumPanelChatId,
|
||||||
onDragEnter,
|
onDragEnter,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
openChat,
|
openChat,
|
||||||
|
openForumPanel,
|
||||||
|
closeForumPanel,
|
||||||
focusLastMessage,
|
focusLastMessage,
|
||||||
|
loadTopics,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||||
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
|
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
|
||||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||||
@ -132,74 +147,40 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
|
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
|
||||||
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
|
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
|
||||||
|
|
||||||
const { lastMessage, typingStatus } = chat || {};
|
const { lastMessage, isForum } = chat || {};
|
||||||
const isAction = lastMessage && isActionMessage(lastMessage);
|
|
||||||
|
|
||||||
useEnsureMessage(chatId, isAction ? lastMessage.replyToMessageId : undefined, actionTargetMessage);
|
const { renderSubtitle, ref } = useChatListEntry({
|
||||||
|
chat,
|
||||||
|
chatId,
|
||||||
|
lastMessage,
|
||||||
|
typingStatus,
|
||||||
|
draft,
|
||||||
|
actionTargetMessage,
|
||||||
|
actionTargetUserIds,
|
||||||
|
actionTargetChatId,
|
||||||
|
lastMessageTopic,
|
||||||
|
lastMessageSender,
|
||||||
|
observeIntersection,
|
||||||
|
|
||||||
const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage)
|
animationType,
|
||||||
? getMessageMediaThumbDataUri(lastMessage)
|
animationLevel,
|
||||||
: undefined;
|
orderDiff,
|
||||||
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
});
|
||||||
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
|
||||||
|
|
||||||
const actionTargetUsers = useMemo(() => {
|
|
||||||
if (!actionTargetUserIds) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need for expensive global updates on users, so we avoid them
|
|
||||||
const usersById = getGlobal().users.byId;
|
|
||||||
return actionTargetUserIds.map((userId) => usersById[userId]).filter(Boolean);
|
|
||||||
}, [actionTargetUserIds]);
|
|
||||||
|
|
||||||
// Sets animation excess values when `orderDiff` changes and then resets excess values to animate.
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const element = ref.current;
|
|
||||||
|
|
||||||
if (animationLevel === 0 || !element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Refactor animation: create `useListAnimation` that owns `orderDiff` and `animationType`
|
|
||||||
if (animationType === ChatAnimationTypes.Opacity) {
|
|
||||||
element.style.opacity = '0';
|
|
||||||
|
|
||||||
fastRaf(() => {
|
|
||||||
element.classList.add('animate-opacity');
|
|
||||||
element.style.opacity = '1';
|
|
||||||
});
|
|
||||||
} else if (animationType === ChatAnimationTypes.Move) {
|
|
||||||
element.style.transform = `translate3d(0, ${-orderDiff * 100}%, 0)`;
|
|
||||||
|
|
||||||
fastRaf(() => {
|
|
||||||
element.classList.add('animate-transform');
|
|
||||||
element.style.transform = '';
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
fastRaf(() => {
|
|
||||||
element.classList.remove('animate-opacity', 'animate-transform');
|
|
||||||
element.style.opacity = '';
|
|
||||||
element.style.transform = '';
|
|
||||||
});
|
|
||||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
|
||||||
}, [animationLevel, orderDiff, animationType]);
|
|
||||||
|
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
|
if (chat?.isForum) {
|
||||||
|
openForumPanel({ chatId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (forumPanelChatId) closeForumPanel();
|
||||||
openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
|
openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
|
||||||
|
|
||||||
if (isSelected && canScrollDown) {
|
if (isSelected && canScrollDown) {
|
||||||
focusLastMessage();
|
focusLastMessage();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isSelected,
|
chat?.isForum, forumPanelChatId, closeForumPanel, openChat, chatId, isSelected, canScrollDown, openForumPanel,
|
||||||
canScrollDown,
|
|
||||||
openChat,
|
|
||||||
chatId,
|
|
||||||
focusLastMessage,
|
focusLastMessage,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -235,79 +216,66 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
canChangeFolder,
|
canChangeFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lang = useLang();
|
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||||
|
|
||||||
|
// Load the forum topics to display unread count badge
|
||||||
|
useEffect(() => {
|
||||||
|
if (isIntersecting && lastSyncTime && isForum && chat && chat.topics === undefined) {
|
||||||
|
loadTopics({ chatId });
|
||||||
|
}
|
||||||
|
}, [chat, chatId, isForum, isIntersecting, lastSyncTime, loadTopics]);
|
||||||
|
|
||||||
|
const isOnForumPanel = chatId === forumPanelChatId;
|
||||||
|
const prevIsForumPanelActive = usePrevious(isForumPanelActive);
|
||||||
|
const isAnimatingRef = useRef(false);
|
||||||
|
|
||||||
|
if (prevIsForumPanelActive !== isForumPanelActive) {
|
||||||
|
isAnimatingRef.current = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animate changing to smaller chat size when navigating to/from forum topic list
|
||||||
|
useEffect(() => {
|
||||||
|
const current = ref.current;
|
||||||
|
|
||||||
|
if (current && isAnimatingRef.current && isForumPanelActive !== prevIsForumPanelActive) {
|
||||||
|
current.classList.add('animate-transform');
|
||||||
|
current.style.transform = '';
|
||||||
|
setTimeout(() => {
|
||||||
|
// Wait one more frame for better animation performance
|
||||||
|
fastRaf(() => {
|
||||||
|
isAnimatingRef.current = false;
|
||||||
|
current.classList.remove('animate-transform');
|
||||||
|
});
|
||||||
|
}, TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY + ANIMATION_END_DELAY);
|
||||||
|
}
|
||||||
|
}, [ref, isForumPanelActive, prevIsForumPanelActive]);
|
||||||
|
|
||||||
if (!chat) {
|
if (!chat) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLastMessageOrTyping() {
|
|
||||||
if (typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
|
|
||||||
return <TypingStatus typingStatus={typingStatus} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (draft?.text.length) {
|
|
||||||
return (
|
|
||||||
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
|
||||||
<span className="draft">{lang('Draft')}</span>
|
|
||||||
{renderTextWithEntities(draft.text, draft.entities, undefined, undefined, undefined, undefined, true)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!lastMessage) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAction) {
|
|
||||||
const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
|
||||||
{renderActionMessageText(
|
|
||||||
lang,
|
|
||||||
lastMessage,
|
|
||||||
!isChat ? lastMessageSender : undefined,
|
|
||||||
isChat ? chat : undefined,
|
|
||||||
actionTargetUsers,
|
|
||||||
actionTargetMessage,
|
|
||||||
actionTargetChatId,
|
|
||||||
{ isEmbedded: true },
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const senderName = getMessageSenderName(lang, chatId, lastMessageSender);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
|
||||||
{senderName && (
|
|
||||||
<>
|
|
||||||
<span className="sender-name">{renderText(senderName)}</span>
|
|
||||||
<span className="colon">:</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
|
||||||
</p>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'Chat chat-item-clickable',
|
'Chat chat-item-clickable',
|
||||||
isUserId(chatId) ? 'private' : 'group',
|
isUserId(chatId) ? 'private' : 'group',
|
||||||
|
isForum && 'forum',
|
||||||
isSelected && 'selected',
|
isSelected && 'selected',
|
||||||
|
isForumPanelActive && 'smaller',
|
||||||
|
isOnForumPanel && 'active-forum',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const chatTop = isForumPanelActive ? (offsetTop - offsetTopInSmallerMode) : offsetTop;
|
||||||
|
const offsetAnimate = isForumPanelActive ? offsetTopInSmallerMode : -offsetTopInSmallerMode;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={className}
|
className={className}
|
||||||
style={style}
|
style={buildStyle(`top: ${chatTop}px`, isAnimatingRef.current && `transform: translateY(${offsetAnimate}px)`)}
|
||||||
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
ripple={!isForum && !IS_SINGLE_COLUMN_LAYOUT}
|
||||||
contextActions={contextActions}
|
contextActions={contextActions}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
|
shouldUsePortalForMenu={isForumPanelActive}
|
||||||
>
|
>
|
||||||
<div className="status">
|
<div className="status">
|
||||||
<Avatar
|
<Avatar
|
||||||
@ -320,6 +288,13 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
withVideo
|
withVideo
|
||||||
observeIntersection={observeIntersection}
|
observeIntersection={observeIntersection}
|
||||||
/>
|
/>
|
||||||
|
<div className={buildClassName(
|
||||||
|
'status-badge-wrapper',
|
||||||
|
isForumPanelActive && 'status-badge-wrapper-visible',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant />
|
||||||
|
</div>
|
||||||
{chat.isCallActive && chat.isCallNotEmpty && (
|
{chat.isCallActive && chat.isCallNotEmpty && (
|
||||||
<ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} />
|
<ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} />
|
||||||
)}
|
)}
|
||||||
@ -343,7 +318,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{renderLastMessageOrTyping()}
|
{renderSubtitle()}
|
||||||
<Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
<Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -376,31 +351,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderSummary(
|
|
||||||
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
|
||||||
) {
|
|
||||||
const messageSummary = (
|
|
||||||
<MessageSummary
|
|
||||||
lang={lang}
|
|
||||||
message={message}
|
|
||||||
noEmoji={Boolean(blobUrl)}
|
|
||||||
observeIntersectionForLoading={observeIntersection}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!blobUrl) {
|
|
||||||
return messageSummary;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className="media-preview">
|
|
||||||
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
|
||||||
{getMessageVideo(message) && <i className="icon-play" />}
|
|
||||||
{messageSummary}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId }): StateProps => {
|
(global, { chatId }): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
@ -409,7 +359,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage || {};
|
const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage || {};
|
||||||
const lastMessageSender = senderId ? selectUser(global, senderId) : undefined;
|
const lastMessageSender = senderId
|
||||||
|
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
|
||||||
const lastMessageAction = chat.lastMessage ? getMessageAction(chat.lastMessage) : undefined;
|
const lastMessageAction = chat.lastMessage ? getMessageAction(chat.lastMessage) : undefined;
|
||||||
const actionTargetMessage = lastMessageAction && replyToMessageId
|
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||||
@ -421,12 +372,16 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
threadId: currentThreadId,
|
threadId: currentThreadId,
|
||||||
type: messageListType,
|
type: messageListType,
|
||||||
} = selectCurrentMessageList(global) || {};
|
} = selectCurrentMessageList(global) || {};
|
||||||
|
const isForumPanelActive = selectIsForumPanelOpen(global);
|
||||||
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
|
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
|
||||||
|
|
||||||
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||||
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
|
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
|
||||||
const statusEmoji = user?.emojiStatus && global.customEmojis.byId[user.emojiStatus.documentId];
|
const statusEmoji = user?.emojiStatus && global.customEmojis.byId[user.emojiStatus.documentId];
|
||||||
const isEmojiStatusColored = statusEmoji && selectIsDefaultEmojiStatusPack(global, statusEmoji.stickerSetInfo);
|
const isEmojiStatusColored = statusEmoji && selectIsDefaultEmojiStatusPack(global, statusEmoji.stickerSetInfo);
|
||||||
|
const lastMessageTopic = chat.lastMessage && selectTopicFromMessage(global, chat.lastMessage);
|
||||||
|
|
||||||
|
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
@ -437,6 +392,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
||||||
animationLevel: global.settings.byKey.animationLevel,
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
|
isForumPanelActive,
|
||||||
isSelected,
|
isSelected,
|
||||||
canScrollDown: isSelected && messageListType === 'thread',
|
canScrollDown: isSelected && messageListType === 'thread',
|
||||||
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
|
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
|
||||||
@ -447,6 +403,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
user,
|
user,
|
||||||
userStatus,
|
userStatus,
|
||||||
isEmojiStatusColored,
|
isEmojiStatusColored,
|
||||||
|
lastMessageTopic,
|
||||||
|
typingStatus,
|
||||||
|
forumPanelChatId: global.forumPanelChatId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(Chat));
|
)(Chat));
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import ChatList from './ChatList';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
onScreenSelect: (screen: SettingsScreens) => void;
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
foldersDispatch: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
|
shouldHideFolderTabs?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -51,6 +52,7 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
|
shouldHideFolderTabs,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadChatFolders,
|
loadChatFolders,
|
||||||
@ -221,7 +223,12 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
const shouldRenderFolders = folderTabs && folderTabs.length > 1;
|
const shouldRenderFolders = folderTabs && folderTabs.length > 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ChatFolders">
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
'ChatFolders',
|
||||||
|
shouldRenderFolders && shouldHideFolderTabs && 'ChatFolders--tabs-hidden',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{shouldRenderFolders ? (
|
{shouldRenderFolders ? (
|
||||||
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} areFolders />
|
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} areFolders />
|
||||||
) : shouldRenderPlaceholder ? (
|
) : shouldRenderPlaceholder ? (
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useMemo, useEffect, useRef, useCallback,
|
memo, useEffect, useRef, useCallback, useMemo,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../global';
|
import { getActions, getGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import type { SettingsScreens } from '../../../types';
|
import type { SettingsScreens } from '../../../types';
|
||||||
@ -9,21 +9,20 @@ import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReduc
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ALL_FOLDER_ID,
|
ALL_FOLDER_ID,
|
||||||
ARCHIVED_FOLDER_ID,
|
ARCHIVED_FOLDER_ID, CHAT_HEIGHT_FORUM_PX,
|
||||||
CHAT_HEIGHT_PX,
|
CHAT_HEIGHT_PX,
|
||||||
CHAT_LIST_SLICE,
|
CHAT_LIST_SLICE,
|
||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { IS_MAC_OS, IS_PWA } from '../../../util/environment';
|
import { IS_MAC_OS, IS_PWA } from '../../../util/environment';
|
||||||
import { mapValues } from '../../../util/iteratees';
|
|
||||||
import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager';
|
import { getPinnedChatsCount, getOrderKey } from '../../../util/folderManager';
|
||||||
|
import { selectChat } from '../../../global/selectors';
|
||||||
|
|
||||||
import usePrevious from '../../../hooks/usePrevious';
|
|
||||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
|
||||||
import { useChatAnimationType } from './hooks';
|
|
||||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
import useDebouncedCallback from '../../../hooks/useDebouncedCallback';
|
||||||
|
import useChatOrderDiff from './hooks/useChatOrderDiff';
|
||||||
|
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import Loading from '../../ui/Loading';
|
import Loading from '../../ui/Loading';
|
||||||
@ -60,28 +59,7 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
|
||||||
|
|
||||||
const orderById = useMemo(() => {
|
const { orderDiffById, getAnimationType } = useChatOrderDiff(orderedIds);
|
||||||
if (!orderedIds) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return orderedIds.reduce((acc, id, i) => {
|
|
||||||
acc[id] = i;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>);
|
|
||||||
}, [orderedIds]);
|
|
||||||
|
|
||||||
const prevOrderById = usePrevious(orderById);
|
|
||||||
|
|
||||||
const orderDiffById = useMemo(() => {
|
|
||||||
if (!orderById || !prevOrderById) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapValues(orderById, (order, id) => {
|
|
||||||
return prevOrderById[id] !== undefined ? order - prevOrderById[id] : -Infinity;
|
|
||||||
});
|
|
||||||
}, [orderById, prevOrderById]);
|
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
||||||
|
|
||||||
@ -122,8 +100,6 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
};
|
};
|
||||||
}, [isActive, openChat, openNextChat, orderedIds]);
|
}, [isActive, openChat, openNextChat, orderedIds]);
|
||||||
|
|
||||||
const getAnimationType = useChatAnimationType(orderDiffById);
|
|
||||||
|
|
||||||
const { observe } = useIntersectionObserver({
|
const { observe } = useIntersectionObserver({
|
||||||
rootRef: containerRef,
|
rootRef: containerRef,
|
||||||
throttleMs: INTERSECTION_THROTTLE,
|
throttleMs: INTERSECTION_THROTTLE,
|
||||||
@ -145,12 +121,31 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
shouldIgnoreDragRef.current = true;
|
shouldIgnoreDragRef.current = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const viewportOffsetPx = useMemo(() => {
|
||||||
|
if (!viewportIds?.length) return 0;
|
||||||
|
const global = getGlobal();
|
||||||
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
|
return orderedIds!.reduce((acc, id, i) => {
|
||||||
|
if (i >= viewportOffset) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return acc + (selectChat(global, id)!.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
||||||
|
}, 0);
|
||||||
|
}, [orderedIds, viewportIds]);
|
||||||
|
|
||||||
function renderChats() {
|
function renderChats() {
|
||||||
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
|
const global = getGlobal();
|
||||||
|
|
||||||
const pinnedCount = getPinnedChatsCount(resolvedFolderId) || 0;
|
const pinnedCount = getPinnedChatsCount(resolvedFolderId) || 0;
|
||||||
|
|
||||||
|
let currentChatListHeight = viewportOffsetPx;
|
||||||
|
|
||||||
return viewportIds!.map((id, i) => {
|
return viewportIds!.map((id, i) => {
|
||||||
const isPinned = viewportOffset + i < pinnedCount;
|
const isPinned = viewportOffset + i < pinnedCount;
|
||||||
|
const chatTop = currentChatListHeight;
|
||||||
|
const chatTopSmaller = (viewportOffset + i) * CHAT_HEIGHT_PX;
|
||||||
|
currentChatListHeight += (selectChat(global, id)!.isForum ? CHAT_HEIGHT_FORUM_PX : CHAT_HEIGHT_PX);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chat
|
<Chat
|
||||||
@ -161,7 +156,8 @@ const ChatList: FC<OwnProps> = ({
|
|||||||
folderId={folderId}
|
folderId={folderId}
|
||||||
animationType={getAnimationType(id)}
|
animationType={getAnimationType(id)}
|
||||||
orderDiff={orderDiffById[id]}
|
orderDiff={orderDiffById[id]}
|
||||||
style={`top: ${(viewportOffset + i) * CHAT_HEIGHT_PX}px;`}
|
offsetTop={chatTop}
|
||||||
|
offsetTopInSmallerMode={chatTop - chatTopSmaller}
|
||||||
observeIntersection={observe}
|
observeIntersection={observe}
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.EmptyFolder {
|
.root {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 80%;
|
height: 80%;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -11,13 +11,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sticker {
|
.sticker {
|
||||||
height: 8rem;
|
height: 6rem;
|
||||||
margin-bottom: 1.875rem;
|
margin-bottom: 1.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
margin-bottom: 0.125rem;
|
margin-bottom: 0.125rem;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
@ -30,12 +33,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button.pill {
|
:global(.Button.pill) {
|
||||||
|
max-width: 100%;
|
||||||
margin-top: 0.625rem;
|
margin-top: 0.625rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding-inline-start: 0.75rem;
|
padding-inline-start: 0.75rem;
|
||||||
unicode-bidi: plaintext;
|
unicode-bidi: plaintext;
|
||||||
|
|
||||||
|
justify-content: start;
|
||||||
|
|
||||||
|
.button-text {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
margin-inline-end: 0.625rem;
|
margin-inline-end: 0.625rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@ -13,7 +13,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||||
|
|
||||||
import './EmptyFolder.scss';
|
import styles from './EmptyFolder.module.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
folderId?: number;
|
folderId?: number;
|
||||||
@ -27,7 +27,7 @@ type StateProps = {
|
|||||||
animatedEmoji?: ApiSticker;
|
animatedEmoji?: ApiSticker;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ICON_SIZE = 128;
|
const ICON_SIZE = 96;
|
||||||
|
|
||||||
const EmptyFolder: FC<OwnProps & StateProps> = ({
|
const EmptyFolder: FC<OwnProps & StateProps> = ({
|
||||||
chatFolder, animatedEmoji, foldersDispatch, onScreenSelect,
|
chatFolder, animatedEmoji, foldersDispatch, onScreenSelect,
|
||||||
@ -40,12 +40,12 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
|
|||||||
}, [chatFolder, foldersDispatch, onScreenSelect]);
|
}, [chatFolder, foldersDispatch, onScreenSelect]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="EmptyFolder">
|
<div className={styles.root}>
|
||||||
<div className="sticker">
|
<div className={styles.sticker}>
|
||||||
{animatedEmoji && <AnimatedIconFromSticker sticker={animatedEmoji} size={ICON_SIZE} />}
|
{animatedEmoji && <AnimatedIconFromSticker sticker={animatedEmoji} size={ICON_SIZE} />}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="title" dir="auto">{lang('FilterNoChatsToDisplay')}</h3>
|
<h3 className={styles.title} dir="auto">{lang('FilterNoChatsToDisplay')}</h3>
|
||||||
<p className="description" dir="auto">
|
<p className={styles.description} dir="auto">
|
||||||
{lang(chatFolder ? 'ChatList.EmptyChatListFilterText' : 'Chat.EmptyChat')}
|
{lang(chatFolder ? 'ChatList.EmptyChatListFilterText' : 'Chat.EmptyChat')}
|
||||||
</p>
|
</p>
|
||||||
{chatFolder && foldersDispatch && onScreenSelect && (
|
{chatFolder && foldersDispatch && onScreenSelect && (
|
||||||
@ -58,7 +58,9 @@ const EmptyFolder: FC<OwnProps & StateProps> = ({
|
|||||||
isRtl={lang.isRtl}
|
isRtl={lang.isRtl}
|
||||||
>
|
>
|
||||||
<i className="icon-settings" />
|
<i className="icon-settings" />
|
||||||
{lang('ChatList.EmptyChatListEditFilter')}
|
<div className={styles.buttonText}>
|
||||||
|
{lang('ChatList.EmptyChatListEditFilter')}
|
||||||
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
58
src/components/left/main/EmptyTopic.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiSticker } from '../../../api/types';
|
||||||
|
|
||||||
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
|
import { selectAnimatedEmoji } from '../../../global/selectors';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||||
|
|
||||||
|
import styles from './EmptyFolder.module.scss';
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
animatedEmoji?: ApiSticker;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ICON_SIZE = 96;
|
||||||
|
|
||||||
|
// TODO[forums] Open create topic screen if has permission
|
||||||
|
const EmptyTopic: FC<StateProps> = ({
|
||||||
|
animatedEmoji,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleCreateTopic = useCallback(() => {
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<div className={styles.sticker}>
|
||||||
|
{animatedEmoji && <AnimatedIconFromSticker sticker={animatedEmoji} size={ICON_SIZE} />}
|
||||||
|
</div>
|
||||||
|
<h3 className={styles.title} dir="auto">{lang('ChatList.EmptyTopicsTitle')}</h3>
|
||||||
|
<Button
|
||||||
|
ripple={!IS_SINGLE_COLUMN_LAYOUT}
|
||||||
|
fluid
|
||||||
|
pill
|
||||||
|
onClick={handleCreateTopic}
|
||||||
|
size="smaller"
|
||||||
|
isRtl={lang.isRtl}
|
||||||
|
>
|
||||||
|
<i className="icon-add" />
|
||||||
|
<div className={styles.buttonText}>
|
||||||
|
{lang('ChatList.EmptyTopicsCreate')}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal((global): StateProps => {
|
||||||
|
return {
|
||||||
|
animatedEmoji: selectAnimatedEmoji(global, '👀'),
|
||||||
|
};
|
||||||
|
})(EmptyTopic));
|
||||||
96
src/components/left/main/ForumPanel.module.scss
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
.root {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
left: 4.75rem;
|
||||||
|
z-index: var(--z-forum-panel);
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
border-left: 1px solid var(--color-borders);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.rtl {
|
||||||
|
left: 0;
|
||||||
|
right: 4.75rem;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
border-left: none;
|
||||||
|
border-right: 1px solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
transition: transform var(--slide-transition);
|
||||||
|
transform: translate3d(100%, 0, 0);
|
||||||
|
|
||||||
|
:global(.chat-list) {
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-top: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.HeaderActions) {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-call {
|
||||||
|
position: static !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-bottom {
|
||||||
|
width: 100%;
|
||||||
|
height: 0;
|
||||||
|
transition: 0.1s ease-out border-color;
|
||||||
|
border-bottom: 0.0625rem solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrolled .border-bottom {
|
||||||
|
border-color: var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-top-handler {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-left: 0.4375rem;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
:global(.info) {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.fullName) {
|
||||||
|
line-height: 1.375rem;
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
font-size: 1rem !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.status) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
256
src/components/left/main/ForumPanel.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import React, {
|
||||||
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type { ApiChat } from '../../../api/types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TOPICS_SLICE, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA,
|
||||||
|
} from '../../../config';
|
||||||
|
import { selectChat, selectCurrentMessageList } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { fastRaf } from '../../../util/schedulers';
|
||||||
|
import { getOrderedTopics } from '../../../global/helpers';
|
||||||
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
||||||
|
|
||||||
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
|
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import useChatOrderDiff from './hooks/useChatOrderDiff';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import usePrevious from '../../../hooks/usePrevious';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
|
||||||
|
|
||||||
|
import GroupChatInfo from '../../common/GroupChatInfo';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import Topic from './Topic';
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
import Loading from '../../ui/Loading';
|
||||||
|
import HeaderActions from '../../middle/HeaderActions';
|
||||||
|
import GroupCallTopPane from '../../calls/group/GroupCallTopPane';
|
||||||
|
import EmptyTopic from './EmptyTopic';
|
||||||
|
|
||||||
|
import styles from './ForumPanel.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
isHidden?: boolean;
|
||||||
|
onTopicSearch?: NoneToVoidFunction;
|
||||||
|
onCloseAnimationEnd?: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
currentTopicId?: number;
|
||||||
|
lastSyncTime?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTERSECTION_THROTTLE = 200;
|
||||||
|
|
||||||
|
const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||||
|
chat,
|
||||||
|
currentTopicId,
|
||||||
|
isOpen,
|
||||||
|
isHidden,
|
||||||
|
lastSyncTime,
|
||||||
|
onTopicSearch,
|
||||||
|
onCloseAnimationEnd,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
closeForumPanel, openChatWithInfo, loadTopics,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const scrollTopHandlerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (lastSyncTime && chat && !chat.topics) {
|
||||||
|
loadTopics({ chatId: chat.id });
|
||||||
|
}
|
||||||
|
}, [chat, lastSyncTime, loadTopics]);
|
||||||
|
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
closeForumPanel();
|
||||||
|
}, [closeForumPanel]);
|
||||||
|
|
||||||
|
const handleToggleChatInfo = useCallback(() => {
|
||||||
|
if (!chat) return;
|
||||||
|
openChatWithInfo({ id: chat.id, shouldReplaceHistory: true });
|
||||||
|
}, [chat, openChatWithInfo]);
|
||||||
|
|
||||||
|
const { observe } = useIntersectionObserver({
|
||||||
|
rootRef: containerRef,
|
||||||
|
throttleMs: INTERSECTION_THROTTLE,
|
||||||
|
});
|
||||||
|
|
||||||
|
useOnIntersect(scrollTopHandlerRef, observe, ({ isIntersecting }) => {
|
||||||
|
setIsScrolled(!isIntersecting);
|
||||||
|
});
|
||||||
|
|
||||||
|
const orderedIds = useMemo(() => {
|
||||||
|
return chat?.topics
|
||||||
|
? getOrderedTopics(Object.values(chat.topics), chat.orderedPinnedTopicIds).map(({ id }) => id)
|
||||||
|
: [];
|
||||||
|
}, [chat]);
|
||||||
|
|
||||||
|
const { orderDiffById, getAnimationType } = useChatOrderDiff(orderedIds);
|
||||||
|
|
||||||
|
const [viewportIds, getMore] = useInfiniteScroll(() => {
|
||||||
|
if (!chat || !lastSyncTime) return;
|
||||||
|
loadTopics({ chatId: chat.id });
|
||||||
|
}, orderedIds, !chat?.topicsCount || orderedIds.length >= chat.topicsCount, TOPICS_SLICE);
|
||||||
|
|
||||||
|
const shouldRenderRef = useRef(false);
|
||||||
|
const isVisible = isOpen && !isHidden;
|
||||||
|
const prevIsVisible = usePrevious(isVisible);
|
||||||
|
|
||||||
|
if (prevIsVisible !== isVisible) {
|
||||||
|
shouldRenderRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive: isVisible,
|
||||||
|
onBack: handleClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => (isVisible ? captureEscKeyListener(handleClose) : undefined), [handleClose, isVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevIsVisible !== isVisible) {
|
||||||
|
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||||
|
waitForTransitionEnd(ref.current!, () => {
|
||||||
|
dispatchHeavyAnimationStop();
|
||||||
|
});
|
||||||
|
|
||||||
|
// For performance reasons, we delay animation of the topic list panel to the next animation frame
|
||||||
|
fastRaf(() => {
|
||||||
|
if (isVisible) {
|
||||||
|
shouldRenderRef.current = true;
|
||||||
|
ref.current!.style.transform = 'none';
|
||||||
|
} else {
|
||||||
|
shouldRenderRef.current = false;
|
||||||
|
ref.current!.style.transform = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isVisible, prevIsVisible]);
|
||||||
|
|
||||||
|
function renderTopics() {
|
||||||
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
||||||
|
|
||||||
|
return viewportIds?.map((id, i) => (
|
||||||
|
<Topic
|
||||||
|
key={id}
|
||||||
|
chatId={chat!.id}
|
||||||
|
topic={chat!.topics![id]}
|
||||||
|
style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
|
||||||
|
isSelected={currentTopicId === id}
|
||||||
|
observeIntersection={observe}
|
||||||
|
animationType={getAnimationType(id)}
|
||||||
|
orderDiff={orderDiffById[id]}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const isLoading = chat?.topics === undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
isScrolled && styles.scrolled,
|
||||||
|
lang.isRtl && styles.rtl,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
onTransitionEnd={!isOpen ? onCloseAnimationEnd : undefined}
|
||||||
|
>
|
||||||
|
<div className="left-header">
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
onClick={handleClose}
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
>
|
||||||
|
<i className="icon-close" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{chat && (
|
||||||
|
<GroupChatInfo
|
||||||
|
noAvatar
|
||||||
|
className={styles.info}
|
||||||
|
chatId={chat.id}
|
||||||
|
onClick={handleToggleChatInfo}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{chat
|
||||||
|
&& (
|
||||||
|
<HeaderActions
|
||||||
|
chatId={chat.id}
|
||||||
|
threadId={MAIN_THREAD_ID}
|
||||||
|
messageListType="thread"
|
||||||
|
canExpandActions={false}
|
||||||
|
withForumActions
|
||||||
|
onTopicSearch={onTopicSearch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{chat && <GroupCallTopPane chatId={chat.id} hasPinnedOffset={false} className={styles.groupCall} />}
|
||||||
|
|
||||||
|
<div className={styles.borderBottom} />
|
||||||
|
|
||||||
|
<InfiniteScroll
|
||||||
|
className="chat-list custom-scroll"
|
||||||
|
ref={containerRef}
|
||||||
|
items={viewportIds}
|
||||||
|
preloadBackwards={TOPICS_SLICE}
|
||||||
|
withAbsolutePositioning
|
||||||
|
maxHeight={(orderedIds?.length || 0) * TOPIC_HEIGHT_PX}
|
||||||
|
onLoadMore={getMore}
|
||||||
|
sensitiveArea={TOPIC_LIST_SENSITIVE_AREA}
|
||||||
|
beforeChildren={<div ref={scrollTopHandlerRef} className={styles.scrollTopHandler} />}
|
||||||
|
>
|
||||||
|
{viewportIds?.length ? (
|
||||||
|
renderTopics()
|
||||||
|
) : !isLoading ? (
|
||||||
|
<EmptyTopic />
|
||||||
|
) : (
|
||||||
|
<Loading key="loading" />
|
||||||
|
)}
|
||||||
|
</InfiniteScroll>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, ownProps, detachWhenChanged): StateProps => {
|
||||||
|
const chatId = global.forumPanelChatId;
|
||||||
|
detachWhenChanged(chatId);
|
||||||
|
|
||||||
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const {
|
||||||
|
chatId: currentChatId,
|
||||||
|
threadId: currentThreadId,
|
||||||
|
} = selectCurrentMessageList(global) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
lastSyncTime: global.lastSyncTime,
|
||||||
|
currentTopicId: chatId === currentChatId ? currentThreadId : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(ForumPanel));
|
||||||
@ -17,6 +17,14 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: 0.25s ease-out transform;
|
||||||
|
|
||||||
|
&--tabs-hidden {
|
||||||
|
transform: translateY(-3.125rem);
|
||||||
|
|
||||||
|
height: calc(100% + 3.125rem);
|
||||||
|
}
|
||||||
|
|
||||||
.tabs-placeholder {
|
.tabs-placeholder {
|
||||||
height: 2.625rem;
|
height: 2.625rem;
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { IS_TOUCH_ENV } from '../../../util/environment';
|
|||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useForumPanelRender from '../../../hooks/useForumPanelRender';
|
||||||
|
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
import LeftMainHeader from './LeftMainHeader';
|
import LeftMainHeader from './LeftMainHeader';
|
||||||
@ -19,6 +20,7 @@ import LeftSearch from '../search/LeftSearch.async';
|
|||||||
import ContactList from './ContactList.async';
|
import ContactList from './ContactList.async';
|
||||||
import NewChatButton from '../NewChatButton';
|
import NewChatButton from '../NewChatButton';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
|
import ForumPanel from './ForumPanel';
|
||||||
|
|
||||||
import './LeftMain.scss';
|
import './LeftMain.scss';
|
||||||
|
|
||||||
@ -30,9 +32,12 @@ type OwnProps = {
|
|||||||
shouldSkipTransition?: boolean;
|
shouldSkipTransition?: boolean;
|
||||||
foldersDispatch: FolderEditDispatch;
|
foldersDispatch: FolderEditDispatch;
|
||||||
isUpdateAvailable?: boolean;
|
isUpdateAvailable?: boolean;
|
||||||
|
isForumPanelOpen?: boolean;
|
||||||
|
isClosingSearch?: boolean;
|
||||||
onSearchQuery: (query: string) => void;
|
onSearchQuery: (query: string) => void;
|
||||||
onContentChange: (content: LeftColumnContent) => void;
|
onContentChange: (content: LeftColumnContent) => void;
|
||||||
onScreenSelect: (screen: SettingsScreens) => void;
|
onScreenSelect: (screen: SettingsScreens) => void;
|
||||||
|
onTopicSearch: NoneToVoidFunction;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -45,17 +50,23 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
content,
|
content,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
searchDate,
|
searchDate,
|
||||||
|
isClosingSearch,
|
||||||
contactsFilter,
|
contactsFilter,
|
||||||
shouldSkipTransition,
|
shouldSkipTransition,
|
||||||
foldersDispatch,
|
foldersDispatch,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
|
isForumPanelOpen,
|
||||||
onSearchQuery,
|
onSearchQuery,
|
||||||
onContentChange,
|
onContentChange,
|
||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
|
onTopicSearch,
|
||||||
}) => {
|
}) => {
|
||||||
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
const [isNewChatButtonShown, setIsNewChatButtonShown] = useState(IS_TOUCH_ENV);
|
||||||
|
|
||||||
|
const { shouldRenderForumPanel, handleForumPanelAnimationEnd } = useForumPanelRender(isForumPanelOpen);
|
||||||
|
const isForumPanelVisible = isForumPanelOpen && content === LeftColumnContent.ChatList;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderUpdateButton,
|
shouldRender: shouldRenderUpdateButton,
|
||||||
transitionClassNames: updateButtonClassNames,
|
transitionClassNames: updateButtonClassNames,
|
||||||
@ -137,6 +148,7 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||||
>
|
>
|
||||||
<LeftMainHeader
|
<LeftMainHeader
|
||||||
|
shouldHideSearch={isForumPanelVisible}
|
||||||
content={content}
|
content={content}
|
||||||
contactsFilter={contactsFilter}
|
contactsFilter={contactsFilter}
|
||||||
onSearchQuery={onSearchQuery}
|
onSearchQuery={onSearchQuery}
|
||||||
@ -145,6 +157,7 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
onSelectArchived={handleSelectArchived}
|
onSelectArchived={handleSelectArchived}
|
||||||
onReset={onReset}
|
onReset={onReset}
|
||||||
shouldSkipTransition={shouldSkipTransition}
|
shouldSkipTransition={shouldSkipTransition}
|
||||||
|
isClosingSearch={isClosingSearch}
|
||||||
/>
|
/>
|
||||||
<Transition
|
<Transition
|
||||||
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
|
name={shouldSkipTransition ? 'none' : 'zoom-fade'}
|
||||||
@ -156,7 +169,13 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
{(isActive) => {
|
{(isActive) => {
|
||||||
switch (content) {
|
switch (content) {
|
||||||
case LeftColumnContent.ChatList:
|
case LeftColumnContent.ChatList:
|
||||||
return <ChatFolders onScreenSelect={onScreenSelect} foldersDispatch={foldersDispatch} />;
|
return (
|
||||||
|
<ChatFolders
|
||||||
|
shouldHideFolderTabs={isForumPanelVisible}
|
||||||
|
onScreenSelect={onScreenSelect}
|
||||||
|
foldersDispatch={foldersDispatch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case LeftColumnContent.GlobalSearch:
|
case LeftColumnContent.GlobalSearch:
|
||||||
return (
|
return (
|
||||||
<LeftSearch
|
<LeftSearch
|
||||||
@ -183,6 +202,14 @@ const LeftMain: FC<OwnProps> = ({
|
|||||||
{lang('lng_update_telegram')}
|
{lang('lng_update_telegram')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{shouldRenderForumPanel && (
|
||||||
|
<ForumPanel
|
||||||
|
isOpen={isForumPanelOpen}
|
||||||
|
isHidden={!isForumPanelVisible}
|
||||||
|
onTopicSearch={onTopicSearch}
|
||||||
|
onCloseAnimationEnd={handleForumPanelAnimationEnd}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NewChatButton
|
<NewChatButton
|
||||||
isShown={isNewChatButtonShown}
|
isShown={isNewChatButtonShown}
|
||||||
onNewPrivateChat={handleSelectContacts}
|
onNewPrivateChat={handleSelectContacts}
|
||||||
|
|||||||
@ -3,6 +3,29 @@
|
|||||||
#LeftMainHeader {
|
#LeftMainHeader {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
.DropdownMenuFiller {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DropdownMenu.rtl {
|
||||||
|
transition: var(--slide-transition) transform;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&.right-aligned {
|
||||||
|
transform: translateX(calc(clamp(
|
||||||
|
var(--left-column-min-width),
|
||||||
|
var(--left-column-width),
|
||||||
|
var(--left-column-max-width)
|
||||||
|
) - 4.375rem));
|
||||||
|
}
|
||||||
|
|
||||||
|
&.disable-transition {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.animated-menu-icon {
|
.animated-menu-icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
||||||
@ -54,35 +77,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.archived-badge {
|
|
||||||
min-width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
margin-left: auto;
|
|
||||||
background: var(--color-gray);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0 0.4375rem;
|
|
||||||
color: white;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.MenuItem.compact .archived-badge {
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.MenuItem.compact .Switcher {
|
.MenuItem.compact .Switcher {
|
||||||
transform: scale(0.75);
|
transform: scale(0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir="rtl"] .archived-badge {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Menu .bubble {
|
.Menu .bubble {
|
||||||
min-width: 17rem;
|
min-width: 17rem;
|
||||||
@ -102,4 +100,15 @@
|
|||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.SearchInput {
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
transition-timing-function: ease-out;
|
||||||
|
transition-property: opacity;
|
||||||
|
|
||||||
|
&--hidden {
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
|||||||
import { useHotkeys } from '../../../hooks/useHotkeys';
|
import { useHotkeys } from '../../../hooks/useHotkeys';
|
||||||
import { getPromptInstall } from '../../../util/installPrompt';
|
import { getPromptInstall } from '../../../util/installPrompt';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
import useLeftHeaderButtonRtlForumTransition from './hooks/useLeftHeaderButtonRtlForumTransition';
|
||||||
|
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
import DropdownMenu from '../../ui/DropdownMenu';
|
||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
@ -45,8 +46,10 @@ import ConnectionStatusOverlay from '../ConnectionStatusOverlay';
|
|||||||
import './LeftMainHeader.scss';
|
import './LeftMainHeader.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
|
shouldHideSearch?: boolean;
|
||||||
content: LeftColumnContent;
|
content: LeftColumnContent;
|
||||||
contactsFilter: string;
|
contactsFilter: string;
|
||||||
|
isClosingSearch?: boolean;
|
||||||
shouldSkipTransition?: boolean;
|
shouldSkipTransition?: boolean;
|
||||||
onSearchQuery: (query: string) => void;
|
onSearchQuery: (query: string) => void;
|
||||||
onSelectSettings: () => void;
|
onSelectSettings: () => void;
|
||||||
@ -77,9 +80,11 @@ const LEGACY_VERSION_URL = 'https://web.telegram.org/?legacy=1';
|
|||||||
const WEBK_VERSION_URL = 'https://web.telegram.org/k/';
|
const WEBK_VERSION_URL = 'https://web.telegram.org/k/';
|
||||||
|
|
||||||
const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||||
|
shouldHideSearch,
|
||||||
content,
|
content,
|
||||||
contactsFilter,
|
contactsFilter,
|
||||||
onSearchQuery,
|
onSearchQuery,
|
||||||
|
isClosingSearch,
|
||||||
onSelectSettings,
|
onSelectSettings,
|
||||||
onSelectContacts,
|
onSelectContacts,
|
||||||
onSelectArchived,
|
onSelectArchived,
|
||||||
@ -250,12 +255,26 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const versionString = IS_BETA ? `${APP_VERSION} Beta (${APP_REVISION})` : (DEBUG ? APP_REVISION : APP_VERSION);
|
const versionString = IS_BETA ? `${APP_VERSION} Beta (${APP_REVISION})` : (DEBUG ? APP_REVISION : APP_VERSION);
|
||||||
|
|
||||||
|
// Disable dropdown menu RTL animation for resize
|
||||||
|
const {
|
||||||
|
shouldDisableDropdownMenuTransitionRef,
|
||||||
|
handleDropdownMenuTransitionEnd,
|
||||||
|
} = useLeftHeaderButtonRtlForumTransition(shouldHideSearch);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LeftMainHeader">
|
<div className="LeftMainHeader">
|
||||||
<div id="LeftMainHeader" className="left-header">
|
<div id="LeftMainHeader" className="left-header">
|
||||||
|
{lang.isRtl && <div className="DropdownMenuFiller" />}
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
trigger={MainButton}
|
trigger={MainButton}
|
||||||
footer={`${APP_NAME} ${versionString}`}
|
footer={`${APP_NAME} ${versionString}`}
|
||||||
|
className={buildClassName(
|
||||||
|
lang.isRtl && 'rtl',
|
||||||
|
shouldHideSearch && lang.isRtl && 'right-aligned',
|
||||||
|
shouldDisableDropdownMenuTransitionRef.current && lang.isRtl && 'disable-transition',
|
||||||
|
)}
|
||||||
|
positionX={shouldHideSearch && lang.isRtl ? 'right' : 'left'}
|
||||||
|
onTransitionEnd={lang.isRtl ? handleDropdownMenuTransitionEnd : undefined}
|
||||||
>
|
>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="saved-messages"
|
icon="saved-messages"
|
||||||
@ -269,7 +288,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<span className="menu-item-name">{lang('ArchivedChats')}</span>
|
<span className="menu-item-name">{lang('ArchivedChats')}</span>
|
||||||
{archivedUnreadChatsCount > 0 && (
|
{archivedUnreadChatsCount > 0 && (
|
||||||
<div className="archived-badge">{archivedUnreadChatsCount}</div>
|
<div className="right-badge">{archivedUnreadChatsCount}</div>
|
||||||
)}
|
)}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@ -357,8 +376,11 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<SearchInput
|
<SearchInput
|
||||||
inputId="telegram-search-input"
|
inputId="telegram-search-input"
|
||||||
parentContainerClassName="LeftSearch"
|
parentContainerClassName="LeftSearch"
|
||||||
className={globalSearchChatId || searchDate ? 'with-picker-item' : ''}
|
className={buildClassName(
|
||||||
value={contactsFilter || searchQuery}
|
(globalSearchChatId || searchDate) ? 'with-picker-item' : undefined,
|
||||||
|
shouldHideSearch && 'SearchInput--hidden',
|
||||||
|
)}
|
||||||
|
value={isClosingSearch ? undefined : (contactsFilter || searchQuery)}
|
||||||
focused={isSearchFocused}
|
focused={isSearchFocused}
|
||||||
isLoading={isLoading || connectionStatusPosition === 'minimized'}
|
isLoading={isLoading || connectionStatusPosition === 'minimized'}
|
||||||
spinnerColor={connectionStatusPosition === 'minimized' ? 'yellow' : undefined}
|
spinnerColor={connectionStatusPosition === 'minimized' ? 'yellow' : undefined}
|
||||||
|
|||||||
34
src/components/left/main/Topic.module.scss
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
.root {
|
||||||
|
:global(.ListItem-button) {
|
||||||
|
padding: 0.5rem 0.75rem !important;
|
||||||
|
|
||||||
|
:global(.last-message),
|
||||||
|
:global(.status),
|
||||||
|
:global(.typing-status) {
|
||||||
|
line-height: 1.5rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.LastMessageMeta) {
|
||||||
|
padding-top: 0;
|
||||||
|
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
margin-right: 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.fullName .emoji-small) {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.closed-icon {
|
||||||
|
color: var(--color-pinned);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-top: -0.5rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-icon {
|
||||||
|
margin-inline-end: 0.25rem;
|
||||||
|
}
|
||||||
236
src/components/left/main/Topic.tsx
Normal file
@ -0,0 +1,236 @@
|
|||||||
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import type {
|
||||||
|
ApiChat, ApiFormattedText, ApiTopic, ApiMessage, ApiMessageOutgoingStatus,
|
||||||
|
ApiTypingStatus,
|
||||||
|
ApiUser,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||||
|
import type { ChatAnimationTypes } from './hooks';
|
||||||
|
import type { AnimationLevel } from '../../../types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
selectCanDeleteTopic,
|
||||||
|
selectChat,
|
||||||
|
selectChatMessage, selectCurrentMessageList,
|
||||||
|
selectDraft,
|
||||||
|
selectOutgoingStatus, selectThreadInfo, selectThreadParam, selectUser,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import useChatListEntry from './hooks/useChatListEntry';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import { getMessageAction } from '../../../global/helpers';
|
||||||
|
import useTopicContextActions from './hooks/useTopicContextActions';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||||
|
import Badge from './Badge';
|
||||||
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
|
import TopicIcon from '../../common/TopicIcon';
|
||||||
|
|
||||||
|
import styles from './Topic.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
topic: ApiTopic;
|
||||||
|
isSelected: boolean;
|
||||||
|
style: string;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
|
||||||
|
orderDiff: number;
|
||||||
|
animationType: ChatAnimationTypes;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat: ApiChat;
|
||||||
|
canDelete?: boolean;
|
||||||
|
lastMessage?: ApiMessage;
|
||||||
|
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
|
actionTargetMessage?: ApiMessage;
|
||||||
|
actionTargetUserIds?: string[];
|
||||||
|
lastMessageSender?: ApiUser | ApiChat;
|
||||||
|
actionTargetChatId?: string;
|
||||||
|
animationLevel?: AnimationLevel;
|
||||||
|
typingStatus?: ApiTypingStatus;
|
||||||
|
draft?: ApiFormattedText;
|
||||||
|
canScrollDown?: boolean;
|
||||||
|
wasTopicOpened?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Topic: FC<OwnProps & StateProps> = ({
|
||||||
|
topic,
|
||||||
|
isSelected,
|
||||||
|
chatId,
|
||||||
|
chat,
|
||||||
|
style,
|
||||||
|
lastMessage,
|
||||||
|
canScrollDown,
|
||||||
|
lastMessageOutgoingStatus,
|
||||||
|
observeIntersection,
|
||||||
|
canDelete,
|
||||||
|
actionTargetMessage,
|
||||||
|
actionTargetUserIds,
|
||||||
|
actionTargetChatId,
|
||||||
|
lastMessageSender,
|
||||||
|
animationType,
|
||||||
|
animationLevel,
|
||||||
|
orderDiff,
|
||||||
|
typingStatus,
|
||||||
|
draft,
|
||||||
|
wasTopicOpened,
|
||||||
|
}) => {
|
||||||
|
const { openChat, deleteTopic, focusLastMessage } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||||
|
const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isPinned, isClosed,
|
||||||
|
} = topic;
|
||||||
|
const isMuted = topic.isMuted || (topic.isMuted === undefined && chat.isMuted);
|
||||||
|
|
||||||
|
const handleOpenDeleteModal = useCallback(() => {
|
||||||
|
markRenderDeleteModal();
|
||||||
|
openDeleteModal();
|
||||||
|
}, [markRenderDeleteModal, openDeleteModal]);
|
||||||
|
|
||||||
|
const handleDelete = useCallback(() => {
|
||||||
|
deleteTopic({ chatId: chat.id, topicId: topic.id });
|
||||||
|
}, [chat.id, deleteTopic, topic.id]);
|
||||||
|
|
||||||
|
const { renderSubtitle, ref } = useChatListEntry({
|
||||||
|
chat,
|
||||||
|
chatId,
|
||||||
|
lastMessage,
|
||||||
|
draft,
|
||||||
|
actionTargetMessage,
|
||||||
|
actionTargetUserIds,
|
||||||
|
actionTargetChatId,
|
||||||
|
lastMessageSender,
|
||||||
|
lastMessageTopic: topic,
|
||||||
|
observeIntersection,
|
||||||
|
isTopic: true,
|
||||||
|
typingStatus,
|
||||||
|
|
||||||
|
animationType,
|
||||||
|
animationLevel,
|
||||||
|
orderDiff,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOpenTopic = useCallback(() => {
|
||||||
|
openChat({ id: chatId, threadId: topic.id, shouldReplaceHistory: true });
|
||||||
|
|
||||||
|
if (canScrollDown) {
|
||||||
|
focusLastMessage();
|
||||||
|
}
|
||||||
|
}, [openChat, chatId, topic.id, canScrollDown, focusLastMessage]);
|
||||||
|
|
||||||
|
const contextActions = useTopicContextActions(topic, chat, wasTopicOpened, canDelete, handleOpenDeleteModal);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
'Chat',
|
||||||
|
isSelected && 'selected',
|
||||||
|
'chat-item-clickable',
|
||||||
|
)}
|
||||||
|
onClick={handleOpenTopic}
|
||||||
|
style={style}
|
||||||
|
contextActions={contextActions}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<div className="info">
|
||||||
|
<div className="info-row">
|
||||||
|
<div className={buildClassName('title')}>
|
||||||
|
<TopicIcon topic={topic} className={styles.topicIcon} />
|
||||||
|
<h3 dir="auto" className="fullName">{renderText(topic.title)}</h3>
|
||||||
|
</div>
|
||||||
|
{topic.isMuted && <i className="icon-muted" />}
|
||||||
|
<div className="separator" />
|
||||||
|
{isClosed && (
|
||||||
|
<i className={buildClassName(
|
||||||
|
'icon-lock-badge',
|
||||||
|
styles.closedIcon,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{lastMessage && (
|
||||||
|
<LastMessageMeta
|
||||||
|
message={lastMessage}
|
||||||
|
outgoingStatus={lastMessageOutgoingStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="subtitle">
|
||||||
|
{renderSubtitle()}
|
||||||
|
<Badge
|
||||||
|
chat={chat}
|
||||||
|
isPinned={isPinned}
|
||||||
|
isMuted={isMuted}
|
||||||
|
topic={topic}
|
||||||
|
wasTopicOpened={wasTopicOpened}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{shouldRenderDeleteModal && (
|
||||||
|
<ConfirmDialog
|
||||||
|
isOpen={isDeleteModalOpen}
|
||||||
|
onClose={closeDeleteModal}
|
||||||
|
onCloseAnimationEnd={unmarkRenderDeleteModal}
|
||||||
|
confirmIsDestructive
|
||||||
|
confirmHandler={handleDelete}
|
||||||
|
text={lang('lng_forum_topic_delete_sure')}
|
||||||
|
confirmLabel={lang('Delete')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId, topic, isSelected }) => {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
|
||||||
|
const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId)!;
|
||||||
|
const { senderId, replyToMessageId, isOutgoing } = lastMessage || {};
|
||||||
|
const lastMessageSender = senderId
|
||||||
|
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
|
||||||
|
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
|
||||||
|
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||||
|
? selectChatMessage(global, chatId, replyToMessageId)
|
||||||
|
: undefined;
|
||||||
|
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||||
|
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
|
||||||
|
const draft = selectDraft(global, chatId, topic.id);
|
||||||
|
const threadInfo = selectThreadInfo(global, chatId, topic.id);
|
||||||
|
const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId);
|
||||||
|
|
||||||
|
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
lastMessage,
|
||||||
|
actionTargetUserIds,
|
||||||
|
actionTargetChatId,
|
||||||
|
actionTargetMessage,
|
||||||
|
lastMessageSender,
|
||||||
|
typingStatus,
|
||||||
|
canDelete: selectCanDeleteTopic(global, chatId, topic.id),
|
||||||
|
animationLevel: global.settings.byKey.animationLevel,
|
||||||
|
draft,
|
||||||
|
...(isOutgoing && lastMessage && {
|
||||||
|
lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage),
|
||||||
|
}),
|
||||||
|
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
|
||||||
|
wasTopicOpened,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(Topic));
|
||||||
@ -6,13 +6,13 @@ export enum ChatAnimationTypes {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useChatAnimationType(orderDiffById: Record<string, number>) {
|
export function useChatAnimationType<T extends number | string>(orderDiffById: Record<T, number>) {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const orderDiffs = Object.values(orderDiffById);
|
const orderDiffs = Object.values(orderDiffById) as T[];
|
||||||
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
|
const numberOfUp = orderDiffs.filter((diff) => diff < 0).length;
|
||||||
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
|
const numberOfDown = orderDiffs.filter((diff) => diff > 0).length;
|
||||||
|
|
||||||
return (chatId: string): ChatAnimationTypes => {
|
return (chatId: T): ChatAnimationTypes => {
|
||||||
const orderDiff = orderDiffById[chatId];
|
const orderDiff = orderDiffById[chatId];
|
||||||
if (orderDiff === 0) {
|
if (orderDiff === 0) {
|
||||||
return ChatAnimationTypes.None;
|
return ChatAnimationTypes.None;
|
||||||
|
|||||||
224
src/components/left/main/hooks/useChatListEntry.tsx
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
import React, { useLayoutEffect, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||||
|
import { getGlobal } from '../../../../global';
|
||||||
|
|
||||||
|
import type { AnimationLevel } from '../../../../types';
|
||||||
|
import type { LangFn } from '../../../../hooks/useLang';
|
||||||
|
import type {
|
||||||
|
ApiChat, ApiTopic, ApiMessage, ApiTypingStatus, ApiUser,
|
||||||
|
} from '../../../../api/types';
|
||||||
|
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
|
import type { Thread } from '../../../../global/types';
|
||||||
|
|
||||||
|
import { ANIMATION_END_DELAY } from '../../../../config';
|
||||||
|
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||||
|
import {
|
||||||
|
getMessageMediaHash,
|
||||||
|
getMessageMediaThumbDataUri, getMessageRoundVideo,
|
||||||
|
getMessageSenderName, getMessageSticker, getMessageVideo, isActionMessage, isChatChannel,
|
||||||
|
} from '../../../../global/helpers';
|
||||||
|
import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText';
|
||||||
|
import renderText from '../../../common/helpers/renderText';
|
||||||
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import useEnsureMessage from '../../../../hooks/useEnsureMessage';
|
||||||
|
import useMedia from '../../../../hooks/useMedia';
|
||||||
|
import { ChatAnimationTypes } from './useChatAnimationType';
|
||||||
|
import { fastRaf } from '../../../../util/schedulers';
|
||||||
|
|
||||||
|
import MessageSummary from '../../../common/MessageSummary';
|
||||||
|
import ChatForumLastMessage from '../../../common/ChatForumLastMessage';
|
||||||
|
import TypingStatus from '../../../common/TypingStatus';
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
|
export default function useChatListEntry({
|
||||||
|
chat,
|
||||||
|
lastMessage,
|
||||||
|
chatId,
|
||||||
|
typingStatus,
|
||||||
|
draft,
|
||||||
|
actionTargetMessage,
|
||||||
|
actionTargetUserIds,
|
||||||
|
lastMessageTopic,
|
||||||
|
lastMessageSender,
|
||||||
|
actionTargetChatId,
|
||||||
|
observeIntersection,
|
||||||
|
animationType,
|
||||||
|
orderDiff,
|
||||||
|
animationLevel,
|
||||||
|
isTopic,
|
||||||
|
}: {
|
||||||
|
chat?: ApiChat;
|
||||||
|
lastMessage?: ApiMessage;
|
||||||
|
chatId: string;
|
||||||
|
typingStatus?: ApiTypingStatus;
|
||||||
|
draft?: Thread['draft'];
|
||||||
|
actionTargetMessage?: ApiMessage;
|
||||||
|
actionTargetUserIds?: string[];
|
||||||
|
lastMessageTopic?: ApiTopic;
|
||||||
|
lastMessageSender?: ApiUser | ApiChat;
|
||||||
|
actionTargetChatId?: string;
|
||||||
|
observeIntersection?: ObserveFn;
|
||||||
|
isTopic?: boolean;
|
||||||
|
|
||||||
|
animationType: ChatAnimationTypes;
|
||||||
|
orderDiff: number;
|
||||||
|
animationLevel?: AnimationLevel;
|
||||||
|
}) {
|
||||||
|
const lang = useLang();
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isAction = lastMessage && isActionMessage(lastMessage);
|
||||||
|
|
||||||
|
useEnsureMessage(chatId, isAction ? lastMessage.replyToMessageId : undefined, actionTargetMessage);
|
||||||
|
|
||||||
|
const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage)
|
||||||
|
? getMessageMediaThumbDataUri(lastMessage)
|
||||||
|
: undefined;
|
||||||
|
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
||||||
|
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
||||||
|
|
||||||
|
const actionTargetUsers = useMemo(() => {
|
||||||
|
if (!actionTargetUserIds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need for expensive global updates on users, so we avoid them
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
return actionTargetUserIds.map((userId) => usersById[userId]).filter(Boolean);
|
||||||
|
}, [actionTargetUserIds]);
|
||||||
|
|
||||||
|
function renderSubtitle() {
|
||||||
|
if (chat?.isForum && !isTopic) {
|
||||||
|
return (
|
||||||
|
<ChatForumLastMessage
|
||||||
|
chat={chat}
|
||||||
|
renderLastMessage={renderLastMessageOrTyping}
|
||||||
|
observeIntersection={observeIntersection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderLastMessageOrTyping();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLastMessageOrTyping() {
|
||||||
|
if (typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
|
||||||
|
return <TypingStatus typingStatus={typingStatus} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draft?.text.length) {
|
||||||
|
return (
|
||||||
|
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||||
|
<span className="draft">{lang('Draft')}</span>
|
||||||
|
{renderTextWithEntities(draft.text, draft.entities, undefined, undefined, undefined, undefined, true)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastMessage) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAction) {
|
||||||
|
const isChat = chat && (isChatChannel(chat) || lastMessage.senderId === lastMessage.chatId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||||
|
{renderActionMessageText(
|
||||||
|
lang,
|
||||||
|
lastMessage,
|
||||||
|
!isChat ? lastMessageSender as ApiUser : undefined,
|
||||||
|
isChat ? chat : undefined,
|
||||||
|
actionTargetUsers,
|
||||||
|
actionTargetMessage,
|
||||||
|
actionTargetChatId,
|
||||||
|
lastMessageTopic,
|
||||||
|
{ isEmbedded: true },
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderName = getMessageSenderName(lang, chatId, lastMessageSender);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
|
||||||
|
{senderName && (
|
||||||
|
<>
|
||||||
|
<span className="sender-name">{renderText(senderName)}</span>
|
||||||
|
<span className="colon">:</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sets animation excess values when `orderDiff` changes and then resets excess values to animate
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
|
||||||
|
if (animationLevel === 0 || !element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Refactor animation: create `useListAnimation` that owns `orderDiff` and `animationType`
|
||||||
|
if (animationType === ChatAnimationTypes.Opacity) {
|
||||||
|
element.style.opacity = '0';
|
||||||
|
|
||||||
|
fastRaf(() => {
|
||||||
|
element.classList.add('animate-opacity');
|
||||||
|
element.style.opacity = '1';
|
||||||
|
});
|
||||||
|
} else if (animationType === ChatAnimationTypes.Move) {
|
||||||
|
element.style.transform = `translate3d(0, ${-orderDiff * 100}%, 0)`;
|
||||||
|
|
||||||
|
fastRaf(() => {
|
||||||
|
element.classList.add('animate-transform');
|
||||||
|
element.style.transform = '';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fastRaf(() => {
|
||||||
|
element.classList.remove('animate-opacity', 'animate-transform');
|
||||||
|
element.style.opacity = '';
|
||||||
|
element.style.transform = '';
|
||||||
|
});
|
||||||
|
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||||
|
}, [animationLevel, orderDiff, animationType]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
renderSubtitle,
|
||||||
|
ref,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(
|
||||||
|
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
||||||
|
) {
|
||||||
|
const messageSummary = (
|
||||||
|
<MessageSummary
|
||||||
|
lang={lang}
|
||||||
|
message={message}
|
||||||
|
noEmoji={Boolean(blobUrl)}
|
||||||
|
observeIntersectionForLoading={observeIntersection}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!blobUrl) {
|
||||||
|
return messageSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="media-preview">
|
||||||
|
<img src={blobUrl} alt="" className={buildClassName('media-preview--image', isRoundVideo && 'round')} />
|
||||||
|
{getMessageVideo(message) && <i className="icon-play" />}
|
||||||
|
{messageSummary}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
36
src/components/left/main/hooks/useChatOrderDiff.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useMemo } from '../../../../lib/teact/teact';
|
||||||
|
import usePrevious from '../../../../hooks/usePrevious';
|
||||||
|
import { mapValues } from '../../../../util/iteratees';
|
||||||
|
import { useChatAnimationType } from './useChatAnimationType';
|
||||||
|
|
||||||
|
export default function useChatOrderDiff(orderedIds: (string | number)[] | undefined) {
|
||||||
|
const orderById = useMemo(() => {
|
||||||
|
if (!orderedIds) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return orderedIds.reduce((acc, id, i) => {
|
||||||
|
acc[id] = i;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
}, [orderedIds]);
|
||||||
|
|
||||||
|
const prevOrderById = usePrevious(orderById);
|
||||||
|
|
||||||
|
const orderDiffById = useMemo(() => {
|
||||||
|
if (!orderById || !prevOrderById) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapValues(orderById, (order, id) => {
|
||||||
|
return prevOrderById[id] !== undefined ? order - prevOrderById[id] : -Infinity;
|
||||||
|
});
|
||||||
|
}, [orderById, prevOrderById]);
|
||||||
|
|
||||||
|
const getAnimationType = useChatAnimationType(orderDiffById);
|
||||||
|
|
||||||
|
return {
|
||||||
|
orderDiffById,
|
||||||
|
getAnimationType,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
import { useRef } from '../../../../lib/teact/teact';
|
||||||
|
import usePrevious from '../../../../hooks/usePrevious';
|
||||||
|
import useForceUpdate from '../../../../hooks/useForceUpdate';
|
||||||
|
|
||||||
|
export default function useLeftHeaderButtonRtlForumTransition(shouldHideSearch?: boolean) {
|
||||||
|
const forceUpdate = useForceUpdate();
|
||||||
|
const shouldDisableDropdownMenuTransitionRef = useRef(shouldHideSearch);
|
||||||
|
const prevShouldHideSearch = usePrevious(shouldHideSearch);
|
||||||
|
|
||||||
|
function handleDropdownMenuTransitionEnd() {
|
||||||
|
shouldDisableDropdownMenuTransitionRef.current = Boolean(shouldHideSearch);
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldHideSearch === false && prevShouldHideSearch !== shouldHideSearch) {
|
||||||
|
shouldDisableDropdownMenuTransitionRef.current = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { shouldDisableDropdownMenuTransitionRef, handleDropdownMenuTransitionEnd };
|
||||||
|
}
|
||||||
98
src/components/left/main/hooks/useTopicContextActions.ts
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiTopic } from '../../../../api/types';
|
||||||
|
|
||||||
|
import { compact } from '../../../../util/iteratees';
|
||||||
|
import { getHasAdminRight } from '../../../../global/helpers';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
|
import { useMemo } from '../../../../lib/teact/teact';
|
||||||
|
|
||||||
|
export default function useTopicContextActions(
|
||||||
|
topic: ApiTopic,
|
||||||
|
chat: ApiChat,
|
||||||
|
wasOpened?: boolean,
|
||||||
|
canDelete?: boolean,
|
||||||
|
handleDelete?: NoneToVoidFunction,
|
||||||
|
) {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const {
|
||||||
|
isPinned, isMuted, isClosed, isOwner, id: topicId,
|
||||||
|
} = topic;
|
||||||
|
|
||||||
|
const chatId = chat.id;
|
||||||
|
|
||||||
|
const {
|
||||||
|
editTopic,
|
||||||
|
toggleTopicPinned,
|
||||||
|
markTopicRead,
|
||||||
|
updateTopicMutedState,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const canToggleClosed = isOwner || chat.isCreator || getHasAdminRight(chat, 'manageTopics');
|
||||||
|
const canTogglePinned = chat.isCreator || getHasAdminRight(chat, 'manageTopics');
|
||||||
|
|
||||||
|
const actionUnreadMark = topic.unreadCount || !wasOpened
|
||||||
|
? {
|
||||||
|
title: lang('MarkAsRead'),
|
||||||
|
icon: 'readchats',
|
||||||
|
handler: () => {
|
||||||
|
markTopicRead({ chatId, topicId });
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const actionPin = canTogglePinned ? (isPinned
|
||||||
|
? {
|
||||||
|
title: lang('UnpinFromTop'),
|
||||||
|
icon: 'unpin',
|
||||||
|
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: false }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: lang('PinToTop'),
|
||||||
|
icon: 'pin',
|
||||||
|
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }),
|
||||||
|
}) : undefined;
|
||||||
|
|
||||||
|
const actionMute = ((chat.isMuted && isMuted !== false) || isMuted === true)
|
||||||
|
? {
|
||||||
|
title: lang('ChatList.Unmute'),
|
||||||
|
icon: 'unmute',
|
||||||
|
handler: () => updateTopicMutedState({ chatId, topicId, isMuted: false }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: lang('ChatList.Mute'),
|
||||||
|
icon: 'mute',
|
||||||
|
handler: () => updateTopicMutedState({ chatId, topicId, isMuted: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionCloseTopic = canToggleClosed ? (isClosed
|
||||||
|
? {
|
||||||
|
title: lang('lng_forum_topic_reopen'),
|
||||||
|
icon: 'reopen-topic',
|
||||||
|
handler: () => editTopic({ chatId, topicId, isClosed: false }),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
title: lang('lng_forum_topic_close'),
|
||||||
|
icon: 'close-topic',
|
||||||
|
handler: () => editTopic({ chatId, topicId, isClosed: true }),
|
||||||
|
}) : undefined;
|
||||||
|
|
||||||
|
const actionDelete = canDelete ? {
|
||||||
|
title: lang('lng_forum_topic_delete'),
|
||||||
|
icon: 'delete',
|
||||||
|
destructive: true,
|
||||||
|
handler: handleDelete,
|
||||||
|
} : undefined;
|
||||||
|
|
||||||
|
return compact([
|
||||||
|
actionPin,
|
||||||
|
actionUnreadMark,
|
||||||
|
actionMute,
|
||||||
|
actionCloseTopic,
|
||||||
|
actionDelete,
|
||||||
|
]);
|
||||||
|
}, [topic, chat, wasOpened, lang, canDelete, handleDelete]);
|
||||||
|
}
|
||||||
@ -7,13 +7,15 @@ import { LoadMoreDirection } from '../../../types';
|
|||||||
|
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import useLang from '../../../hooks/useLang';
|
|
||||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||||
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
|
||||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
import NothingFound from '../../common/NothingFound';
|
import NothingFound from '../../common/NothingFound';
|
||||||
import ChatMessage from './ChatMessage';
|
import ChatMessage from './ChatMessage';
|
||||||
import DateSuggest from './DateSuggest';
|
import DateSuggest from './DateSuggest';
|
||||||
|
import LeftSearchResultTopic from './LeftSearchResultTopic';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
searchQuery?: string;
|
searchQuery?: string;
|
||||||
@ -28,6 +30,8 @@ type StateProps = {
|
|||||||
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
||||||
chatsById: Record<string, ApiChat>;
|
chatsById: Record<string, ApiChat>;
|
||||||
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
||||||
|
foundTopicIds?: number[];
|
||||||
|
searchChatId?: string;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -42,9 +46,12 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
|||||||
chatsById,
|
chatsById,
|
||||||
fetchingStatus,
|
fetchingStatus,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
foundTopicIds,
|
||||||
|
searchChatId,
|
||||||
onSearchDateSelect,
|
onSearchDateSelect,
|
||||||
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
const { searchMessagesGlobal } = getActions();
|
const { searchMessagesGlobal, openChat } = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
|
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
|
||||||
@ -59,6 +66,17 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [currentUserId, lastSyncTime, searchMessagesGlobal, searchQuery]);
|
}, [currentUserId, lastSyncTime, searchMessagesGlobal, searchQuery]);
|
||||||
|
|
||||||
|
const handleTopicClick = useCallback(
|
||||||
|
(id: number) => {
|
||||||
|
openChat({ id: searchChatId, threadId: id, shouldReplaceHistory: true });
|
||||||
|
|
||||||
|
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||||
|
onReset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openChat, searchChatId, onReset],
|
||||||
|
);
|
||||||
|
|
||||||
const foundMessages = useMemo(() => {
|
const foundMessages = useMemo(() => {
|
||||||
if (!foundIds || foundIds.length === 0) {
|
if (!foundIds || foundIds.length === 0) {
|
||||||
return MEMO_EMPTY_ARRAY;
|
return MEMO_EMPTY_ARRAY;
|
||||||
@ -91,7 +109,8 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nothingFound = fetchingStatus && !fetchingStatus.chats && !fetchingStatus.messages && !foundMessages.length;
|
const nothingFound = fetchingStatus && !fetchingStatus.chats && !fetchingStatus.messages && !foundMessages.length
|
||||||
|
&& !foundTopicIds?.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="LeftSearch">
|
<div className="LeftSearch">
|
||||||
@ -115,7 +134,30 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
|||||||
description={lang('ChatList.Search.NoResultsDescription')}
|
description={lang('ChatList.Search.NoResultsDescription')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{foundMessages.map(renderFoundMessage)}
|
{Boolean(foundTopicIds?.length) && (
|
||||||
|
<div className="pb-2">
|
||||||
|
<h3 className="section-heading topic-search-heading" dir={lang.isRtl ? 'auto' : undefined}>
|
||||||
|
{lang('Topics')}
|
||||||
|
</h3>
|
||||||
|
{foundTopicIds!.map((id) => {
|
||||||
|
return (
|
||||||
|
<LeftSearchResultTopic
|
||||||
|
chatId={searchChatId!}
|
||||||
|
topicId={id}
|
||||||
|
onClick={handleTopicClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{Boolean(foundMessages.length) && (
|
||||||
|
<div className="pb-2">
|
||||||
|
<h3 className="section-heading topic-search-heading" dir={lang.isRtl ? 'auto' : undefined}>
|
||||||
|
{lang('SearchMessages')}
|
||||||
|
</h3>
|
||||||
|
{foundMessages.map(renderFoundMessage)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -125,7 +167,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const { byId: chatsById } = global.chats;
|
const { byId: chatsById } = global.chats;
|
||||||
const { currentUserId, messages: { byChatId: globalMessagesByChatId }, lastSyncTime } = global;
|
const { currentUserId, messages: { byChatId: globalMessagesByChatId }, lastSyncTime } = global;
|
||||||
const { fetchingStatus, resultsByType } = global.globalSearch;
|
const {
|
||||||
|
fetchingStatus, resultsByType, foundTopicIds, chatId: searchChatId,
|
||||||
|
} = global.globalSearch;
|
||||||
|
|
||||||
const { foundIds } = (resultsByType?.text) || {};
|
const { foundIds } = (resultsByType?.text) || {};
|
||||||
|
|
||||||
@ -135,7 +179,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
globalMessagesByChatId,
|
globalMessagesByChatId,
|
||||||
chatsById,
|
chatsById,
|
||||||
fetchingStatus,
|
fetchingStatus,
|
||||||
|
foundTopicIds,
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
|
searchChatId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(ChatMessageResults));
|
)(ChatMessageResults));
|
||||||
|
|||||||
@ -54,12 +54,18 @@
|
|||||||
right: 0.625rem;
|
right: 0.625rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.topic-search-heading {
|
||||||
|
margin-left: -1.0625rem !important;
|
||||||
|
padding-left: 2.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.LeftSearch .search-section .section-heading,
|
.LeftSearch .search-section .section-heading,
|
||||||
.RecentContacts .search-section .section-heading {
|
.RecentContacts .search-section .section-heading {
|
||||||
margin-left: -1rem !important;
|
margin-left: -0.5rem !important;
|
||||||
width: calc(100% + 2rem);
|
padding-left: 1.5rem;
|
||||||
|
width: calc(100% + 0.625rem);
|
||||||
box-shadow: 0 -1px 0 0 var(--color-borders);
|
box-shadow: 0 -1px 0 0 var(--color-borders);
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
@ -135,6 +141,28 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topic-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.topic-icon {
|
||||||
|
--custom-emoji-size: 2rem;
|
||||||
|
margin-inline-end: 0.25rem !important;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
font-size: 2rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fullName {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ListItem.search-result-message {
|
.ListItem.search-result-message {
|
||||||
@ -150,12 +178,12 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -0.625rem;
|
margin: 0 -0.125rem 0 -0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-section {
|
.search-section {
|
||||||
padding: 0 1rem 0.5rem;
|
padding: 0 0.125rem 0.5rem 0.5rem;
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
|
|||||||
71
src/components/left/search/LeftSearchResultTopic.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiTopic } from '../../../api/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
selectChat,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
|
||||||
|
|
||||||
|
import ListItem from '../../ui/ListItem';
|
||||||
|
import TopicIcon from '../../common/TopicIcon';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
topicId: number;
|
||||||
|
onClick: (id: number) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
topic?: ApiTopic;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOPIC_ICON_SIZE = 2 * REM;
|
||||||
|
|
||||||
|
const LeftSearchResultTopic: FC<OwnProps & StateProps> = ({
|
||||||
|
topicId,
|
||||||
|
topic,
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
|
onClick(topicId);
|
||||||
|
}, [topicId, onClick]);
|
||||||
|
|
||||||
|
const buttonRef = useSelectWithEnter(handleClick);
|
||||||
|
|
||||||
|
if (!topic) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
className="chat-item-clickable search-result"
|
||||||
|
onClick={handleClick}
|
||||||
|
buttonClassName="topic-item"
|
||||||
|
buttonRef={buttonRef}
|
||||||
|
>
|
||||||
|
<TopicIcon
|
||||||
|
size={TOPIC_ICON_SIZE}
|
||||||
|
topic={topic}
|
||||||
|
className="topic-icon"
|
||||||
|
letterClassName="topic-icon-letter"
|
||||||
|
/>
|
||||||
|
<div dir="auto" className="fullName">{renderText(topic.title)}</div>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId, topicId }): StateProps => {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const topic = chat?.topics?.[topicId];
|
||||||
|
|
||||||
|
return {
|
||||||
|
topic,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(LeftSearchResultTopic));
|
||||||
@ -38,7 +38,11 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
margin-left: 0.125rem;
|
margin-left: 0.5rem;
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
margin-left: 0.125rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
|
|||||||
@ -33,8 +33,13 @@ const DraftRecipientPicker: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, markIsShown]);
|
}, [isOpen, markIsShown]);
|
||||||
|
|
||||||
const handleSelectRecipient = useCallback((recipientId: string) => {
|
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
|
||||||
openChatWithDraft({ chatId: recipientId, text: requestedDraft!.text, files: requestedDraft!.files });
|
openChatWithDraft({
|
||||||
|
chatId: recipientId,
|
||||||
|
threadId,
|
||||||
|
text: requestedDraft!.text,
|
||||||
|
files: requestedDraft!.files,
|
||||||
|
});
|
||||||
}, [openChatWithDraft, requestedDraft]);
|
}, [openChatWithDraft, requestedDraft]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
isManyMessages,
|
isManyMessages,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setForwardChatId,
|
setForwardChatOrTopic,
|
||||||
exitForwardMode,
|
exitForwardMode,
|
||||||
forwardToSavedMessages,
|
forwardToSavedMessages,
|
||||||
showNotification,
|
showNotification,
|
||||||
@ -39,7 +39,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isOpen, markIsShown]);
|
}, [isOpen, markIsShown]);
|
||||||
|
|
||||||
const handleSelectRecipient = useCallback((recipientId: string) => {
|
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
|
||||||
if (recipientId === currentUserId) {
|
if (recipientId === currentUserId) {
|
||||||
forwardToSavedMessages();
|
forwardToSavedMessages();
|
||||||
showNotification({
|
showNotification({
|
||||||
@ -48,9 +48,9 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
|
|||||||
: 'Conversation.ForwardTooltip.SavedMessages.One'),
|
: 'Conversation.ForwardTooltip.SavedMessages.One'),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setForwardChatId({ id: recipientId });
|
setForwardChatOrTopic({ chatId: recipientId, topicId: threadId });
|
||||||
}
|
}
|
||||||
}, [currentUserId, forwardToSavedMessages, isManyMessages, lang, setForwardChatId, showNotification]);
|
}, [currentUserId, forwardToSavedMessages, isManyMessages, lang, setForwardChatOrTopic, showNotification]);
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
exitForwardMode();
|
exitForwardMode();
|
||||||
|
|||||||
@ -28,9 +28,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#LeftColumn {
|
#LeftColumn {
|
||||||
min-width: 12rem;
|
|
||||||
width: 33vw;
|
width: 33vw;
|
||||||
max-width: 26.5rem;
|
|
||||||
|
--left-column-min-width: 16rem;
|
||||||
|
--left-column-max-width: 26.5rem;
|
||||||
|
|
||||||
|
min-width: var(--left-column-min-width);
|
||||||
|
max-width: var(--left-column-max-width);
|
||||||
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
@ -45,12 +50,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 926px) {
|
@media (min-width: 926px) {
|
||||||
max-width: 40vw;
|
--left-column-max-width: 40vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1276px) {
|
@media (min-width: 1276px) {
|
||||||
width: 25vw;
|
width: 25vw;
|
||||||
max-width: 33vw;
|
--left-column-max-width: 33vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 925px) {
|
@media (max-width: 925px) {
|
||||||
@ -115,6 +120,7 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
|
--left-column-max-width: calc(100vw - env(safe-area-inset-left));
|
||||||
transform: translate3d(-20vw, 0, 0);
|
transform: translate3d(-20vw, 0, 0);
|
||||||
left: env(safe-area-inset-left) !important;
|
left: env(safe-area-inset-left) !important;
|
||||||
width: calc(100vw - env(safe-area-inset-left)) !important;
|
width: calc(100vw - env(safe-area-inset-left)) !important;
|
||||||
|
|||||||
@ -108,7 +108,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const {
|
const {
|
||||||
url, buttonText, queryId,
|
url, buttonText, queryId, replyToMessageId, threadId,
|
||||||
} = webApp || {};
|
} = webApp || {};
|
||||||
const isOpen = Boolean(url);
|
const isOpen = Boolean(url);
|
||||||
const isSimple = !queryId;
|
const isSimple = !queryId;
|
||||||
@ -204,6 +204,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
botId: bot!.id,
|
botId: bot!.id,
|
||||||
queryId: queryId!,
|
queryId: queryId!,
|
||||||
peerId: chat!.id,
|
peerId: chat!.id,
|
||||||
|
replyToMessageId,
|
||||||
|
threadId,
|
||||||
});
|
});
|
||||||
}, queryId ? PROLONG_INTERVAL : undefined, true);
|
}, queryId ? PROLONG_INTERVAL : undefined, true);
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
selectListedIds,
|
selectListedIds,
|
||||||
selectOutlyingIds,
|
selectOutlyingIds,
|
||||||
selectScheduledMessage,
|
selectScheduledMessage,
|
||||||
selectScheduledMessages,
|
selectChatScheduledMessages,
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||||
@ -463,7 +463,7 @@ export default memo(withGlobal(
|
|||||||
let chatMessages: Record<number, ApiMessage> | undefined;
|
let chatMessages: Record<number, ApiMessage> | undefined;
|
||||||
|
|
||||||
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
if (origin && [MediaViewerOrigin.ScheduledAlbum, MediaViewerOrigin.ScheduledInline].includes(origin)) {
|
||||||
chatMessages = selectScheduledMessages(global, chatId);
|
chatMessages = selectChatScheduledMessages(global, chatId);
|
||||||
} else {
|
} else {
|
||||||
chatMessages = selectChatMessages(global, chatId);
|
chatMessages = selectChatMessages(global, chatId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import React, {
|
|||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiUser, ApiMessage, ApiChat, ApiSticker,
|
ApiUser, ApiMessage, ApiChat, ApiSticker, ApiTopic,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { FocusDirection } from '../../types';
|
import type { FocusDirection } from '../../types';
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import {
|
|||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectIsMessageFocused,
|
selectIsMessageFocused,
|
||||||
selectChat,
|
selectChat,
|
||||||
|
selectTopicFromMessage,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
@ -39,6 +40,7 @@ type OwnProps = {
|
|||||||
isEmbedded?: boolean;
|
isEmbedded?: boolean;
|
||||||
appearanceOrder?: number;
|
appearanceOrder?: number;
|
||||||
isLastInList?: boolean;
|
isLastInList?: boolean;
|
||||||
|
isInsideTopic?: boolean;
|
||||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,6 +52,7 @@ type StateProps = {
|
|||||||
targetMessage?: ApiMessage;
|
targetMessage?: ApiMessage;
|
||||||
targetChatId?: string;
|
targetChatId?: string;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
|
topic?: ApiTopic;
|
||||||
focusDirection?: FocusDirection;
|
focusDirection?: FocusDirection;
|
||||||
noFocusHighlight?: boolean;
|
noFocusHighlight?: boolean;
|
||||||
premiumGiftSticker?: ApiSticker;
|
premiumGiftSticker?: ApiSticker;
|
||||||
@ -59,9 +62,6 @@ const APPEARANCE_DELAY = 10;
|
|||||||
|
|
||||||
const ActionMessage: FC<OwnProps & StateProps> = ({
|
const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||||
message,
|
message,
|
||||||
observeIntersectionForReading,
|
|
||||||
observeIntersectionForLoading,
|
|
||||||
observeIntersectionForPlaying,
|
|
||||||
isEmbedded,
|
isEmbedded,
|
||||||
appearanceOrder = 0,
|
appearanceOrder = 0,
|
||||||
isLastInList,
|
isLastInList,
|
||||||
@ -75,7 +75,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
focusDirection,
|
focusDirection,
|
||||||
noFocusHighlight,
|
noFocusHighlight,
|
||||||
premiumGiftSticker,
|
premiumGiftSticker,
|
||||||
|
isInsideTopic,
|
||||||
|
topic,
|
||||||
memoFirstUnreadIdRef,
|
memoFirstUnreadIdRef,
|
||||||
|
observeIntersectionForReading,
|
||||||
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
}) => {
|
}) => {
|
||||||
const { openPremiumModal, requestConfetti } = getActions();
|
const { openPremiumModal, requestConfetti } = getActions();
|
||||||
|
|
||||||
@ -130,6 +135,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
targetUsers,
|
targetUsers,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
|
topic,
|
||||||
{ isEmbedded },
|
{ isEmbedded },
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
@ -155,6 +161,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Refactoring for action rendering
|
||||||
|
const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction';
|
||||||
|
if (shouldSkipRender) {
|
||||||
|
return <span ref={ref} />;
|
||||||
|
}
|
||||||
|
|
||||||
if (isEmbedded) {
|
if (isEmbedded) {
|
||||||
return <span ref={ref} className="embedded-action-message">{content}</span>;
|
return <span ref={ref} className="embedded-action-message">{content}</span>;
|
||||||
}
|
}
|
||||||
@ -229,6 +241,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const senderUser = !isChat && userId ? selectUser(global, userId) : undefined;
|
const senderUser = !isChat && userId ? selectUser(global, userId) : undefined;
|
||||||
const senderChat = isChat ? chat : undefined;
|
const senderChat = isChat ? chat : undefined;
|
||||||
const premiumGiftSticker = global.premiumGifts?.stickers?.[0];
|
const premiumGiftSticker = global.premiumGifts?.stickers?.[0];
|
||||||
|
const topic = selectTopicFromMessage(global, message);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
usersById,
|
usersById,
|
||||||
@ -239,6 +252,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
targetMessage,
|
targetMessage,
|
||||||
isFocused,
|
isFocused,
|
||||||
premiumGiftSticker,
|
premiumGiftSticker,
|
||||||
|
topic,
|
||||||
...(isFocused && { focusDirection, noFocusHighlight }),
|
...(isFocused && { focusDirection, noFocusHighlight }),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -39,6 +39,8 @@ interface OwnProps {
|
|||||||
threadId: number;
|
threadId: number;
|
||||||
messageListType: MessageListType;
|
messageListType: MessageListType;
|
||||||
canExpandActions: boolean;
|
canExpandActions: boolean;
|
||||||
|
withForumActions?: boolean;
|
||||||
|
onTopicSearch?: NoneToVoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface StateProps {
|
interface StateProps {
|
||||||
@ -81,10 +83,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
canCreateVoiceChat,
|
canCreateVoiceChat,
|
||||||
pendingJoinRequests,
|
pendingJoinRequests,
|
||||||
isRightColumnShown,
|
isRightColumnShown,
|
||||||
|
withForumActions,
|
||||||
canExpandActions,
|
canExpandActions,
|
||||||
shouldJoinToSend,
|
shouldJoinToSend,
|
||||||
shouldSendJoinRequest,
|
shouldSendJoinRequest,
|
||||||
noAnimation,
|
noAnimation,
|
||||||
|
onTopicSearch,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
joinChannel,
|
joinChannel,
|
||||||
@ -94,6 +98,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
requestCall,
|
requestCall,
|
||||||
requestNextManagementScreen,
|
requestNextManagementScreen,
|
||||||
showNotification,
|
showNotification,
|
||||||
|
openChat,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
const menuButtonRef = useRef<HTMLButtonElement>(null);
|
||||||
@ -137,6 +142,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
}, [requestNextManagementScreen]);
|
}, [requestNextManagementScreen]);
|
||||||
|
|
||||||
const handleSearchClick = useCallback(() => {
|
const handleSearchClick = useCallback(() => {
|
||||||
|
if (withForumActions) {
|
||||||
|
onTopicSearch?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openLocalTextSearch();
|
openLocalTextSearch();
|
||||||
|
|
||||||
if (IS_SINGLE_COLUMN_LAYOUT) {
|
if (IS_SINGLE_COLUMN_LAYOUT) {
|
||||||
@ -151,7 +161,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
|
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
|
||||||
}
|
}
|
||||||
}, [noAnimation, openLocalTextSearch]);
|
}, [noAnimation, onTopicSearch, openLocalTextSearch, withForumActions]);
|
||||||
|
|
||||||
|
const handleAsMessagesClick = useCallback(() => {
|
||||||
|
openChat({ id: chatId, threadId: MAIN_THREAD_ID });
|
||||||
|
}, [chatId, openChat]);
|
||||||
|
|
||||||
function handleRequestCall() {
|
function handleRequestCall() {
|
||||||
requestCall({ userId: chatId });
|
requestCall({ userId: chatId });
|
||||||
@ -240,7 +254,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{Boolean(pendingJoinRequests) && (
|
{!withForumActions && Boolean(pendingJoinRequests) && (
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
className="badge-button"
|
className="badge-button"
|
||||||
@ -285,8 +299,12 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
canLeave={canLeave}
|
canLeave={canLeave}
|
||||||
canEnterVoiceChat={canEnterVoiceChat}
|
canEnterVoiceChat={canEnterVoiceChat}
|
||||||
canCreateVoiceChat={canCreateVoiceChat}
|
canCreateVoiceChat={canCreateVoiceChat}
|
||||||
|
pendingJoinRequests={pendingJoinRequests}
|
||||||
|
onJoinRequestsClick={handleJoinRequestsClick}
|
||||||
|
withForumActions={withForumActions}
|
||||||
onSubscribeChannel={handleSubscribeClick}
|
onSubscribeChannel={handleSubscribeClick}
|
||||||
onSearchClick={handleSearchClick}
|
onSearchClick={handleSearchClick}
|
||||||
|
onAsMessagesClick={handleAsMessagesClick}
|
||||||
onClose={handleHeaderMenuClose}
|
onClose={handleHeaderMenuClose}
|
||||||
onCloseAnimationEnd={handleHeaderMenuHide}
|
onCloseAnimationEnd={handleHeaderMenuHide}
|
||||||
/>
|
/>
|
||||||
@ -315,17 +333,17 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
||||||
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
const canStartBot = !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
||||||
const canSubscribe = Boolean(
|
const canSubscribe = Boolean(
|
||||||
isMainThread && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
|
(isMainThread || chat.isForum) && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
|
||||||
);
|
);
|
||||||
const canSearch = isMainThread || isDiscussionThread;
|
const canSearch = isMainThread || isDiscussionThread;
|
||||||
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot;
|
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot;
|
||||||
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
|
const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
|
||||||
const canLeave = isMainThread && !canSubscribe;
|
const canLeave = isMainThread && !canSubscribe;
|
||||||
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat.isCallActive;
|
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive;
|
||||||
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && !chat.isCallActive
|
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && !chat.isCallActive
|
||||||
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
||||||
const canViewStatistics = chat.fullInfo?.canViewStatistics;
|
const canViewStatistics = isMainThread && chat.fullInfo?.canViewStatistics;
|
||||||
const pendingJoinRequests = chat.fullInfo?.requestsPending;
|
const pendingJoinRequests = isMainThread ? chat.fullInfo?.requestsPending : undefined;
|
||||||
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
|
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
|
||||||
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
||||||
const noAnimation = global.settings.byKey.animationLevel === ANIMATION_LEVEL_MIN;
|
const noAnimation = global.settings.byKey.animationLevel === ANIMATION_LEVEL_MIN;
|
||||||
|
|||||||
@ -6,17 +6,25 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
|
|
||||||
import type { ApiBotCommand, ApiChat } from '../../api/types';
|
import type { ApiBotCommand, ApiChat } from '../../api/types';
|
||||||
import type { IAnchorPosition } from '../../types';
|
import type { IAnchorPosition } from '../../types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
import { REPLIES_USER_ID } from '../../config';
|
import { REPLIES_USER_ID } from '../../config';
|
||||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
|
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
|
||||||
import {
|
import {
|
||||||
selectChat, selectNotifySettings, selectNotifyExceptions, selectUser, selectChatBot, selectIsPremiumPurchaseBlocked,
|
selectChat,
|
||||||
|
selectNotifySettings,
|
||||||
|
selectNotifyExceptions,
|
||||||
|
selectUser,
|
||||||
|
selectChatBot,
|
||||||
|
selectIsPremiumPurchaseBlocked,
|
||||||
|
selectCurrentMessageList,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import {
|
import {
|
||||||
isUserId, getCanDeleteChat, selectIsChatMuted, getCanAddContact, isChatChannel, isChatGroup,
|
isUserId, getCanDeleteChat, selectIsChatMuted, getCanAddContact, isChatChannel, isChatGroup,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
|
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import Portal from '../ui/Portal';
|
import Portal from '../ui/Portal';
|
||||||
@ -56,13 +64,17 @@ export type OwnProps = {
|
|||||||
canCall?: boolean;
|
canCall?: boolean;
|
||||||
canMute?: boolean;
|
canMute?: boolean;
|
||||||
canViewStatistics?: boolean;
|
canViewStatistics?: boolean;
|
||||||
|
withForumActions?: boolean;
|
||||||
canLeave?: boolean;
|
canLeave?: boolean;
|
||||||
canEnterVoiceChat?: boolean;
|
canEnterVoiceChat?: boolean;
|
||||||
canCreateVoiceChat?: boolean;
|
canCreateVoiceChat?: boolean;
|
||||||
|
pendingJoinRequests?: number;
|
||||||
onSubscribeChannel: () => void;
|
onSubscribeChannel: () => void;
|
||||||
onSearchClick: () => void;
|
onSearchClick: () => void;
|
||||||
|
onAsMessagesClick: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onCloseAnimationEnd: () => void;
|
onCloseAnimationEnd: () => void;
|
||||||
|
onJoinRequestsClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -70,20 +82,28 @@ type StateProps = {
|
|||||||
botCommands?: ApiBotCommand[];
|
botCommands?: ApiBotCommand[];
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
|
isTopic?: boolean;
|
||||||
canAddContact?: boolean;
|
canAddContact?: boolean;
|
||||||
canReportChat?: boolean;
|
canReportChat?: boolean;
|
||||||
canDeleteChat?: boolean;
|
canDeleteChat?: boolean;
|
||||||
canGiftPremium?: boolean;
|
canGiftPremium?: boolean;
|
||||||
hasLinkedChat?: boolean;
|
hasLinkedChat?: boolean;
|
||||||
|
isChatInfoShown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CLOSE_MENU_ANIMATION_DURATION = 200;
|
||||||
|
|
||||||
const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
isOpen,
|
isOpen,
|
||||||
withExtraActions,
|
withExtraActions,
|
||||||
anchor,
|
anchor,
|
||||||
isChannel,
|
isChannel,
|
||||||
botCommands,
|
botCommands,
|
||||||
|
withForumActions,
|
||||||
|
isTopic,
|
||||||
|
isChatInfoShown,
|
||||||
canStartBot,
|
canStartBot,
|
||||||
canRestartBot,
|
canRestartBot,
|
||||||
canSubscribe,
|
canSubscribe,
|
||||||
@ -91,6 +111,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canCall,
|
canCall,
|
||||||
canMute,
|
canMute,
|
||||||
canViewStatistics,
|
canViewStatistics,
|
||||||
|
pendingJoinRequests,
|
||||||
canLeave,
|
canLeave,
|
||||||
canEnterVoiceChat,
|
canEnterVoiceChat,
|
||||||
canCreateVoiceChat,
|
canCreateVoiceChat,
|
||||||
@ -102,8 +123,10 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
canGiftPremium,
|
canGiftPremium,
|
||||||
hasLinkedChat,
|
hasLinkedChat,
|
||||||
canAddContact,
|
canAddContact,
|
||||||
|
onJoinRequestsClick,
|
||||||
onSubscribeChannel,
|
onSubscribeChannel,
|
||||||
onSearchClick,
|
onSearchClick,
|
||||||
|
onAsMessagesClick,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
}) => {
|
}) => {
|
||||||
@ -119,13 +142,18 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
requestCall,
|
requestCall,
|
||||||
toggleStatistics,
|
toggleStatistics,
|
||||||
openGiftPremiumModal,
|
openGiftPremiumModal,
|
||||||
|
openChatWithInfo,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
const [isReportModalOpen, setIsReportModalOpen] = useState(false);
|
||||||
const { x, y } = anchor;
|
const { x, y } = anchor;
|
||||||
|
|
||||||
useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
useShowTransition(isOpen, onCloseAnimationEnd, undefined, false);
|
||||||
|
const isViewGroupInfoShown = usePrevDuringAnimation(
|
||||||
|
(!isChatInfoShown && (withForumActions || isTopic)) ? true : undefined, CLOSE_MENU_ANIMATION_DURATION,
|
||||||
|
);
|
||||||
|
|
||||||
const handleReport = useCallback(() => {
|
const handleReport = useCallback(() => {
|
||||||
setIsMenuOpen(false);
|
setIsMenuOpen(false);
|
||||||
@ -147,6 +175,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
onClose();
|
onClose();
|
||||||
}, [onClose]);
|
}, [onClose]);
|
||||||
|
|
||||||
|
const handleViewGroupInfo = useCallback(() => {
|
||||||
|
openChatWithInfo({ id: chatId, threadId });
|
||||||
|
closeMenu();
|
||||||
|
}, [chatId, closeMenu, openChatWithInfo, threadId]);
|
||||||
|
|
||||||
const closeDeleteModal = useCallback(() => {
|
const closeDeleteModal = useCallback(() => {
|
||||||
setIsDeleteModalOpen(false);
|
setIsDeleteModalOpen(false);
|
||||||
onClose();
|
onClose();
|
||||||
@ -224,6 +257,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
closeMenu();
|
closeMenu();
|
||||||
}, [closeMenu, enterMessageSelectMode]);
|
}, [closeMenu, enterMessageSelectMode]);
|
||||||
|
|
||||||
|
const handleOpenAsMessages = useCallback(() => {
|
||||||
|
onAsMessagesClick();
|
||||||
|
closeMenu();
|
||||||
|
}, [closeMenu, onAsMessagesClick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
disableScrolling();
|
disableScrolling();
|
||||||
|
|
||||||
@ -263,6 +301,31 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
style={`left: ${x}px;top: ${y}px;`}
|
style={`left: ${x}px;top: ${y}px;`}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
>
|
>
|
||||||
|
{isViewGroupInfoShown && (
|
||||||
|
<MenuItem
|
||||||
|
icon="info"
|
||||||
|
onClick={handleViewGroupInfo}
|
||||||
|
>
|
||||||
|
{isTopic ? lang('lng_context_view_topic') : lang('lng_context_view_group')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{withForumActions && Boolean(pendingJoinRequests) && (
|
||||||
|
<MenuItem
|
||||||
|
icon="user"
|
||||||
|
onClick={onJoinRequestsClick}
|
||||||
|
>
|
||||||
|
{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}
|
||||||
|
<div className="right-badge">{pendingJoinRequests}</div>
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
{withForumActions && !isTopic && (
|
||||||
|
<MenuItem
|
||||||
|
icon="message"
|
||||||
|
onClick={handleOpenAsMessages}
|
||||||
|
>
|
||||||
|
{lang('lng_forum_view_as_messages')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{withExtraActions && canStartBot && (
|
{withExtraActions && canStartBot && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="bots"
|
icon="bots"
|
||||||
@ -343,12 +406,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
{lang(isChannel ? 'ViewDiscussion' : 'lng_profile_view_channel')}
|
{lang(isChannel ? 'ViewDiscussion' : 'lng_profile_view_channel')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem
|
{!withForumActions && (
|
||||||
icon="select"
|
<MenuItem
|
||||||
onClick={handleSelectMessages}
|
icon="select"
|
||||||
>
|
onClick={handleSelectMessages}
|
||||||
{lang('ReportSelectMessages')}
|
>
|
||||||
</MenuItem>
|
{lang('ReportSelectMessages')}
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
{canViewStatistics && (
|
{canViewStatistics && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon="stats"
|
icon="stats"
|
||||||
@ -407,7 +472,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { chatId }): StateProps => {
|
(global, { chatId, threadId }): StateProps => {
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
if (!chat || chat.isRestricted) {
|
if (!chat || chat.isRestricted) {
|
||||||
return {};
|
return {};
|
||||||
@ -415,7 +480,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isPrivate = isUserId(chat.id);
|
const isPrivate = isUserId(chat.id);
|
||||||
const user = isPrivate ? selectUser(global, chatId) : undefined;
|
const user = isPrivate ? selectUser(global, chatId) : undefined;
|
||||||
const canAddContact = user && getCanAddContact(user);
|
const canAddContact = user && getCanAddContact(user);
|
||||||
const canReportChat = isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf);
|
const isMainThread = threadId === MAIN_THREAD_ID;
|
||||||
|
const canReportChat = isMainThread && (isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf));
|
||||||
|
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
||||||
|
|
||||||
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
|
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
|
||||||
const canGiftPremium = Boolean(
|
const canGiftPremium = Boolean(
|
||||||
@ -428,12 +495,14 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
chat,
|
chat,
|
||||||
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
|
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
|
||||||
isPrivate,
|
isPrivate,
|
||||||
|
isTopic: chat?.isForum && !isMainThread,
|
||||||
canAddContact,
|
canAddContact,
|
||||||
canReportChat,
|
canReportChat,
|
||||||
canDeleteChat: getCanDeleteChat(chat),
|
canDeleteChat: getCanDeleteChat(chat),
|
||||||
canGiftPremium,
|
canGiftPremium,
|
||||||
hasLinkedChat: Boolean(chat?.fullInfo?.linkedChatId),
|
hasLinkedChat: Boolean(chat?.fullInfo?.linkedChatId),
|
||||||
botCommands: chatBot?.fullInfo?.botInfo?.commands,
|
botCommands: chatBot?.fullInfo?.botInfo?.commands,
|
||||||
|
isChatInfoShown: global.isChatInfoShown && currentChatId === chatId && currentThreadId === threadId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(HeaderMenuContainer));
|
)(HeaderMenuContainer));
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getPictogramDimensions } from '../common/helpers/mediaDimensions';
|
|||||||
import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers';
|
import { getMessageMediaHash, getMessageSingleInlineButton } from '../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
|
import renderText from '../common/helpers/renderText';
|
||||||
|
|
||||||
import useMedia from '../../hooks/useMedia';
|
import useMedia from '../../hooks/useMedia';
|
||||||
import useThumbnail from '../../hooks/useThumbnail';
|
import useThumbnail from '../../hooks/useThumbnail';
|
||||||
@ -104,7 +105,7 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
|||||||
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl)}
|
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl)}
|
||||||
<div className="message-text">
|
<div className="message-text">
|
||||||
<div className="title" dir="auto">
|
<div className="title" dir="auto">
|
||||||
{customTitle || `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
|
{customTitle ? renderText(customTitle) : `${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`}
|
||||||
</div>
|
</div>
|
||||||
<p dir="auto">
|
<p dir="auto">
|
||||||
<MessageSummary lang={lang} message={message} noEmoji={Boolean(mediaThumbnail)} />
|
<MessageSummary lang={lang} message={message} noEmoji={Boolean(mediaThumbnail)} />
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import {
|
|||||||
selectScrollOffset,
|
selectScrollOffset,
|
||||||
selectThreadTopMessageId,
|
selectThreadTopMessageId,
|
||||||
selectFirstMessageId,
|
selectFirstMessageId,
|
||||||
selectScheduledMessages,
|
selectChatScheduledMessages,
|
||||||
selectCurrentMessageIds,
|
selectCurrentMessageIds,
|
||||||
selectIsCurrentUserPremium,
|
selectIsCurrentUserPremium,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
@ -226,7 +226,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewportIds = threadTopMessageId && (!messageIds[0] || threadFirstMessageId === messageIds[0])
|
const viewportIds = threadTopMessageId && threadFirstMessageId !== threadTopMessageId
|
||||||
|
&& (!messageIds[0] || threadFirstMessageId === messageIds[0])
|
||||||
? [threadTopMessageId, ...messageIds]
|
? [threadTopMessageId, ...messageIds]
|
||||||
: messageIds;
|
: messageIds;
|
||||||
|
|
||||||
@ -625,12 +626,12 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const messageIds = selectCurrentMessageIds(global, chatId, threadId, type);
|
const messageIds = selectCurrentMessageIds(global, chatId, threadId, type);
|
||||||
const messagesById = type === 'scheduled'
|
const messagesById = type === 'scheduled'
|
||||||
? selectScheduledMessages(global, chatId)
|
? selectChatScheduledMessages(global, chatId)
|
||||||
: selectChatMessages(global, chatId);
|
: selectChatMessages(global, chatId);
|
||||||
const threadTopMessageId = selectThreadTopMessageId(global, chatId, threadId);
|
const threadTopMessageId = selectThreadTopMessageId(global, chatId, threadId);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
threadId !== MAIN_THREAD_ID
|
threadId !== MAIN_THREAD_ID && !chat?.isForum
|
||||||
&& !(messagesById && threadTopMessageId && messagesById[threadTopMessageId])
|
&& !(messagesById && threadTopMessageId && messagesById[threadTopMessageId])
|
||||||
) {
|
) {
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@ -22,10 +22,12 @@ 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 { getActions } from '../../global';
|
||||||
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
threadId: number;
|
||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
messageGroups: MessageDateGroup[];
|
messageGroups: MessageDateGroup[];
|
||||||
isViewportNewest: boolean;
|
isViewportNewest: boolean;
|
||||||
@ -36,7 +38,6 @@ interface OwnProps {
|
|||||||
anchorIdRef: { current: string | undefined };
|
anchorIdRef: { current: string | undefined };
|
||||||
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
memoUnreadDividerBeforeIdRef: { current: number | undefined };
|
||||||
memoFirstUnreadIdRef: { current: number | undefined };
|
memoFirstUnreadIdRef: { current: number | undefined };
|
||||||
threadId: number;
|
|
||||||
type: MessageListType;
|
type: MessageListType;
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
isScrollingRef: { current: boolean | undefined };
|
isScrollingRef: { current: boolean | undefined };
|
||||||
@ -54,6 +55,7 @@ const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
|||||||
const MessageListContent: FC<OwnProps> = ({
|
const MessageListContent: FC<OwnProps> = ({
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
messageIds,
|
messageIds,
|
||||||
messageGroups,
|
messageGroups,
|
||||||
isViewportNewest,
|
isViewportNewest,
|
||||||
@ -64,7 +66,6 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
anchorIdRef,
|
anchorIdRef,
|
||||||
memoUnreadDividerBeforeIdRef,
|
memoUnreadDividerBeforeIdRef,
|
||||||
memoFirstUnreadIdRef,
|
memoFirstUnreadIdRef,
|
||||||
threadId,
|
|
||||||
type,
|
type,
|
||||||
isReady,
|
isReady,
|
||||||
isScrollingRef,
|
isScrollingRef,
|
||||||
@ -141,6 +142,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
<ActionMessage
|
<ActionMessage
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID)}
|
||||||
observeIntersectionForReading={observeIntersectionForReading}
|
observeIntersectionForReading={observeIntersectionForReading}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
|||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../lib/teact/teactn';
|
import { getActions, withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { createMessageHash } from '../../util/routing';
|
import { createLocationHash } from '../../util/routing';
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
import type { MessageList as GlobalMessageList } from '../../global/types';
|
import type { MessageList as GlobalMessageList } from '../../global/types';
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ const MessageListHistoryHandler: FC<StateProps> = ({ messageLists }) => {
|
|||||||
const MessageHistoryRecord: FC<GlobalMessageList> = ({ chatId, type, threadId }) => {
|
const MessageHistoryRecord: FC<GlobalMessageList> = ({ chatId, type, threadId }) => {
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive: true,
|
isActive: true,
|
||||||
hash: createMessageHash(chatId, type, threadId),
|
hash: createLocationHash(chatId, type, threadId),
|
||||||
onBack: closeChat,
|
onBack: closeChat,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../global';
|
import { getActions, withGlobal } from '../../global';
|
||||||
|
|
||||||
import type { ApiChatBannedRights } from '../../api/types';
|
import type { ApiChat, ApiChatBannedRights } from '../../api/types';
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
import type {
|
import type {
|
||||||
MessageListType,
|
MessageListType,
|
||||||
@ -42,10 +42,17 @@ import {
|
|||||||
selectIsRightColumnShown,
|
selectIsRightColumnShown,
|
||||||
selectIsUserBlocked,
|
selectIsUserBlocked,
|
||||||
selectPinnedIds,
|
selectPinnedIds,
|
||||||
|
selectReplyingToId,
|
||||||
selectTheme,
|
selectTheme,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import {
|
import {
|
||||||
getCanPostInChat, getMessageSendingRestrictionReason, isChatChannel, isChatGroup, isChatSuperGroup, isUserId,
|
getCanPostInChat,
|
||||||
|
getMessageSendingRestrictionReason,
|
||||||
|
getForumComposerPlaceholder,
|
||||||
|
isChatChannel,
|
||||||
|
isChatGroup,
|
||||||
|
isChatSuperGroup,
|
||||||
|
isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
@ -80,6 +87,8 @@ type StateProps = {
|
|||||||
chatId?: string;
|
chatId?: string;
|
||||||
threadId?: number;
|
threadId?: number;
|
||||||
messageListType?: MessageListType;
|
messageListType?: MessageListType;
|
||||||
|
chat?: ApiChat;
|
||||||
|
replyingToId?: number;
|
||||||
isPrivate?: boolean;
|
isPrivate?: boolean;
|
||||||
isPinnedMessageList?: boolean;
|
isPinnedMessageList?: boolean;
|
||||||
isScheduledMessageList?: boolean;
|
isScheduledMessageList?: boolean;
|
||||||
@ -125,6 +134,8 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
messageListType,
|
messageListType,
|
||||||
|
chat,
|
||||||
|
replyingToId,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
isPinnedMessageList,
|
isPinnedMessageList,
|
||||||
canPost,
|
canPost,
|
||||||
@ -302,10 +313,10 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleUnpinAllMessages = useCallback(() => {
|
const handleUnpinAllMessages = useCallback(() => {
|
||||||
unpinAllMessages({ chatId });
|
unpinAllMessages({ chatId, threadId });
|
||||||
closeUnpinModal();
|
closeUnpinModal();
|
||||||
openPreviousChat();
|
openPreviousChat();
|
||||||
}, [unpinAllMessages, chatId, closeUnpinModal, openPreviousChat]);
|
}, [unpinAllMessages, chatId, threadId, closeUnpinModal, openPreviousChat]);
|
||||||
|
|
||||||
const handleTabletFocus = useCallback(() => {
|
const handleTabletFocus = useCallback(() => {
|
||||||
openChat({ id: chatId });
|
openChat({ id: chatId });
|
||||||
@ -352,6 +363,9 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
|
const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
|
||||||
lang, currentUserBannedRights, defaultBannedRights,
|
lang, currentUserBannedRights, defaultBannedRights,
|
||||||
);
|
);
|
||||||
|
const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(replyingToId));
|
||||||
|
|
||||||
|
const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder;
|
||||||
|
|
||||||
// CSS Variables calculation doesn't work properly with transforms, so we calculate transform values in JS
|
// CSS Variables calculation doesn't work properly with transforms, so we calculate transform values in JS
|
||||||
const {
|
const {
|
||||||
@ -381,10 +395,11 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
|
|
||||||
const isMessagingDisabled = Boolean(
|
const isMessagingDisabled = Boolean(
|
||||||
!isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
!isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
||||||
&& !renderingCanSubscribe && messageSendingRestrictionReason,
|
&& !renderingCanSubscribe && composerRestrictionMessage,
|
||||||
);
|
);
|
||||||
const withMessageListBottomShift = Boolean(
|
const withMessageListBottomShift = Boolean(
|
||||||
renderingCanRestartBot || renderingCanSubscribe || renderingCanStartBot || isPinnedMessageList,
|
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
|
||||||
|
|| isPinnedMessageList,
|
||||||
);
|
);
|
||||||
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList);
|
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList);
|
||||||
|
|
||||||
@ -469,7 +484,7 @@ const MiddleColumn: FC<StateProps> = ({
|
|||||||
<div className={messagingDisabledClassName}>
|
<div className={messagingDisabledClassName}>
|
||||||
<div className="messaging-disabled-inner">
|
<div className="messaging-disabled-inner">
|
||||||
<span>
|
<span>
|
||||||
{messageSendingRestrictionReason}
|
{composerRestrictionMessage}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -609,7 +624,7 @@ export default memo(withGlobal(
|
|||||||
const isPrivate = isUserId(chatId);
|
const isPrivate = isUserId(chatId);
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const bot = selectChatBot(global, chatId);
|
const bot = selectChatBot(global, chatId);
|
||||||
const pinnedIds = selectPinnedIds(global, chatId);
|
const pinnedIds = selectPinnedIds(global, chatId, threadId);
|
||||||
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
||||||
|
|
||||||
const canPost = chat && getCanPostInChat(chat, threadId);
|
const canPost = chat && getCanPostInChat(chat, threadId);
|
||||||
@ -626,24 +641,30 @@ export default memo(withGlobal(
|
|||||||
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
||||||
const canStartBot = !canRestartBot && isBotNotStarted;
|
const canStartBot = !canRestartBot && isBotNotStarted;
|
||||||
const shouldLoadFullChat = Boolean(chat && isChatGroup(chat) && !chat.fullInfo && lastSyncTime);
|
const shouldLoadFullChat = Boolean(chat && isChatGroup(chat) && !chat.fullInfo && lastSyncTime);
|
||||||
|
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
||||||
|
const shouldBlockBeforeReply = chat?.isForum ? (threadId === MAIN_THREAD_ID && !replyingToId) : false;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
chatId,
|
chatId,
|
||||||
threadId,
|
threadId,
|
||||||
messageListType,
|
messageListType,
|
||||||
|
chat,
|
||||||
|
replyingToId,
|
||||||
isPrivate,
|
isPrivate,
|
||||||
areChatSettingsLoaded: Boolean(chat?.settings),
|
areChatSettingsLoaded: Boolean(chat?.settings),
|
||||||
canPost: !isPinnedMessageList
|
canPost: !isPinnedMessageList
|
||||||
&& (!chat || canPost)
|
&& (!chat || canPost)
|
||||||
|
&& !(isScheduledMessageList && chat?.isForum && threadId === MAIN_THREAD_ID)
|
||||||
&& !isBotNotStarted
|
&& !isBotNotStarted
|
||||||
&& !(shouldJoinToSend && chat?.isNotJoined),
|
&& !(shouldJoinToSend && chat?.isNotJoined)
|
||||||
|
&& !shouldBlockBeforeReply,
|
||||||
isPinnedMessageList,
|
isPinnedMessageList,
|
||||||
isScheduledMessageList,
|
isScheduledMessageList,
|
||||||
currentUserBannedRights: chat?.currentUserBannedRights,
|
currentUserBannedRights: chat?.currentUserBannedRights,
|
||||||
defaultBannedRights: chat?.defaultBannedRights,
|
defaultBannedRights: chat?.defaultBannedRights,
|
||||||
hasPinnedOrAudioPlayer: (
|
hasPinnedOrAudioPlayer: (
|
||||||
threadId !== MAIN_THREAD_ID
|
(threadId !== MAIN_THREAD_ID && !chat?.isForum)
|
||||||
|| Boolean(!isPinnedMessageList && pinnedIds?.length)
|
|| Boolean(!isPinnedMessageList && pinnedIds?.length)
|
||||||
|| Boolean(audioChatId && audioMessageId)
|
|| Boolean(audioChatId && audioMessageId)
|
||||||
),
|
),
|
||||||
|
|||||||
@ -284,6 +284,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
--custom-emoji-size: 1.375rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@ -291,10 +293,6 @@
|
|||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-emoji {
|
|
||||||
--custom-emoji-size: 1.375rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status,
|
.status,
|
||||||
@ -322,11 +320,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Avatar {
|
.Avatar, .topic-header-icon {
|
||||||
margin-right: 0.625rem;
|
|
||||||
// TODO For some reason webpack imports `Audio.scss` second time when loading calls bundle
|
// TODO For some reason webpack imports `Audio.scss` second time when loading calls bundle
|
||||||
width: 2.5rem !important;
|
width: 2.5rem !important;
|
||||||
height: 2.5rem !important;
|
height: 2.5rem !important;
|
||||||
|
|
||||||
|
margin-right: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-header-icon {
|
||||||
|
--custom-emoji-size: 2.5rem;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
|
||||||
|
.emoji-small {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topic-icon-letter {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.general-forum-icon {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.Avatar {
|
||||||
font-size: 1.0625rem;
|
font-size: 1.0625rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
selectPinnedIds,
|
selectPinnedIds,
|
||||||
selectScheduledIds,
|
selectScheduledIds,
|
||||||
selectThreadInfo,
|
selectThreadInfo,
|
||||||
|
selectThreadParam,
|
||||||
selectThreadTopMessageId,
|
selectThreadTopMessageId,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||||
@ -141,12 +142,13 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const chatTitleLength = chat && getChatTitle(lang, chat).length;
|
const chatTitleLength = chat && getChatTitle(lang, chat).length;
|
||||||
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
|
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
|
||||||
const { settings } = chat || {};
|
const { settings } = chat || {};
|
||||||
|
const isForum = chat?.isForum;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (threadId === MAIN_THREAD_ID && lastSyncTime && isReady) {
|
if (lastSyncTime && isReady && (threadId === MAIN_THREAD_ID || isForum)) {
|
||||||
loadPinnedMessages({ chatId });
|
loadPinnedMessages({ chatId, threadId });
|
||||||
}
|
}
|
||||||
}, [chatId, loadPinnedMessages, lastSyncTime, threadId, isReady]);
|
}, [chatId, loadPinnedMessages, lastSyncTime, threadId, isReady, isForum]);
|
||||||
|
|
||||||
// Reset pinned index when switching chats and pinning/unpinning
|
// Reset pinned index when switching chats and pinning/unpinning
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -165,8 +167,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const shouldAnimateTools = useRef<boolean>(true);
|
const shouldAnimateTools = useRef<boolean>(true);
|
||||||
|
|
||||||
const handleHeaderClick = useCallback(() => {
|
const handleHeaderClick = useCallback(() => {
|
||||||
openChatWithInfo({ id: chatId });
|
openChatWithInfo({ id: chatId, threadId });
|
||||||
}, [openChatWithInfo, chatId]);
|
}, [openChatWithInfo, chatId, threadId]);
|
||||||
|
|
||||||
const handleUnpinMessage = useCallback((messageId: number) => {
|
const handleUnpinMessage = useCallback((messageId: number) => {
|
||||||
pinMessage({ chatId, messageId, isUnpin: true });
|
pinMessage({ chatId, messageId, isUnpin: true });
|
||||||
@ -182,8 +184,8 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
}, [pinnedMessage, focusMessage, threadId, pinnedMessagesCount, pinnedMessageIndex]);
|
}, [pinnedMessage, focusMessage, threadId, pinnedMessagesCount, pinnedMessageIndex]);
|
||||||
|
|
||||||
const handleAllPinnedClick = useCallback(() => {
|
const handleAllPinnedClick = useCallback(() => {
|
||||||
openChat({ id: chatId, threadId: MAIN_THREAD_ID, type: 'pinned' });
|
openChat({ id: chatId, threadId, type: 'pinned' });
|
||||||
}, [openChat, chatId]);
|
}, [openChat, chatId, threadId]);
|
||||||
|
|
||||||
const setBackButtonActive = useCallback(() => {
|
const setBackButtonActive = useCallback(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@ -207,7 +209,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (threadId === MAIN_THREAD_ID && messageListType === 'thread' && currentTransitionKey === 0) {
|
if (messageListType === 'thread' && currentTransitionKey === 0) {
|
||||||
if (IS_SINGLE_COLUMN_LAYOUT || shouldShowCloseButton) {
|
if (IS_SINGLE_COLUMN_LAYOUT || shouldShowCloseButton) {
|
||||||
e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets
|
e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets
|
||||||
openChat({ id: undefined }, { forceOnHeavyAnimation: true });
|
openChat({ id: undefined }, { forceOnHeavyAnimation: true });
|
||||||
@ -223,7 +225,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
openPreviousChat();
|
openPreviousChat();
|
||||||
setBackButtonActive();
|
setBackButtonActive();
|
||||||
}, [
|
}, [
|
||||||
threadId, messageListType, currentTransitionKey, isSelectModeActive, openPreviousChat, shouldShowCloseButton,
|
messageListType, currentTransitionKey, isSelectModeActive, openPreviousChat, shouldShowCloseButton,
|
||||||
openChat, toggleLeftColumn, exitMessageSelectMode, setBackButtonActive,
|
openChat, toggleLeftColumn, exitMessageSelectMode, setBackButtonActive,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -304,35 +306,29 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing, true);
|
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing, true);
|
||||||
|
|
||||||
function renderInfo() {
|
function renderInfo() {
|
||||||
|
if (messageListType === 'thread') {
|
||||||
|
if (threadId === MAIN_THREAD_ID || chat?.isForum) {
|
||||||
|
return renderChatInfo();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
messageListType === 'thread' && threadId === MAIN_THREAD_ID ? (
|
<>
|
||||||
renderMainThreadInfo()
|
{renderBackButton()}
|
||||||
) : messageListType === 'thread' ? (
|
<h3>
|
||||||
<>
|
{messagesCount !== undefined ? (
|
||||||
{renderBackButton()}
|
messageListType === 'thread' ? (lang('CommentsCount', messagesCount, 'i'))
|
||||||
<h3>
|
: messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i'))
|
||||||
{lang('CommentsCount', messagesCount, 'i')}
|
: messageListType === 'scheduled' ? (
|
||||||
</h3>
|
isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')
|
||||||
</>
|
) : undefined
|
||||||
) : messageListType === 'pinned' ? (
|
) : lang('Loading')}
|
||||||
<>
|
</h3>
|
||||||
{renderBackButton()}
|
</>
|
||||||
<h3>
|
|
||||||
{lang('PinnedMessagesCount', messagesCount, 'i')}
|
|
||||||
</h3>
|
|
||||||
</>
|
|
||||||
) : messageListType === 'scheduled' ? (
|
|
||||||
<>
|
|
||||||
{renderBackButton()}
|
|
||||||
<h3>
|
|
||||||
{isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')}
|
|
||||||
</h3>
|
|
||||||
</>
|
|
||||||
) : undefined
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMainThreadInfo() {
|
function renderChatInfo() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, true)}
|
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, true)}
|
||||||
@ -355,11 +351,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<GroupChatInfo
|
<GroupChatInfo
|
||||||
key={chatId}
|
key={chatId}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
typingStatus={typingStatus}
|
typingStatus={typingStatus}
|
||||||
status={connectionStatusText}
|
status={connectionStatusText}
|
||||||
withDots={Boolean(connectionStatusText)}
|
withDots={Boolean(connectionStatusText)}
|
||||||
withMediaViewer
|
withMediaViewer={threadId === MAIN_THREAD_ID}
|
||||||
withFullInfo
|
withFullInfo={threadId === MAIN_THREAD_ID}
|
||||||
withUpdatingStatus
|
withUpdatingStatus
|
||||||
withVideoAvatar={isReady}
|
withVideoAvatar={isReady}
|
||||||
noRtl
|
noRtl
|
||||||
@ -456,7 +453,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global, { chatId, threadId, messageListType }): StateProps => {
|
(global, { chatId, threadId, messageListType }): StateProps => {
|
||||||
const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global;
|
const { isLeftColumnShown, lastSyncTime, shouldSkipHistoryAnimations } = global;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
const { typingStatus } = chat || {};
|
|
||||||
|
|
||||||
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
|
||||||
const audioMessage = audioChatId && audioMessageId
|
const audioMessage = audioChatId && audioMessageId
|
||||||
@ -465,10 +461,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
let messagesCount: number | undefined;
|
let messagesCount: number | undefined;
|
||||||
if (messageListType === 'pinned') {
|
if (messageListType === 'pinned') {
|
||||||
const pinnedIds = selectPinnedIds(global, chatId);
|
const pinnedIds = selectPinnedIds(global, chatId, threadId);
|
||||||
messagesCount = pinnedIds?.length;
|
messagesCount = pinnedIds?.length;
|
||||||
} else if (messageListType === 'scheduled') {
|
} else if (messageListType === 'scheduled') {
|
||||||
const scheduledIds = selectScheduledIds(global, chatId);
|
const scheduledIds = selectScheduledIds(global, chatId, threadId);
|
||||||
messagesCount = scheduledIds?.length;
|
messagesCount = scheduledIds?.length;
|
||||||
} else if (messageListType === 'thread' && threadId !== MAIN_THREAD_ID) {
|
} else if (messageListType === 'thread' && threadId !== MAIN_THREAD_ID) {
|
||||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||||
@ -480,9 +476,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const canRestartBot = Boolean(isChatWithBot && selectIsUserBlocked(global, chatId));
|
const canRestartBot = Boolean(isChatWithBot && selectIsUserBlocked(global, chatId));
|
||||||
const canStartBot = isChatWithBot && !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
const canStartBot = isChatWithBot && !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
||||||
const canSubscribe = Boolean(
|
const canSubscribe = Boolean(
|
||||||
isMainThread && chat && (isChatChannel(chat) || isChatSuperGroup(chat)) && chat.isNotJoined,
|
chat && (isMainThread || chat.isForum) && (isChatChannel(chat) || isChatSuperGroup(chat)) && chat.isNotJoined,
|
||||||
);
|
);
|
||||||
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
||||||
|
const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus');
|
||||||
|
|
||||||
const state: StateProps = {
|
const state: StateProps = {
|
||||||
typingStatus,
|
typingStatus,
|
||||||
@ -508,7 +505,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
Object.assign(state, { messagesById });
|
Object.assign(state, { messagesById });
|
||||||
|
|
||||||
if (threadId !== MAIN_THREAD_ID) {
|
if (threadId !== MAIN_THREAD_ID && !chat?.isForum) {
|
||||||
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;
|
||||||
@ -521,7 +518,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pinnedMessageIds = selectPinnedIds(global, chatId);
|
const pinnedMessageIds = selectPinnedIds(global, chatId, threadId);
|
||||||
if (pinnedMessageIds?.length) {
|
if (pinnedMessageIds?.length) {
|
||||||
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
|
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -19,6 +19,7 @@ type OwnProps = {
|
|||||||
bot: ApiAttachBot;
|
bot: ApiAttachBot;
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
onMenuOpened: VoidFunction;
|
onMenuOpened: VoidFunction;
|
||||||
onMenuClosed: VoidFunction;
|
onMenuClosed: VoidFunction;
|
||||||
};
|
};
|
||||||
@ -27,6 +28,7 @@ const AttachBotItem: FC<OwnProps> = ({
|
|||||||
bot,
|
bot,
|
||||||
theme,
|
theme,
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
onMenuOpened,
|
onMenuOpened,
|
||||||
onMenuClosed,
|
onMenuClosed,
|
||||||
}) => {
|
}) => {
|
||||||
@ -74,6 +76,7 @@ const AttachBotItem: FC<OwnProps> = ({
|
|||||||
onClick={() => callAttachBot({
|
onClick={() => callAttachBot({
|
||||||
botId: bot.id,
|
botId: bot.id,
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
})}
|
})}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import './AttachMenu.scss';
|
|||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
|
threadId?: number;
|
||||||
isButtonVisible: boolean;
|
isButtonVisible: boolean;
|
||||||
canAttachMedia: boolean;
|
canAttachMedia: boolean;
|
||||||
canAttachPolls: boolean;
|
canAttachPolls: boolean;
|
||||||
@ -37,6 +38,7 @@ export type OwnProps = {
|
|||||||
|
|
||||||
const AttachMenu: FC<OwnProps> = ({
|
const AttachMenu: FC<OwnProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
|
threadId,
|
||||||
isButtonVisible,
|
isButtonVisible,
|
||||||
canAttachMedia,
|
canAttachMedia,
|
||||||
canAttachPolls,
|
canAttachPolls,
|
||||||
@ -149,6 +151,7 @@ const AttachMenu: FC<OwnProps> = ({
|
|||||||
<AttachBotItem
|
<AttachBotItem
|
||||||
bot={bot}
|
bot={bot}
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onMenuOpened={markAttachmentBotMenuOpen}
|
onMenuOpened={markAttachmentBotMenuOpen}
|
||||||
onMenuClosed={unmarkAttachmentBotMenuOpen}
|
onMenuClosed={unmarkAttachmentBotMenuOpen}
|
||||||
|
|||||||
@ -21,9 +21,6 @@ import type {
|
|||||||
ApiBotMenuButton,
|
ApiBotMenuButton,
|
||||||
ApiAttachMenuPeerType,
|
ApiAttachMenuPeerType,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import {
|
|
||||||
MAIN_THREAD_ID,
|
|
||||||
} from '../../../api/types';
|
|
||||||
import type { InlineBotSettings, ISettings } from '../../../types';
|
import type { InlineBotSettings, ISettings } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -322,7 +319,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
}, [chatId]);
|
}, [chatId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (chatId && lastSyncTime && threadId === MAIN_THREAD_ID && isReady) {
|
if (chatId && lastSyncTime && isReady) {
|
||||||
loadScheduledHistory({ chatId });
|
loadScheduledHistory({ chatId });
|
||||||
}
|
}
|
||||||
}, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]);
|
}, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]);
|
||||||
@ -567,7 +564,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
stopRecordingVoiceRef.current!();
|
stopRecordingVoiceRef.current!();
|
||||||
resetComposer();
|
resetComposer();
|
||||||
};
|
};
|
||||||
}, [chatId, resetComposer, stopRecordingVoiceRef]);
|
}, [chatId, threadId, resetComposer, stopRecordingVoiceRef]);
|
||||||
|
|
||||||
const showCustomEmojiPremiumNotification = useCallback(() => {
|
const showCustomEmojiPremiumNotification = useCallback(() => {
|
||||||
const notificationNumber = customEmojiNotificationNumber.current;
|
const notificationNumber = customEmojiNotificationNumber.current;
|
||||||
@ -741,11 +738,14 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const handleClickBotMenu = useCallback(() => {
|
const handleClickBotMenu = useCallback(() => {
|
||||||
if (botMenuButton?.type !== 'webApp') return;
|
if (botMenuButton?.type !== 'webApp') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
callAttachBot({
|
callAttachBot({
|
||||||
botId: chatId, chatId, isFromBotMenu: true, url: botMenuButton.url,
|
botId: chatId, chatId, isFromBotMenu: true, url: botMenuButton.url, threadId,
|
||||||
});
|
});
|
||||||
}, [botMenuButton, callAttachBot, chatId]);
|
}, [botMenuButton, callAttachBot, chatId, threadId]);
|
||||||
|
|
||||||
const handleActivateBotCommandMenu = useCallback(() => {
|
const handleActivateBotCommandMenu = useCallback(() => {
|
||||||
closeSymbolMenu();
|
closeSymbolMenu();
|
||||||
@ -1298,6 +1298,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
<AttachMenu
|
<AttachMenu
|
||||||
chatId={chatId}
|
chatId={chatId}
|
||||||
|
threadId={threadId}
|
||||||
isButtonVisible={!activeVoiceRecording && !editingMessage}
|
isButtonVisible={!activeVoiceRecording && !editingMessage}
|
||||||
canAttachMedia={canAttachMedia}
|
canAttachMedia={canAttachMedia}
|
||||||
canAttachPolls={canAttachPolls}
|
canAttachPolls={canAttachPolls}
|
||||||
@ -1414,7 +1415,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const isChatWithBot = Boolean(chatBot);
|
const isChatWithBot = Boolean(chatBot);
|
||||||
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
|
||||||
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
|
const messageWithActualBotKeyboard = isChatWithBot && selectNewestMessageWithBotKeyboardButtons(global, chatId);
|
||||||
const scheduledIds = selectScheduledIds(global, chatId);
|
const scheduledIds = selectScheduledIds(global, chatId, threadId);
|
||||||
const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey;
|
const { language, shouldSuggestStickers, shouldSuggestCustomEmoji } = global.settings.byKey;
|
||||||
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
|
const baseEmojiKeywords = global.emojiKeywords[BASE_EMOJI_KEYWORD_LANG];
|
||||||
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
||||||
@ -1453,8 +1454,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isRightColumnShown: selectIsRightColumnShown(global),
|
isRightColumnShown: selectIsRightColumnShown(global),
|
||||||
isSelectModeActive: selectIsInSelectMode(global),
|
isSelectModeActive: selectIsInSelectMode(global),
|
||||||
withScheduledButton: (
|
withScheduledButton: (
|
||||||
threadId === MAIN_THREAD_ID
|
messageListType === 'thread'
|
||||||
&& messageListType === 'thread'
|
|
||||||
&& Boolean(scheduledIds?.length)
|
&& Boolean(scheduledIds?.length)
|
||||||
),
|
),
|
||||||
shouldSchedule: messageListType === 'scheduled',
|
shouldSchedule: messageListType === 'scheduled',
|
||||||
|
|||||||