Management: Support adding and removing members, fix editing legacy groups (#1224)
This commit is contained in:
parent
ec442f13be
commit
08a27a98b8
@ -573,74 +573,104 @@ function buildAction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let text = '';
|
let text = '';
|
||||||
|
const translationValues = [];
|
||||||
let type: ApiAction['type'] = 'other';
|
let type: ApiAction['type'] = 'other';
|
||||||
let photo: ApiPhoto | undefined;
|
let photo: ApiPhoto | undefined;
|
||||||
|
|
||||||
const targetUserId = 'users' in action
|
const targetUserIds = 'users' in action
|
||||||
// Api returns array of userIds, but no action currently has multiple users in it
|
? action.users && action.users
|
||||||
? action.users && action.users[0]
|
: ('userId' in action && [action.userId]) || [];
|
||||||
: ('userId' in action && action.userId) || undefined;
|
|
||||||
let targetChatId: number | undefined;
|
let targetChatId: number | undefined;
|
||||||
|
|
||||||
if (action instanceof GramJs.MessageActionChatCreate) {
|
if (action instanceof GramJs.MessageActionChatCreate) {
|
||||||
text = `%action_origin% created the group «${action.title}»`;
|
text = 'Notification.CreatedChatWithTitle';
|
||||||
|
translationValues.push('%action_origin%', action.title);
|
||||||
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
|
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
|
||||||
text = isChannelPost
|
if (isChannelPost) {
|
||||||
? `Channel renamed to «${action.title}»`
|
text = 'Channel.MessageTitleUpdated';
|
||||||
: `%action_origin% changed group name to «${action.title}»`;
|
translationValues.push(action.title);
|
||||||
|
} else {
|
||||||
|
text = 'Notification.ChangedGroupName';
|
||||||
|
translationValues.push('%action_origin%', action.title);
|
||||||
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
} else if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
||||||
text = isChannelPost
|
if (isChannelPost) {
|
||||||
? 'Channel photo updated'
|
text = 'Channel.MessagePhotoUpdated';
|
||||||
: '%action_origin% updated group photo';
|
} else {
|
||||||
|
text = 'Notification.ChangedGroupPhoto';
|
||||||
|
translationValues.push('%action_origin%');
|
||||||
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
||||||
text = isChannelPost
|
if (isChannelPost) {
|
||||||
? 'Channel photo was deleted'
|
text = 'Channel.MessagePhotoRemoved';
|
||||||
: 'Chat photo was deleted';
|
} else {
|
||||||
|
text = 'Group.MessagePhotoRemoved';
|
||||||
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionChatAddUser) {
|
} else if (action instanceof GramJs.MessageActionChatAddUser) {
|
||||||
text = !senderId || senderId === targetUserId
|
if (!senderId || targetUserIds.includes(senderId)) {
|
||||||
? '%target_user% joined the group'
|
text = 'Notification.JoinedChat';
|
||||||
: '%action_origin% added %target_user% to the group';
|
translationValues.push('%target_user%');
|
||||||
|
} else {
|
||||||
|
text = 'Notification.Invited';
|
||||||
|
translationValues.push('%action_origin%', '%target_user%');
|
||||||
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
} else if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
||||||
text = !senderId || senderId === targetUserId
|
if (!senderId || targetUserIds.includes(senderId)) {
|
||||||
? '%target_user% left the group'
|
text = 'Notification.LeftChat';
|
||||||
: '%action_origin% removed %target_user% from the group';
|
translationValues.push('%target_user%');
|
||||||
|
} else {
|
||||||
|
text = 'Notification.Kicked';
|
||||||
|
translationValues.push('%action_origin%', '%target_user%');
|
||||||
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionChatJoinedByLink) {
|
} else if (action instanceof GramJs.MessageActionChatJoinedByLink) {
|
||||||
text = '%action_origin% joined the chat from invitation link';
|
text = 'Notification.JoinedGroupByLink';
|
||||||
|
translationValues.push('%action_origin%');
|
||||||
} else if (action instanceof GramJs.MessageActionChannelCreate) {
|
} else if (action instanceof GramJs.MessageActionChannelCreate) {
|
||||||
text = 'Channel created';
|
text = 'Notification.CreatedChannel';
|
||||||
} else if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
} else if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
||||||
|
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||||
text = 'Migrated to %target_chat%';
|
text = 'Migrated to %target_chat%';
|
||||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
translationValues.push('%target_chat%');
|
||||||
} else if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
} else if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
||||||
text = 'Migrated from %target_chat%';
|
|
||||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||||
|
text = 'Migrated from %target_chat%';
|
||||||
|
translationValues.push('%target_chat%');
|
||||||
} else if (action instanceof GramJs.MessageActionPinMessage) {
|
} else if (action instanceof GramJs.MessageActionPinMessage) {
|
||||||
text = '%action_origin% pinned %message%';
|
text = 'Notification.PinnedTextMessage';
|
||||||
|
translationValues.push('%action_origin%', '%message%');
|
||||||
} else if (action instanceof GramJs.MessageActionHistoryClear) {
|
} else if (action instanceof GramJs.MessageActionHistoryClear) {
|
||||||
text = 'Chat history was cleared';
|
text = 'HistoryCleared';
|
||||||
type = 'historyClear';
|
type = 'historyClear';
|
||||||
} else if (action instanceof GramJs.MessageActionPhoneCall) {
|
} else if (action instanceof GramJs.MessageActionPhoneCall) {
|
||||||
text = `${isOutgoing ? 'Outgoing' : 'Incoming'} ${action.video ? 'Video' : 'Phone'} Call`;
|
const withDuration = Boolean(action.duration);
|
||||||
|
text = [
|
||||||
|
withDuration ? 'ChatList.Service' : 'Chat',
|
||||||
|
action.video ? 'VideoCall' : 'Call',
|
||||||
|
isOutgoing ? (withDuration ? 'outgoing' : 'Outgoing') : (withDuration ? 'incoming' : 'Incoming'),
|
||||||
|
].join('.');
|
||||||
|
|
||||||
if (action.duration) {
|
if (withDuration) {
|
||||||
const mins = Math.max(Math.round(action.duration / 60), 1);
|
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
||||||
text += ` (${mins} min${mins > 1 ? 's' : ''})`;
|
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||||
}
|
}
|
||||||
} else if (action instanceof GramJs.MessageActionContactSignUp) {
|
} else if (action instanceof GramJs.MessageActionContactSignUp) {
|
||||||
text = '%action_origin% joined Telegram';
|
text = 'Notification.Joined';
|
||||||
|
translationValues.push('%action_origin%');
|
||||||
} else if (action instanceof GramJs.MessageActionPaymentSent) {
|
} else if (action instanceof GramJs.MessageActionPaymentSent) {
|
||||||
const currencySign = getCurrencySign(action.currency);
|
const currencySign = getCurrencySign(action.currency);
|
||||||
const amount = (Number(action.totalAmount) / 100).toFixed(2);
|
const amount = (Number(action.totalAmount) / 100).toFixed(2);
|
||||||
text = `You successfully transferred ${currencySign}${amount} to shop for %product%`;
|
text = 'Notification.PaymentSent';
|
||||||
|
translationValues.push(currencySign, amount, '%product%');
|
||||||
} else if (action instanceof GramJs.MessageActionGroupCall) {
|
} else if (action instanceof GramJs.MessageActionGroupCall) {
|
||||||
if (action.duration) {
|
if (action.duration) {
|
||||||
const mins = Math.max(Math.round(action.duration / 60), 1);
|
const mins = Math.max(Math.round(action.duration / 60), 1);
|
||||||
text = `Voice chat ended (${mins} min${mins > 1 ? 's' : ''})`;
|
text = 'Notification.VoiceChatEnded';
|
||||||
|
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||||
} else {
|
} else {
|
||||||
text = 'Voice chat started';
|
text = 'Notification.VoiceChatStartedChannel';
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = '%ACTION_NOT_IMPLEMENTED%';
|
text = 'ChatList.UnsupportedMessage';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ('photo' in action && action.photo instanceof GramJs.Photo) {
|
if ('photo' in action && action.photo instanceof GramJs.Photo) {
|
||||||
@ -651,9 +681,10 @@ function buildAction(
|
|||||||
return {
|
return {
|
||||||
text,
|
text,
|
||||||
type,
|
type,
|
||||||
targetUserId,
|
targetUserIds,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
photo, // TODO Only used internally now, will be used for the UI in future
|
photo, // TODO Only used internally now, will be used for the UI in future
|
||||||
|
translationValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
const avatarHash = mtpUser.photo instanceof GramJs.UserProfilePhoto
|
const avatarHash = mtpUser.photo instanceof GramJs.UserProfilePhoto
|
||||||
? String(mtpUser.photo.photoId)
|
? String(mtpUser.photo.photoId)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
const userType = buildApiUserType(mtpUser);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@ -33,8 +34,9 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
|||||||
...(mtpUser.self && { isSelf: true }),
|
...(mtpUser.self && { isSelf: true }),
|
||||||
...(mtpUser.verified && { isVerified: true }),
|
...(mtpUser.verified && { isVerified: true }),
|
||||||
...((mtpUser.contact || mtpUser.mutualContact) && { isContact: true }),
|
...((mtpUser.contact || mtpUser.mutualContact) && { isContact: true }),
|
||||||
type: buildApiUserType(mtpUser),
|
type: userType,
|
||||||
...(firstName && { firstName }),
|
...(firstName && { firstName }),
|
||||||
|
...(userType === 'userTypeBot' && { canBeInvitedToGroup: !mtpUser.botNochats }),
|
||||||
...(lastName && { lastName }),
|
...(lastName && { lastName }),
|
||||||
username: mtpUser.username || '',
|
username: mtpUser.username || '',
|
||||||
phoneNumber: mtpUser.phone || '',
|
phoneNumber: mtpUser.phone || '',
|
||||||
|
|||||||
@ -172,7 +172,7 @@ export async function searchChats({ query }: { query: string }) {
|
|||||||
updateLocalDb(result);
|
updateLocalDb(result);
|
||||||
|
|
||||||
const localPeerIds = result.myResults.map(getApiChatIdFromMtpPeer);
|
const localPeerIds = result.myResults.map(getApiChatIdFromMtpPeer);
|
||||||
const allChats = [...result.chats, ...result.users]
|
const allChats = result.chats.concat(result.users)
|
||||||
.map((user) => buildApiChatFromPreview(user))
|
.map((user) => buildApiChatFromPreview(user))
|
||||||
.filter<ApiChat>(Boolean as any);
|
.filter<ApiChat>(Boolean as any);
|
||||||
const allUsers = result.users.map(buildApiUser).filter((user) => !!user && !user.isSelf) as ApiUser[];
|
const allUsers = result.users.map(buildApiUser).filter((user) => !!user && !user.isSelf) as ApiUser[];
|
||||||
@ -756,15 +756,15 @@ export function updateChatDefaultBannedRights({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function updateChatMemberBannedRights({
|
export function updateChatMemberBannedRights({
|
||||||
chat, user, bannedRights,
|
chat, user, bannedRights, untilDate,
|
||||||
}: { chat: ApiChat; user: ApiUser; bannedRights: ApiChatBannedRights }) {
|
}: { chat: ApiChat; user: ApiUser; bannedRights: ApiChatBannedRights; untilDate?: number }) {
|
||||||
const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
|
const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
|
||||||
const participant = buildInputPeer(user.id, user.accessHash) as GramJs.InputUser;
|
const participant = buildInputPeer(user.id, user.accessHash) as GramJs.InputUser;
|
||||||
|
|
||||||
return invokeRequest(new GramJs.channels.EditBanned({
|
return invokeRequest(new GramJs.channels.EditBanned({
|
||||||
channel,
|
channel,
|
||||||
participant,
|
participant,
|
||||||
bannedRights: buildChatBannedRights(bannedRights),
|
bannedRights: buildChatBannedRights(bannedRights, untilDate),
|
||||||
}), true);
|
}), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -957,6 +957,51 @@ export async function openChatByInvite(hash: string) {
|
|||||||
return { chatId: chat.id };
|
return { chatId: chat.id };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addChatMembers(chat: ApiChat, users: ApiUser[]) {
|
||||||
|
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
|
||||||
|
return invokeRequest(new GramJs.channels.InviteToChannel({
|
||||||
|
channel: buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel,
|
||||||
|
users: users.map((user) => buildInputEntity(user.id, user.accessHash)) as GramJs.InputUser[],
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(users.map((user) => {
|
||||||
|
return invokeRequest(new GramJs.messages.AddChatUser({
|
||||||
|
chatId: buildInputEntity(chat.id) as number,
|
||||||
|
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||||
|
}), true);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteChatMember(chat: ApiChat, user: ApiUser) {
|
||||||
|
if (chat.type === 'chatTypeChannel' || chat.type === 'chatTypeSuperGroup') {
|
||||||
|
return updateChatMemberBannedRights({
|
||||||
|
chat,
|
||||||
|
user,
|
||||||
|
bannedRights: {
|
||||||
|
viewMessages: true,
|
||||||
|
sendMessages: true,
|
||||||
|
sendMedia: true,
|
||||||
|
sendStickers: true,
|
||||||
|
sendGifs: true,
|
||||||
|
sendGames: true,
|
||||||
|
sendInline: true,
|
||||||
|
embedLinks: true,
|
||||||
|
sendPolls: true,
|
||||||
|
changeInfo: true,
|
||||||
|
inviteUsers: true,
|
||||||
|
pinMessages: true,
|
||||||
|
},
|
||||||
|
untilDate: MAX_INT_32,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return invokeRequest(new GramJs.messages.DeleteChatUser({
|
||||||
|
chatId: buildInputEntity(chat.id) as number,
|
||||||
|
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||||
|
}), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function preparePeers(
|
function preparePeers(
|
||||||
result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs,
|
result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs,
|
||||||
) {
|
) {
|
||||||
|
|||||||
@ -14,7 +14,7 @@ export {
|
|||||||
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
fetchChatFolders, editChatFolder, deleteChatFolder, 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,
|
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember,
|
||||||
} from './chats';
|
} from './chats';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -213,6 +213,14 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
|||||||
if (update._entities && update._entities.some((e): e is GramJs.User => (
|
if (update._entities && update._entities.some((e): e is GramJs.User => (
|
||||||
e instanceof GramJs.User && !!e.self && e.id === action.userId
|
e instanceof GramJs.User && !!e.self && e.id === action.userId
|
||||||
))) {
|
))) {
|
||||||
|
onUpdate({
|
||||||
|
'@type': 'updateChat',
|
||||||
|
id: message.chatId,
|
||||||
|
chat: {
|
||||||
|
isRestricted: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateChatLeave',
|
'@type': 'updateChatLeave',
|
||||||
id: message.chatId,
|
id: message.chatId,
|
||||||
|
|||||||
@ -145,10 +145,11 @@ export type ApiNewPoll = {
|
|||||||
|
|
||||||
export interface ApiAction {
|
export interface ApiAction {
|
||||||
text: string;
|
text: string;
|
||||||
targetUserId?: number;
|
targetUserIds?: number[];
|
||||||
targetChatId?: number;
|
targetChatId?: number;
|
||||||
type: 'historyClear' | 'other';
|
type: 'historyClear' | 'other';
|
||||||
photo?: ApiPhoto;
|
photo?: ApiPhoto;
|
||||||
|
translationValues: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiWebPage {
|
export interface ApiWebPage {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export interface ApiUser {
|
|||||||
accessHash?: string;
|
accessHash?: string;
|
||||||
avatarHash?: string;
|
avatarHash?: string;
|
||||||
photos?: ApiPhoto[];
|
photos?: ApiPhoto[];
|
||||||
|
canBeInvitedToGroup?: boolean;
|
||||||
|
|
||||||
// Obtained from GetFullUser / UserFullInfo
|
// Obtained from GetFullUser / UserFullInfo
|
||||||
fullInfo?: ApiUserFullInfo;
|
fullInfo?: ApiUserFullInfo;
|
||||||
|
|||||||
@ -165,7 +165,7 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{renderMessage()}
|
{renderMessage()}
|
||||||
{canDeleteForAll && (
|
{canDeleteForAll && (
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||||
{contactName ? lang('ChatList.DeleteForEveryone', contactName) : lang('DeleteForAll')}
|
{contactName ? renderText(lang('ChatList.DeleteForEveryone', contactName)) : lang('DeleteForAll')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}>
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}>
|
||||||
|
|||||||
@ -98,7 +98,7 @@ const DeleteMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
)}
|
)}
|
||||||
{canDeleteForAll && (
|
{canDeleteForAll && (
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||||
{contactName && lang('Conversation.DeleteMessagesFor', renderText(contactName))}
|
{contactName && renderText(lang('Conversation.DeleteMessagesFor', contactName))}
|
||||||
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
{!contactName && lang('Conversation.DeleteMessagesForEveryone')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type OwnProps = {
|
|||||||
notFoundText?: string;
|
notFoundText?: string;
|
||||||
searchInputId?: string;
|
searchInputId?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
noScrollRestore?: boolean;
|
||||||
onSelectedIdsChange: (ids: number[]) => void;
|
onSelectedIdsChange: (ids: number[]) => void;
|
||||||
onFilterChange: (value: string) => void;
|
onFilterChange: (value: string) => void;
|
||||||
onLoadMore?: () => void;
|
onLoadMore?: () => void;
|
||||||
@ -45,6 +46,7 @@ const Picker: FC<OwnProps> = ({
|
|||||||
notFoundText,
|
notFoundText,
|
||||||
searchInputId,
|
searchInputId,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
noScrollRestore,
|
||||||
onSelectedIdsChange,
|
onSelectedIdsChange,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
@ -107,6 +109,7 @@ const Picker: FC<OwnProps> = ({
|
|||||||
className="picker-list custom-scroll"
|
className="picker-list custom-scroll"
|
||||||
items={viewportIds}
|
items={viewportIds}
|
||||||
onLoadMore={getMore}
|
onLoadMore={getMore}
|
||||||
|
noScrollRestore={noScrollRestore}
|
||||||
>
|
>
|
||||||
{viewportIds.map((id) => (
|
{viewportIds.map((id) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import { pick } from '../../util/iteratees';
|
import { pick } from '../../util/iteratees';
|
||||||
import useLang from '../../hooks/useLang';
|
import useLang from '../../hooks/useLang';
|
||||||
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
import Modal from '../ui/Modal';
|
import Modal from '../ui/Modal';
|
||||||
import Button from '../ui/Button';
|
import Button from '../ui/Button';
|
||||||
@ -91,7 +92,7 @@ const PinMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{canPinForAll && (
|
{canPinForAll && (
|
||||||
<Button className="confirm-dialog-button" isText onClick={handlePinMessageForAll}>
|
<Button className="confirm-dialog-button" isText onClick={handlePinMessageForAll}>
|
||||||
{contactName
|
{contactName
|
||||||
? lang('Conversation.PinMessagesFor', contactName)
|
? renderText(lang('Conversation.PinMessagesFor', contactName))
|
||||||
: lang('Conversation.PinMessageAlert.PinAndNotifyMembers')}
|
: lang('Conversation.PinMessageAlert.PinAndNotifyMembers')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -29,7 +29,7 @@ export function renderActionMessageText(
|
|||||||
lang: LangFn,
|
lang: LangFn,
|
||||||
message: ApiMessage,
|
message: ApiMessage,
|
||||||
actionOrigin?: ApiUser | ApiChat,
|
actionOrigin?: ApiUser | ApiChat,
|
||||||
targetUser?: ApiUser,
|
targetUsers?: ApiUser[],
|
||||||
targetMessage?: ApiMessage,
|
targetMessage?: ApiMessage,
|
||||||
targetChatId?: number,
|
targetChatId?: number,
|
||||||
options: ActionMessageTextOptions = {},
|
options: ActionMessageTextOptions = {},
|
||||||
@ -37,13 +37,13 @@ export function renderActionMessageText(
|
|||||||
if (!message.content.action) {
|
if (!message.content.action) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const { text } = message.content.action;
|
const { text, translationValues } = message.content.action;
|
||||||
const content: TextPart[] = [];
|
const content: TextPart[] = [];
|
||||||
const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 16 };
|
const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 16 };
|
||||||
|
|
||||||
let unprocessed: string;
|
let unprocessed: string;
|
||||||
let processed = processPlaceholder(
|
let processed = processPlaceholder(
|
||||||
text,
|
lang(text, translationValues && translationValues.length ? translationValues : undefined),
|
||||||
'%action_origin%',
|
'%action_origin%',
|
||||||
actionOrigin
|
actionOrigin
|
||||||
? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP
|
? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP
|
||||||
@ -56,8 +56,8 @@ export function renderActionMessageText(
|
|||||||
processed = processPlaceholder(
|
processed = processPlaceholder(
|
||||||
unprocessed,
|
unprocessed,
|
||||||
'%target_user%',
|
'%target_user%',
|
||||||
targetUser
|
targetUsers
|
||||||
? renderUserContent(targetUser, options.asPlain)
|
? targetUsers.map((user) => renderUserContent(user, options.asPlain)).filter<TextPart>(Boolean as any)
|
||||||
: 'User',
|
: 'User',
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ function renderMigratedContent(chatId: number, asPlain?: boolean): string | Text
|
|||||||
return <ChatLink className="action-link" chatId={chatId}>{text}</ChatLink>;
|
return <ChatLink className="action-link" chatId={chatId}>{text}</ChatLink>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function processPlaceholder(text: string, placeholder: string, replaceValue?: TextPart): TextPart[] {
|
function processPlaceholder(text: string, placeholder: string, replaceValue?: TextPart | TextPart[]): TextPart[] {
|
||||||
const placeholderPosition = text.indexOf(placeholder);
|
const placeholderPosition = text.indexOf(placeholder);
|
||||||
if (placeholderPosition < 0 || !replaceValue) {
|
if (placeholderPosition < 0 || !replaceValue) {
|
||||||
return [text];
|
return [text];
|
||||||
@ -188,7 +188,16 @@ function processPlaceholder(text: string, placeholder: string, replaceValue?: Te
|
|||||||
|
|
||||||
const content: TextPart[] = [];
|
const content: TextPart[] = [];
|
||||||
content.push(text.substring(0, placeholderPosition));
|
content.push(text.substring(0, placeholderPosition));
|
||||||
content.push(replaceValue);
|
if (Array.isArray(replaceValue)) {
|
||||||
|
replaceValue.forEach((value, index) => {
|
||||||
|
content.push(value);
|
||||||
|
if (index + 1 < replaceValue.length) {
|
||||||
|
content.push(', ');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
content.push(replaceValue);
|
||||||
|
}
|
||||||
content.push(text.substring(placeholderPosition + placeholder.length));
|
content.push(text.substring(placeholderPosition + placeholder.length));
|
||||||
|
|
||||||
return content;
|
return content;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useCallback, useLayoutEffect, useRef,
|
FC, memo, useCallback, useLayoutEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../lib/teact/teactn';
|
import { withGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -66,7 +66,8 @@ type StateProps = {
|
|||||||
chat?: ApiChat;
|
chat?: ApiChat;
|
||||||
isMuted?: boolean;
|
isMuted?: boolean;
|
||||||
privateChatUser?: ApiUser;
|
privateChatUser?: ApiUser;
|
||||||
actionTargetUser?: ApiUser;
|
usersById?: Record<number, ApiUser>;
|
||||||
|
actionTargetUserIds?: number[];
|
||||||
actionTargetMessage?: ApiMessage;
|
actionTargetMessage?: ApiMessage;
|
||||||
actionTargetChatId?: number;
|
actionTargetChatId?: number;
|
||||||
lastMessageSender?: ApiUser;
|
lastMessageSender?: ApiUser;
|
||||||
@ -91,8 +92,9 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isPinned,
|
isPinned,
|
||||||
chat,
|
chat,
|
||||||
isMuted,
|
isMuted,
|
||||||
|
usersById,
|
||||||
privateChatUser,
|
privateChatUser,
|
||||||
actionTargetUser,
|
actionTargetUserIds,
|
||||||
lastMessageSender,
|
lastMessageSender,
|
||||||
lastMessageOutgoingStatus,
|
lastMessageOutgoingStatus,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
@ -122,6 +124,12 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
||||||
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
||||||
|
|
||||||
|
const actionTargetUsers = useMemo(() => {
|
||||||
|
return actionTargetUserIds
|
||||||
|
? actionTargetUserIds.map((userId) => usersById && usersById[userId]).filter<ApiUser>(Boolean as any)
|
||||||
|
: undefined;
|
||||||
|
}, [actionTargetUserIds, usersById]);
|
||||||
|
|
||||||
// Sets animation excess values when `orderDiff` changes and then resets excess values to animate.
|
// Sets animation excess values when `orderDiff` changes and then resets excess values to animate.
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const element = ref.current;
|
const element = ref.current;
|
||||||
@ -221,7 +229,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
lang,
|
lang,
|
||||||
lastMessage,
|
lastMessage,
|
||||||
actionOrigin,
|
actionOrigin,
|
||||||
actionTargetUser,
|
actionTargetUsers,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
{ asPlain: true },
|
{ asPlain: true },
|
||||||
@ -322,8 +330,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const actionTargetMessage = lastMessageAction && replyToMessageId
|
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const { targetUserId: actionTargetUserId, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||||
const privateChatUserId = getPrivateChatUserId(chat);
|
const privateChatUserId = getPrivateChatUserId(chat);
|
||||||
|
const { byId: usersById } = global.users;
|
||||||
const {
|
const {
|
||||||
chatId: currentChatId,
|
chatId: currentChatId,
|
||||||
threadId: currentThreadId,
|
threadId: currentThreadId,
|
||||||
@ -336,7 +345,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
lastMessageSender,
|
lastMessageSender,
|
||||||
...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }),
|
...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }),
|
||||||
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }),
|
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }),
|
||||||
...(actionTargetUserId && { actionTargetUser: selectUser(global, actionTargetUserId) }),
|
usersById,
|
||||||
|
actionTargetUserIds,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { ApiChat, ApiUser } from '../../../api/types';
|
|||||||
import { pick, unique } from '../../../util/iteratees';
|
import { pick, unique } from '../../../util/iteratees';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import searchWords from '../../../util/searchWords';
|
import searchWords from '../../../util/searchWords';
|
||||||
import { getUserFullName, sortChatIds } from '../../../modules/helpers';
|
import { getUserFullName, isUserBot, sortChatIds } from '../../../modules/helpers';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
@ -98,7 +98,11 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
...foundContactIds,
|
...foundContactIds,
|
||||||
...(localUserIds || []),
|
...(localUserIds || []),
|
||||||
...(globalUserIds || []),
|
...(globalUserIds || []),
|
||||||
]),
|
]).filter((contactId) => {
|
||||||
|
const user = usersById[contactId];
|
||||||
|
|
||||||
|
return !user || !isUserBot(user) || user.canBeInvitedToGroup;
|
||||||
|
}),
|
||||||
chatsById,
|
chatsById,
|
||||||
false,
|
false,
|
||||||
selectedMemberIds,
|
selectedMemberIds,
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, {
|
import React, {
|
||||||
FC, memo, useEffect, useRef,
|
FC, memo, useEffect, useMemo, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../lib/teact/teactn';
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
@ -36,8 +36,9 @@ type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
|
usersById: Record<number, ApiUser>;
|
||||||
sender?: ApiUser | ApiChat;
|
sender?: ApiUser | ApiChat;
|
||||||
targetUser?: ApiUser;
|
targetUserIds?: number[];
|
||||||
targetMessage?: ApiMessage;
|
targetMessage?: ApiMessage;
|
||||||
targetChatId?: number;
|
targetChatId?: number;
|
||||||
isFocused: boolean;
|
isFocused: boolean;
|
||||||
@ -53,8 +54,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
isEmbedded,
|
isEmbedded,
|
||||||
appearanceOrder = 0,
|
appearanceOrder = 0,
|
||||||
isLastInList,
|
isLastInList,
|
||||||
|
usersById,
|
||||||
sender,
|
sender,
|
||||||
targetUser,
|
targetUserIds,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
isFocused,
|
isFocused,
|
||||||
@ -81,11 +83,17 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||||
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
||||||
|
|
||||||
|
const targetUsers = useMemo(() => {
|
||||||
|
return targetUserIds
|
||||||
|
? targetUserIds.map((userId) => usersById && usersById[userId]).filter<ApiUser>(Boolean as any)
|
||||||
|
: undefined;
|
||||||
|
}, [targetUserIds, usersById]);
|
||||||
|
|
||||||
const content = renderActionMessageText(
|
const content = renderActionMessageText(
|
||||||
lang,
|
lang,
|
||||||
message,
|
message,
|
||||||
sender,
|
sender,
|
||||||
targetUser,
|
targetUsers,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
targetChatId,
|
targetChatId,
|
||||||
isEmbedded ? { isEmbedded: true, asPlain: true } : undefined,
|
isEmbedded ? { isEmbedded: true, asPlain: true } : undefined,
|
||||||
@ -140,8 +148,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { message }): StateProps => {
|
(global, { message }): StateProps => {
|
||||||
|
const { byId: usersById } = global.users;
|
||||||
const userId = message.senderId;
|
const userId = message.senderId;
|
||||||
const { targetUserId, targetChatId } = message.content.action || {};
|
const { targetUserIds, targetChatId } = message.content.action || {};
|
||||||
const targetMessageId = message.replyToMessageId;
|
const targetMessageId = message.replyToMessageId;
|
||||||
const targetMessage = targetMessageId
|
const targetMessage = targetMessageId
|
||||||
? selectChatMessage(global, message.chatId, targetMessageId)
|
? selectChatMessage(global, message.chatId, targetMessageId)
|
||||||
@ -156,9 +165,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
: userId ? selectUser(global, userId) : undefined;
|
: userId ? selectUser(global, userId) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
usersById,
|
||||||
sender,
|
sender,
|
||||||
...(targetUserId && { targetUser: selectUser(global, targetUserId) }),
|
|
||||||
targetChatId,
|
targetChatId,
|
||||||
|
targetUserIds,
|
||||||
targetMessage,
|
targetMessage,
|
||||||
isFocused,
|
isFocused,
|
||||||
...(isFocused && { focusDirection, noFocusHighlight }),
|
...(isFocused && { focusDirection, noFocusHighlight }),
|
||||||
|
|||||||
@ -90,7 +90,7 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{canDeleteForAll && (
|
{canDeleteForAll && (
|
||||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||||
{contactName
|
{contactName
|
||||||
? lang('ChatList.DeleteForEveryone', renderText(contactName))
|
? renderText(lang('ChatList.DeleteForEveryone', contactName))
|
||||||
: lang('Conversation.DeleteMessagesForEveryone')}
|
: lang('Conversation.DeleteMessagesForEveryone')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
10
src/components/right/AddChatMembers.scss
Normal file
10
src/components/right/AddChatMembers.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.AddChatMembers {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&-inner {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/components/right/AddChatMembers.tsx
Normal file
203
src/components/right/AddChatMembers.tsx
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
import React, {
|
||||||
|
FC, useCallback, useMemo, memo, useState, useEffect,
|
||||||
|
} from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions } from '../../global/types';
|
||||||
|
import {
|
||||||
|
ApiChat, ApiChatMember, ApiUpdateConnectionStateType, ApiUser,
|
||||||
|
} from '../../api/types';
|
||||||
|
import { NewChatMembersProgress } from '../../types';
|
||||||
|
|
||||||
|
import { pick, unique } from '../../util/iteratees';
|
||||||
|
import { selectChat } from '../../modules/selectors';
|
||||||
|
import searchWords from '../../util/searchWords';
|
||||||
|
import {
|
||||||
|
getUserFullName, isChatChannel, isUserBot, sortChatIds,
|
||||||
|
} from '../../modules/helpers';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||||
|
|
||||||
|
import Picker from '../common/Picker';
|
||||||
|
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||||
|
import Spinner from '../ui/Spinner';
|
||||||
|
|
||||||
|
import './AddChatMembers.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
chatId: number;
|
||||||
|
isActive: boolean;
|
||||||
|
onNextStep: (memberIds: number[]) => void;
|
||||||
|
onClose: NoneToVoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
connectionState?: ApiUpdateConnectionStateType;
|
||||||
|
isChannel?: boolean;
|
||||||
|
members?: ApiChatMember[];
|
||||||
|
currentUserId?: number;
|
||||||
|
usersById: Record<number, ApiUser>;
|
||||||
|
chatsById: Record<number, ApiChat>;
|
||||||
|
localContactIds?: number[];
|
||||||
|
searchQuery?: string;
|
||||||
|
isLoading: boolean;
|
||||||
|
isSearching?: boolean;
|
||||||
|
localUserIds?: number[];
|
||||||
|
globalUserIds?: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'setUserSearchQuery'>;
|
||||||
|
|
||||||
|
const AddChatMembers: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
|
isChannel,
|
||||||
|
connectionState,
|
||||||
|
members,
|
||||||
|
onNextStep,
|
||||||
|
currentUserId,
|
||||||
|
usersById,
|
||||||
|
chatsById,
|
||||||
|
localContactIds,
|
||||||
|
isLoading,
|
||||||
|
searchQuery,
|
||||||
|
isSearching,
|
||||||
|
localUserIds,
|
||||||
|
globalUserIds,
|
||||||
|
setUserSearchQuery,
|
||||||
|
onClose,
|
||||||
|
isActive,
|
||||||
|
loadContactList,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
const [selectedMemberIds, setSelectedMemberIds] = useState<number[]>([]);
|
||||||
|
const prevSelectedMemberIds = usePrevious(selectedMemberIds);
|
||||||
|
const noPickerScrollRestore = prevSelectedMemberIds === selectedMemberIds;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive && connectionState === 'connectionStateReady') {
|
||||||
|
loadContactList();
|
||||||
|
}
|
||||||
|
}, [connectionState, isActive, loadContactList]);
|
||||||
|
|
||||||
|
useHistoryBack(isActive, onClose);
|
||||||
|
|
||||||
|
const memberIds = useMemo(() => {
|
||||||
|
return members ? members.map((member) => member.userId) : [];
|
||||||
|
}, [members]);
|
||||||
|
|
||||||
|
const handleFilterChange = useCallback((query: string) => {
|
||||||
|
setUserSearchQuery({ query });
|
||||||
|
}, [setUserSearchQuery]);
|
||||||
|
|
||||||
|
const displayedIds = useMemo(() => {
|
||||||
|
const contactIds = localContactIds
|
||||||
|
? sortChatIds(localContactIds.filter((id) => id !== currentUserId), chatsById)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (!searchQuery) {
|
||||||
|
return contactIds.filter((id) => !memberIds.includes(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundContactIds = contactIds.filter((id) => {
|
||||||
|
const user = usersById[id];
|
||||||
|
if (!user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const fullName = getUserFullName(user);
|
||||||
|
return fullName && searchWords(fullName, searchQuery);
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortChatIds(
|
||||||
|
unique([
|
||||||
|
...foundContactIds,
|
||||||
|
...(localUserIds || []),
|
||||||
|
...(globalUserIds || []),
|
||||||
|
]).filter((contactId) => {
|
||||||
|
const user = usersById[contactId];
|
||||||
|
|
||||||
|
// The user can be added to the chat if the following conditions are met:
|
||||||
|
// the user has not yet been added to the current chat
|
||||||
|
// AND (it is not found (user from global search) OR it is not a bot OR it is a bot,
|
||||||
|
// but the current chat is not a channel AND the appropriate permission is set).
|
||||||
|
return !memberIds.includes(contactId)
|
||||||
|
&& (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup));
|
||||||
|
}),
|
||||||
|
chatsById,
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
localContactIds, chatsById, searchQuery, localUserIds, globalUserIds,
|
||||||
|
currentUserId, usersById, memberIds, isChannel,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleNextStep = useCallback(() => {
|
||||||
|
if (selectedMemberIds.length) {
|
||||||
|
setUserSearchQuery({ query: '' });
|
||||||
|
onNextStep(selectedMemberIds);
|
||||||
|
}
|
||||||
|
}, [selectedMemberIds, setUserSearchQuery, onNextStep]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="AddChatMembers">
|
||||||
|
<div className="AddChatMembers-inner">
|
||||||
|
<Picker
|
||||||
|
itemIds={displayedIds}
|
||||||
|
selectedIds={selectedMemberIds}
|
||||||
|
filterValue={searchQuery}
|
||||||
|
filterPlaceholder={lang('lng_channel_add_users')}
|
||||||
|
searchInputId="new-members-picker-search"
|
||||||
|
isLoading={isSearching}
|
||||||
|
onSelectedIdsChange={setSelectedMemberIds}
|
||||||
|
onFilterChange={handleFilterChange}
|
||||||
|
noScrollRestore={noPickerScrollRestore}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FloatingActionButton
|
||||||
|
isShown={Boolean(selectedMemberIds.length)}
|
||||||
|
disabled={isLoading}
|
||||||
|
ariaLabel={lang('lng_channel_add_users')}
|
||||||
|
onClick={handleNextStep}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<Spinner color="white" />
|
||||||
|
) : (
|
||||||
|
<i className="icon-arrow-right" />
|
||||||
|
)}
|
||||||
|
</FloatingActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const { userIds: localContactIds } = global.contactList || {};
|
||||||
|
const { byId: usersById } = global.users;
|
||||||
|
const { byId: chatsById } = global.chats;
|
||||||
|
const { currentUserId, newChatMembersProgress, connectionState } = global;
|
||||||
|
const isChannel = chat && isChatChannel(chat);
|
||||||
|
|
||||||
|
const {
|
||||||
|
query: searchQuery,
|
||||||
|
fetchingStatus,
|
||||||
|
globalUserIds,
|
||||||
|
localUserIds,
|
||||||
|
} = global.userSearch;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isChannel,
|
||||||
|
members: chat && chat.fullInfo ? chat.fullInfo.members : undefined,
|
||||||
|
currentUserId,
|
||||||
|
usersById,
|
||||||
|
chatsById,
|
||||||
|
localContactIds,
|
||||||
|
searchQuery,
|
||||||
|
isSearching: fetchingStatus,
|
||||||
|
isLoading: newChatMembersProgress === NewChatMembersProgress.Loading,
|
||||||
|
globalUserIds,
|
||||||
|
localUserIds,
|
||||||
|
connectionState,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, ['loadContactList', 'setUserSearchQuery']),
|
||||||
|
)(AddChatMembers));
|
||||||
77
src/components/right/DeleteMemberModal.tsx
Normal file
77
src/components/right/DeleteMemberModal.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React, { FC, useCallback, memo } from '../../lib/teact/teact';
|
||||||
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
|
import { GlobalActions } from '../../global/types';
|
||||||
|
import { ApiChat } from '../../api/types';
|
||||||
|
|
||||||
|
import { pick } from '../../util/iteratees';
|
||||||
|
import { selectCurrentChat, selectUser } from '../../modules/selectors';
|
||||||
|
import { getUserFirstOrLastName } from '../../modules/helpers';
|
||||||
|
import renderText from '../common/helpers/renderText';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
|
import Modal from '../ui/Modal';
|
||||||
|
import Button from '../ui/Button';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen: boolean;
|
||||||
|
userId?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
contactName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DispatchProps = Pick<GlobalActions, 'deleteChatMember'>;
|
||||||
|
|
||||||
|
const DeleteMemberModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
|
isOpen,
|
||||||
|
chat,
|
||||||
|
userId,
|
||||||
|
contactName,
|
||||||
|
onClose,
|
||||||
|
deleteChatMember,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const handleDeleteChatMember = useCallback(() => {
|
||||||
|
deleteChatMember({ chatId: chat!.id, userId });
|
||||||
|
onClose();
|
||||||
|
}, [chat, deleteChatMember, onClose, userId]);
|
||||||
|
|
||||||
|
if (!chat || !userId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
onEnter={handleDeleteChatMember}
|
||||||
|
className="delete"
|
||||||
|
title={lang('GroupRemoved.Remove')}
|
||||||
|
>
|
||||||
|
<p>{renderText(lang('PeerInfo.Confirm.RemovePeer', contactName))}</p>
|
||||||
|
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChatMember}>
|
||||||
|
{lang('lng_box_remove')}
|
||||||
|
</Button>
|
||||||
|
<Button className="confirm-dialog-button" isText onClick={onClose}>{lang('Cancel')}</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { userId }): StateProps => {
|
||||||
|
const chat = selectCurrentChat(global);
|
||||||
|
const user = userId && selectUser(global, userId);
|
||||||
|
const contactName = user ? getUserFirstOrLastName(user) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
contactName,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
(setGlobal, actions): DispatchProps => pick(actions, ['deleteChatMember']),
|
||||||
|
)(DeleteMemberModal));
|
||||||
@ -1,14 +1,15 @@
|
|||||||
.Profile {
|
.Profile {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: scroll;
|
|
||||||
overflow-x: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
overflow-x: hidden;
|
||||||
|
|
||||||
@supports (overflow-y: overlay) {
|
@supports (overflow-y: overlay) {
|
||||||
overflow-y: overlay !important;
|
overflow-y: overlay !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
> .profile-info > .ChatInfo {
|
> .profile-info > .ChatInfo {
|
||||||
grid-area: chat_info;
|
grid-area: chat_info;
|
||||||
|
|
||||||
@ -124,7 +125,14 @@
|
|||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
padding: .5rem 0;
|
padding: .5rem 0;
|
||||||
|
.ListItem.chat-item-clickable {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.FloatingActionButton {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,7 @@ import {
|
|||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import { GlobalActions } from '../../global/types';
|
import { GlobalActions } from '../../global/types';
|
||||||
import {
|
import {
|
||||||
ISettings,
|
NewChatMembersProgress, ISettings, MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType,
|
||||||
MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType,
|
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -23,7 +22,7 @@ import {
|
|||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||||
import {
|
import {
|
||||||
isChatAdmin, isChatChannel, isChatGroup, isChatPrivate,
|
getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isChatPrivate,
|
||||||
} from '../../modules/helpers';
|
} from '../../modules/helpers';
|
||||||
import {
|
import {
|
||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
@ -54,6 +53,8 @@ import ChatExtra from './ChatExtra';
|
|||||||
import Media from '../common/Media';
|
import Media from '../common/Media';
|
||||||
import WebLink from '../common/WebLink';
|
import WebLink from '../common/WebLink';
|
||||||
import NothingFound from '../common/NothingFound';
|
import NothingFound from '../common/NothingFound';
|
||||||
|
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||||
|
import DeleteMemberModal from './DeleteMemberModal';
|
||||||
|
|
||||||
import './Profile.scss';
|
import './Profile.scss';
|
||||||
|
|
||||||
@ -67,12 +68,15 @@ type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
theme: ISettings['theme'];
|
theme: ISettings['theme'];
|
||||||
isChannel?: boolean;
|
isChannel?: boolean;
|
||||||
|
currentUserId?: number;
|
||||||
resolvedUserId?: number;
|
resolvedUserId?: number;
|
||||||
chatMessages?: Record<number, ApiMessage>;
|
chatMessages?: Record<number, ApiMessage>;
|
||||||
foundIds?: number[];
|
foundIds?: number[];
|
||||||
mediaSearchType?: SharedMediaType;
|
mediaSearchType?: SharedMediaType;
|
||||||
hasMembersTab?: boolean;
|
hasMembersTab?: boolean;
|
||||||
areMembersHidden?: boolean;
|
areMembersHidden?: boolean;
|
||||||
|
canAddMembers?: boolean;
|
||||||
|
canDeleteMembers?: boolean;
|
||||||
members?: ApiChatMember[];
|
members?: ApiChatMember[];
|
||||||
usersById?: Record<number, ApiUser>;
|
usersById?: Record<number, ApiUser>;
|
||||||
isRightColumnShown: boolean;
|
isRightColumnShown: boolean;
|
||||||
@ -83,7 +87,7 @@ type StateProps = {
|
|||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
|
'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
|
||||||
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos'
|
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos' | 'setNewChatMembersDialogState'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
const TABS = [
|
const TABS = [
|
||||||
@ -102,11 +106,14 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
theme,
|
theme,
|
||||||
isChannel,
|
isChannel,
|
||||||
resolvedUserId,
|
resolvedUserId,
|
||||||
|
currentUserId,
|
||||||
chatMessages,
|
chatMessages,
|
||||||
foundIds,
|
foundIds,
|
||||||
mediaSearchType,
|
mediaSearchType,
|
||||||
hasMembersTab,
|
hasMembersTab,
|
||||||
areMembersHidden,
|
areMembersHidden,
|
||||||
|
canAddMembers,
|
||||||
|
canDeleteMembers,
|
||||||
members,
|
members,
|
||||||
usersById,
|
usersById,
|
||||||
isRightColumnShown,
|
isRightColumnShown,
|
||||||
@ -120,6 +127,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
openUserInfo,
|
openUserInfo,
|
||||||
focusMessage,
|
focusMessage,
|
||||||
loadProfilePhotos,
|
loadProfilePhotos,
|
||||||
|
setNewChatMembersDialogState,
|
||||||
serverTimeOffset,
|
serverTimeOffset,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -128,6 +136,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const transitionRef = useRef<HTMLDivElement>(null);
|
const transitionRef = useRef<HTMLDivElement>(null);
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [activeTab, setActiveTab] = useState(0);
|
const [activeTab, setActiveTab] = useState(0);
|
||||||
|
const [deletingUserId, setDeletingUserId] = useState<number | undefined>();
|
||||||
|
|
||||||
const tabs = useMemo(() => ([
|
const tabs = useMemo(() => ([
|
||||||
...(hasMembersTab ? [{
|
...(hasMembersTab ? [{
|
||||||
@ -154,6 +163,10 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
resetCacheBuster();
|
resetCacheBuster();
|
||||||
}, [releaseTransitionFix, resetCacheBuster]);
|
}, [releaseTransitionFix, resetCacheBuster]);
|
||||||
|
|
||||||
|
const handleNewMemberDialogOpen = useCallback(() => {
|
||||||
|
setNewChatMembersDialogState(NewChatMembersProgress.InProgress);
|
||||||
|
}, [setNewChatMembersDialogState]);
|
||||||
|
|
||||||
// Update search type when switching tabs
|
// Update search type when switching tabs
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalMediaSearchType({ mediaType: tabType });
|
setLocalMediaSearchType({ mediaType: tabType });
|
||||||
@ -188,6 +201,10 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
focusMessage({ chatId: profileId, messageId });
|
focusMessage({ chatId: profileId, messageId });
|
||||||
}, [profileId, focusMessage]);
|
}, [profileId, focusMessage]);
|
||||||
|
|
||||||
|
const handleDeleteMembersModalClose = useCallback(() => {
|
||||||
|
setDeletingUserId(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!transitionRef.current || !IS_TOUCH_ENV) {
|
if (!transitionRef.current || !IS_TOUCH_ENV) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -215,9 +232,19 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
const canRenderContents = useAsyncRendering([chatId, resultType], renderingDelay);
|
const canRenderContents = useAsyncRendering([chatId, resultType], renderingDelay);
|
||||||
|
|
||||||
|
function getMemberContextAction(id: number) {
|
||||||
|
return id === currentUserId || !canDeleteMembers ? undefined : [{
|
||||||
|
title: lang('lng_context_remove_from_group'),
|
||||||
|
icon: 'stop',
|
||||||
|
handler: () => {
|
||||||
|
setDeletingUserId(id);
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
function renderSharedMedia() {
|
function renderSharedMedia() {
|
||||||
if (!viewportIds || !canRenderContents || !chatMessages) {
|
if (!viewportIds || !canRenderContents || !chatMessages) {
|
||||||
// This is just a single-frame delay so we do not show spinner
|
// This is just a single-frame delay, so we do not show spinner
|
||||||
const noSpinner = isFirstTab && viewportIds && !canRenderContents;
|
const noSpinner = isFirstTab && viewportIds && !canRenderContents;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -308,6 +335,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
teactOrderKey={i}
|
teactOrderKey={i}
|
||||||
className="chat-item-clickable scroll-item"
|
className="chat-item-clickable scroll-item"
|
||||||
onClick={() => handleMemberClick(id)}
|
onClick={() => handleMemberClick(id)}
|
||||||
|
contextActions={getMemberContextAction(id)}
|
||||||
>
|
>
|
||||||
<PrivateChatInfo userId={id} forceShowSelf />
|
<PrivateChatInfo userId={id} forceShowSelf />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
@ -334,7 +362,9 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
>
|
>
|
||||||
{!noProfileInfo && renderProfileInfo(chatId, resolvedUserId)}
|
{!noProfileInfo && renderProfileInfo(chatId, resolvedUserId)}
|
||||||
{!isRestricted && (
|
{!isRestricted && (
|
||||||
<div className="shared-media">
|
<div
|
||||||
|
className="shared-media"
|
||||||
|
>
|
||||||
<Transition
|
<Transition
|
||||||
ref={transitionRef}
|
ref={transitionRef}
|
||||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||||
@ -348,8 +378,26 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
{renderSharedMedia}
|
{renderSharedMedia}
|
||||||
</Transition>
|
</Transition>
|
||||||
<TabList big activeTab={activeTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
<TabList big activeTab={activeTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{canAddMembers && (
|
||||||
|
<FloatingActionButton
|
||||||
|
isShown={resultType === 'members'}
|
||||||
|
onClick={handleNewMemberDialogOpen}
|
||||||
|
ariaLabel={lang('lng_channel_add_users')}
|
||||||
|
>
|
||||||
|
<i className="icon-add-user-filled" />
|
||||||
|
</FloatingActionButton>
|
||||||
|
)}
|
||||||
|
{canDeleteMembers && (
|
||||||
|
<DeleteMemberModal
|
||||||
|
isOpen={Boolean(deletingUserId)}
|
||||||
|
userId={deletingUserId}
|
||||||
|
onClose={handleDeleteMembersModalClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -390,6 +438,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!));
|
const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!));
|
||||||
const members = chat && chat.fullInfo && chat.fullInfo.members;
|
const members = chat && chat.fullInfo && chat.fullInfo.members;
|
||||||
const areMembersHidden = hasMembersTab && chat && chat.fullInfo && !chat.fullInfo.canViewMembers;
|
const areMembersHidden = hasMembersTab && chat && chat.fullInfo && !chat.fullInfo.canViewMembers;
|
||||||
|
const canAddMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'inviteUsers') || chat.isCreator);
|
||||||
|
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
||||||
|
|
||||||
let resolvedUserId;
|
let resolvedUserId;
|
||||||
if (userId) {
|
if (userId) {
|
||||||
@ -407,10 +457,13 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
mediaSearchType,
|
mediaSearchType,
|
||||||
hasMembersTab,
|
hasMembersTab,
|
||||||
areMembersHidden,
|
areMembersHidden,
|
||||||
|
canAddMembers,
|
||||||
|
canDeleteMembers,
|
||||||
...(hasMembersTab && members && {
|
...(hasMembersTab && members && {
|
||||||
members,
|
members,
|
||||||
usersById,
|
usersById,
|
||||||
}),
|
}),
|
||||||
|
currentUserId: global.currentUserId,
|
||||||
isRightColumnShown: selectIsRightColumnShown(global),
|
isRightColumnShown: selectIsRightColumnShown(global),
|
||||||
isRestricted: chat && chat.isRestricted,
|
isRestricted: chat && chat.isRestricted,
|
||||||
lastSyncTime: global.lastSyncTime,
|
lastSyncTime: global.lastSyncTime,
|
||||||
@ -426,5 +479,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
'openUserInfo',
|
'openUserInfo',
|
||||||
'focusMessage',
|
'focusMessage',
|
||||||
'loadProfilePhotos',
|
'loadProfilePhotos',
|
||||||
|
'setNewChatMembersDialogState',
|
||||||
]),
|
]),
|
||||||
)(Profile));
|
)(Profile));
|
||||||
|
|||||||
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
// @optimization
|
// @optimization
|
||||||
&:not(:hover) {
|
&:not(:hover) {
|
||||||
.chat-item-clickable:nth-child(n + 18) {
|
.chat-item-clickable:not(.picker-list-item):nth-child(n + 18) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ import React, {
|
|||||||
import { withGlobal } from '../../lib/teact/teactn';
|
import { withGlobal } from '../../lib/teact/teactn';
|
||||||
|
|
||||||
import { GlobalActions } from '../../global/types';
|
import { GlobalActions } from '../../global/types';
|
||||||
import { ManagementScreens, ProfileState, RightColumnContent } from '../../types';
|
import {
|
||||||
|
ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
import { MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
|
import { MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||||
@ -27,6 +29,7 @@ import Management from './management/Management.async';
|
|||||||
import StickerSearch from './StickerSearch.async';
|
import StickerSearch from './StickerSearch.async';
|
||||||
import GifSearch from './GifSearch.async';
|
import GifSearch from './GifSearch.async';
|
||||||
import PollResults from './PollResults.async';
|
import PollResults from './PollResults.async';
|
||||||
|
import AddChatMembers from './AddChatMembers';
|
||||||
|
|
||||||
import './RightColumn.scss';
|
import './RightColumn.scss';
|
||||||
|
|
||||||
@ -40,8 +43,8 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DispatchProps = Pick<GlobalActions, (
|
type DispatchProps = Pick<GlobalActions, (
|
||||||
'toggleChatInfo' | 'toggleManagement' | 'openUserInfo' |
|
'toggleChatInfo' | 'toggleManagement' | 'openUserInfo' | 'setNewChatMembersDialogState' |
|
||||||
'closeLocalTextSearch' | 'closePollResults' |
|
'closeLocalTextSearch' | 'closePollResults' | 'addChatMembers' |
|
||||||
'setStickerSearchQuery' | 'setGifSearchQuery'
|
'setStickerSearchQuery' | 'setGifSearchQuery'
|
||||||
)>;
|
)>;
|
||||||
|
|
||||||
@ -69,6 +72,8 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
setStickerSearchQuery,
|
setStickerSearchQuery,
|
||||||
setGifSearchQuery,
|
setGifSearchQuery,
|
||||||
closePollResults,
|
closePollResults,
|
||||||
|
addChatMembers,
|
||||||
|
setNewChatMembersDialogState,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
}) => {
|
}) => {
|
||||||
const { width: windowWidth } = useWindowSize();
|
const { width: windowWidth } = useWindowSize();
|
||||||
@ -85,6 +90,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||||
const isPollResults = contentKey === RightColumnContent.PollResults;
|
const isPollResults = contentKey === RightColumnContent.PollResults;
|
||||||
|
const isAddingChatMembers = contentKey === RightColumnContent.AddingMembers;
|
||||||
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
|
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
|
||||||
|
|
||||||
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
|
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
|
||||||
@ -93,6 +99,9 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
const close = useCallback((shouldScrollUp = true) => {
|
const close = useCallback((shouldScrollUp = true) => {
|
||||||
switch (contentKey) {
|
switch (contentKey) {
|
||||||
|
case RightColumnContent.AddingMembers:
|
||||||
|
setNewChatMembersDialogState(NewChatMembersProgress.Closed);
|
||||||
|
break;
|
||||||
case RightColumnContent.ChatInfo:
|
case RightColumnContent.ChatInfo:
|
||||||
if (isScrolledDown && shouldScrollUp) {
|
if (isScrolledDown && shouldScrollUp) {
|
||||||
setProfileState(ProfileState.Profile);
|
setProfileState(ProfileState.Profile);
|
||||||
@ -155,7 +164,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults,
|
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults, setNewChatMembersDialogState,
|
||||||
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -164,6 +173,10 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
setIsPromotedByCurrentUser(isPromoted);
|
setIsPromotedByCurrentUser(isPromoted);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleAppendingChatMembers = useCallback((memberIds: number[]) => {
|
||||||
|
addChatMembers({ chatId, memberIds });
|
||||||
|
}, [addChatMembers, chatId]);
|
||||||
|
|
||||||
useEffect(() => (isOpen ? captureEscKeyListener(close) : undefined), [isOpen, close]);
|
useEffect(() => (isOpen ? captureEscKeyListener(close) : undefined), [isOpen, close]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -194,7 +207,8 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
|
|
||||||
|
|
||||||
useHistoryBack(isChatSelected && (contentKey === RightColumnContent.ChatInfo
|
useHistoryBack(isChatSelected && (contentKey === RightColumnContent.ChatInfo
|
||||||
|| contentKey === RightColumnContent.UserInfo || contentKey === RightColumnContent.Management),
|
|| contentKey === RightColumnContent.UserInfo || contentKey === RightColumnContent.Management
|
||||||
|
|| contentKey === RightColumnContent.AddingMembers),
|
||||||
() => close(false), toggleChatInfo);
|
() => close(false), toggleChatInfo);
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
// eslint-disable-next-line consistent-return
|
||||||
@ -204,6 +218,15 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (renderingContentKey) {
|
switch (renderingContentKey) {
|
||||||
|
case RightColumnContent.AddingMembers:
|
||||||
|
return (
|
||||||
|
<AddChatMembers
|
||||||
|
chatId={chatId!}
|
||||||
|
onNextStep={handleAppendingChatMembers}
|
||||||
|
isActive={isOpen && isActive}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case RightColumnContent.ChatInfo:
|
case RightColumnContent.ChatInfo:
|
||||||
case RightColumnContent.UserInfo:
|
case RightColumnContent.UserInfo:
|
||||||
return (
|
return (
|
||||||
@ -258,6 +281,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
|||||||
isStickerSearch={isStickerSearch}
|
isStickerSearch={isStickerSearch}
|
||||||
isGifSearch={isGifSearch}
|
isGifSearch={isGifSearch}
|
||||||
isPollResults={isPollResults}
|
isPollResults={isPollResults}
|
||||||
|
isAddingChatMembers={isAddingChatMembers}
|
||||||
profileState={profileState}
|
profileState={profileState}
|
||||||
managementScreen={managementScreen}
|
managementScreen={managementScreen}
|
||||||
onClose={close}
|
onClose={close}
|
||||||
@ -299,5 +323,7 @@ export default memo(withGlobal(
|
|||||||
'setStickerSearchQuery',
|
'setStickerSearchQuery',
|
||||||
'setGifSearchQuery',
|
'setGifSearchQuery',
|
||||||
'closePollResults',
|
'closePollResults',
|
||||||
|
'addChatMembers',
|
||||||
|
'setNewChatMembersDialogState',
|
||||||
]),
|
]),
|
||||||
)(RightColumn));
|
)(RightColumn));
|
||||||
|
|||||||
@ -36,6 +36,7 @@ type OwnProps = {
|
|||||||
isStickerSearch?: boolean;
|
isStickerSearch?: boolean;
|
||||||
isGifSearch?: boolean;
|
isGifSearch?: boolean;
|
||||||
isPollResults?: boolean;
|
isPollResults?: boolean;
|
||||||
|
isAddingChatMembers?: boolean;
|
||||||
shouldSkipAnimation?: boolean;
|
shouldSkipAnimation?: boolean;
|
||||||
profileState?: ProfileState;
|
profileState?: ProfileState;
|
||||||
managementScreen?: ManagementScreens;
|
managementScreen?: ManagementScreens;
|
||||||
@ -79,6 +80,7 @@ enum HeaderContent {
|
|||||||
StickerSearch,
|
StickerSearch,
|
||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
|
AddingMembers,
|
||||||
}
|
}
|
||||||
|
|
||||||
const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||||
@ -89,6 +91,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
isStickerSearch,
|
isStickerSearch,
|
||||||
isGifSearch,
|
isGifSearch,
|
||||||
isPollResults,
|
isPollResults,
|
||||||
|
isAddingChatMembers,
|
||||||
profileState,
|
profileState,
|
||||||
managementScreen,
|
managementScreen,
|
||||||
canManage,
|
canManage,
|
||||||
@ -149,6 +152,8 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
HeaderContent.StickerSearch
|
HeaderContent.StickerSearch
|
||||||
) : isGifSearch ? (
|
) : isGifSearch ? (
|
||||||
HeaderContent.GifSearch
|
HeaderContent.GifSearch
|
||||||
|
) : isAddingChatMembers ? (
|
||||||
|
HeaderContent.AddingMembers
|
||||||
) : isManagement ? (
|
) : isManagement ? (
|
||||||
managementScreen === ManagementScreens.Initial ? (
|
managementScreen === ManagementScreens.Initial ? (
|
||||||
HeaderContent.ManageInitial
|
HeaderContent.ManageInitial
|
||||||
@ -206,6 +211,8 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
case HeaderContent.AddingMembers:
|
||||||
|
return <h3>{lang('GroupAddMembers')}</h3>;
|
||||||
case HeaderContent.ManageInitial:
|
case HeaderContent.ManageInitial:
|
||||||
return <h3>{lang('Edit')}</h3>;
|
return <h3>{lang('Edit')}</h3>;
|
||||||
case HeaderContent.ManageChatPrivacyType:
|
case HeaderContent.ManageChatPrivacyType:
|
||||||
@ -275,6 +282,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
IS_SINGLE_COLUMN_LAYOUT
|
IS_SINGLE_COLUMN_LAYOUT
|
||||||
|| contentKey === HeaderContent.SharedMedia
|
|| contentKey === HeaderContent.SharedMedia
|
||||||
|| contentKey === HeaderContent.MemberList
|
|| contentKey === HeaderContent.MemberList
|
||||||
|
|| contentKey === HeaderContent.AddingMembers
|
||||||
|| isManagement
|
|| isManagement
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -320,14 +320,15 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const chat = selectChat(global, chatId)!;
|
const chat = selectChat(global, chatId)!;
|
||||||
const { progress } = global.management;
|
const { progress } = global.management;
|
||||||
const hasLinkedChannel = Boolean(chat.fullInfo && chat.fullInfo.linkedChatId);
|
const hasLinkedChannel = Boolean(chat.fullInfo && chat.fullInfo.linkedChatId);
|
||||||
|
const isBasicGroup = isChatBasicGroup(chat);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chat,
|
chat,
|
||||||
progress,
|
progress,
|
||||||
isBasicGroup: isChatBasicGroup(chat),
|
isBasicGroup,
|
||||||
hasLinkedChannel,
|
hasLinkedChannel,
|
||||||
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
||||||
canBanUsers: getHasAdminRight(chat, 'banUsers'),
|
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||||
|
|||||||
@ -147,5 +147,6 @@
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { GlobalState } from './types';
|
import { GlobalState } from './types';
|
||||||
|
import { NewChatMembersProgress } from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR,
|
ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR,
|
||||||
@ -7,6 +8,7 @@ import {
|
|||||||
export const INITIAL_STATE: GlobalState = {
|
export const INITIAL_STATE: GlobalState = {
|
||||||
isLeftColumnShown: true,
|
isLeftColumnShown: true,
|
||||||
isChatInfoShown: false,
|
isChatInfoShown: false,
|
||||||
|
newChatMembersProgress: NewChatMembersProgress.Closed,
|
||||||
uiReadyState: 0,
|
uiReadyState: 0,
|
||||||
serverTimeOffset: 0,
|
serverTimeOffset: 0,
|
||||||
|
|
||||||
@ -73,6 +75,8 @@ export const INITIAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
globalSearch: {},
|
globalSearch: {},
|
||||||
|
|
||||||
|
userSearch: {},
|
||||||
|
|
||||||
localTextSearch: {
|
localTextSearch: {
|
||||||
byChatThreadKey: {},
|
byChatThreadKey: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import {
|
|||||||
NotifyException,
|
NotifyException,
|
||||||
LangCode,
|
LangCode,
|
||||||
EmojiKeywords,
|
EmojiKeywords,
|
||||||
|
NewChatMembersProgress,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type MessageListType = 'thread' | 'pinned' | 'scheduled';
|
export type MessageListType = 'thread' | 'pinned' | 'scheduled';
|
||||||
@ -65,6 +66,7 @@ export type GlobalState = {
|
|||||||
isChatInfoShown: boolean;
|
isChatInfoShown: boolean;
|
||||||
isLeftColumnShown: boolean;
|
isLeftColumnShown: boolean;
|
||||||
isPollModalOpen?: boolean;
|
isPollModalOpen?: boolean;
|
||||||
|
newChatMembersProgress?: NewChatMembersProgress;
|
||||||
uiReadyState: 0 | 1 | 2;
|
uiReadyState: 0 | 1 | 2;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
connectionState?: ApiUpdateConnectionStateType;
|
connectionState?: ApiUpdateConnectionStateType;
|
||||||
@ -248,6 +250,13 @@ export type GlobalState = {
|
|||||||
}>>;
|
}>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userSearch: {
|
||||||
|
query?: string;
|
||||||
|
fetchingStatus?: boolean;
|
||||||
|
localUserIds?: number[];
|
||||||
|
globalUserIds?: number[];
|
||||||
|
};
|
||||||
|
|
||||||
localTextSearch: {
|
localTextSearch: {
|
||||||
byChatThreadKey: Record<string, {
|
byChatThreadKey: Record<string, {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
@ -405,8 +414,8 @@ export type ActionTypes = (
|
|||||||
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
||||||
// ui
|
// ui
|
||||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
||||||
'toggleSafeLinkModal' | 'disableHistoryAnimations' | 'openHistoryCalendar' | 'closeHistoryCalendar' |
|
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||||
'disableContextMenuHint' |
|
'setNewChatMembersDialogState' | 'disableHistoryAnimations' |
|
||||||
// auth
|
// auth
|
||||||
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
||||||
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
||||||
@ -418,6 +427,7 @@ export type ActionTypes = (
|
|||||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
||||||
|
'addChatMembers' | 'deleteChatMember' |
|
||||||
// messages
|
// messages
|
||||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||||
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
||||||
@ -446,7 +456,7 @@ export type ActionTypes = (
|
|||||||
'acceptInviteConfirmation' |
|
'acceptInviteConfirmation' |
|
||||||
// users
|
// users
|
||||||
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadCurrentUser' |
|
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadCurrentUser' |
|
||||||
'updateProfile' | 'checkUsername' | 'updateContact' | 'deleteUser' | 'loadUser' |
|
'updateProfile' | 'checkUsername' | 'updateContact' | 'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
|
||||||
// Channel / groups creation
|
// Channel / groups creation
|
||||||
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
|
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
|
||||||
// settings
|
// settings
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { useState, useEffect } from '../lib/teact/teact';
|
import { useState, useEffect } from '../lib/teact/teact';
|
||||||
import { IAnchorPosition } from '../types';
|
import { IAnchorPosition } from '../types';
|
||||||
|
|
||||||
|
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
|
||||||
|
|
||||||
export default (
|
export default (
|
||||||
anchor: IAnchorPosition | undefined,
|
anchor: IAnchorPosition | undefined,
|
||||||
getTriggerElement: () => HTMLElement | null,
|
getTriggerElement: () => HTMLElement | null,
|
||||||
@ -31,16 +33,18 @@ export default (
|
|||||||
const menuRect = menuEl ? { width: menuEl.offsetWidth, height: menuEl.offsetHeight } : emptyRect;
|
const menuRect = menuEl ? { width: menuEl.offsetWidth, height: menuEl.offsetHeight } : emptyRect;
|
||||||
const rootRect = rootEl ? rootEl.getBoundingClientRect() : emptyRect;
|
const rootRect = rootEl ? rootEl.getBoundingClientRect() : emptyRect;
|
||||||
|
|
||||||
|
let horizontalPostition: 'left' | 'right';
|
||||||
if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) {
|
if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) {
|
||||||
setPositionX('left');
|
|
||||||
x += 3;
|
x += 3;
|
||||||
|
horizontalPostition = 'left';
|
||||||
} else if (x - menuRect.width > 0) {
|
} else if (x - menuRect.width > 0) {
|
||||||
setPositionX('right');
|
horizontalPostition = 'right';
|
||||||
x -= 3;
|
x -= 3;
|
||||||
} else {
|
} else {
|
||||||
setPositionX('left');
|
horizontalPostition = 'left';
|
||||||
x = 16;
|
x = 16;
|
||||||
}
|
}
|
||||||
|
setPositionX(horizontalPostition);
|
||||||
|
|
||||||
if (y + menuRect.height < rootRect.height + rootRect.top) {
|
if (y + menuRect.height < rootRect.height + rootRect.top) {
|
||||||
setPositionY('top');
|
setPositionY('top');
|
||||||
@ -52,7 +56,11 @@ export default (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStyle(`left: ${x - triggerRect.left}px; top: ${y - triggerRect.top}px;`);
|
const left = horizontalPostition === 'left'
|
||||||
|
? Math.min(x - triggerRect.left, rootRect.width - menuRect.width - MENU_POSITION_VISUAL_COMFORT_SPACE_PX)
|
||||||
|
: Math.max((x - triggerRect.left), menuRect.width + MENU_POSITION_VISUAL_COMFORT_SPACE_PX);
|
||||||
|
|
||||||
|
setStyle(`left: ${left}px; top: ${y - triggerRect.top}px;`);
|
||||||
}, [
|
}, [
|
||||||
anchor, extraPaddingX, extraTopPadding,
|
anchor, extraPaddingX, extraTopPadding,
|
||||||
getMenuElement, getRootElement, getTriggerElement,
|
getMenuElement, getRootElement, getTriggerElement,
|
||||||
|
|||||||
@ -987,6 +987,7 @@ messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
|
|||||||
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||||
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
||||||
messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
||||||
|
messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates;
|
||||||
messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates;
|
messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates;
|
||||||
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||||
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
||||||
|
|||||||
@ -987,6 +987,7 @@ messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
|
|||||||
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||||
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
messages.editChatTitle#dc452855 chat_id:int title:string = Updates;
|
||||||
messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
messages.editChatPhoto#ca4c79d8 chat_id:int photo:InputChatPhoto = Updates;
|
||||||
|
messages.addChatUser#f9a0aa09 chat_id:int user_id:InputUser fwd_limit:int = Updates;
|
||||||
messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates;
|
messages.deleteChatUser#c534459a flags:# revoke_history:flags.0?true chat_id:int user_id:InputUser = Updates;
|
||||||
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
messages.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||||
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
ApiChat, ApiUser, ApiChatFolder, MAIN_THREAD_ID,
|
ApiChat, ApiUser, ApiChatFolder, MAIN_THREAD_ID,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import { ChatCreationProgress, ManagementProgress } from '../../../types';
|
import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types';
|
||||||
import { GlobalActions } from '../../../global/types';
|
import { GlobalActions } from '../../../global/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -774,6 +774,38 @@ addReducer('loadMoreMembers', (global) => {
|
|||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('addChatMembers', (global, actions, payload) => {
|
||||||
|
const { chatId, memberIds } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const users = (memberIds as number[]).map((userId) => selectUser(global, userId)).filter<ApiUser>(Boolean as any);
|
||||||
|
|
||||||
|
if (!chat || !users.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.setNewChatMembersDialogState(NewChatMembersProgress.Loading);
|
||||||
|
(async () => {
|
||||||
|
await callApi('addChatMembers', chat, users);
|
||||||
|
actions.setNewChatMembersDialogState(NewChatMembersProgress.Closed);
|
||||||
|
loadFullChat(chat);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
addReducer('deleteChatMember', (global, actions, payload) => {
|
||||||
|
const { chatId, userId } = payload;
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const user = selectUser(global, userId);
|
||||||
|
|
||||||
|
if (!chat || !user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
await callApi('deleteChatMember', chat, user);
|
||||||
|
loadFullChat(chat);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) {
|
async function loadChats(listType: 'active' | 'archived', offsetId?: number, offsetDate?: number) {
|
||||||
const result = await callApi('fetchChats', {
|
const result = await callApi('fetchChats', {
|
||||||
limit: CHAT_LIST_LOAD_SLICE,
|
limit: CHAT_LIST_LOAD_SLICE,
|
||||||
|
|||||||
@ -5,17 +5,19 @@ import {
|
|||||||
import { ApiUser } from '../../../api/types';
|
import { ApiUser } from '../../../api/types';
|
||||||
import { ManagementProgress } from '../../../types';
|
import { ManagementProgress } from '../../../types';
|
||||||
|
|
||||||
import { debounce } from '../../../util/schedulers';
|
import { debounce, throttle } from '../../../util/schedulers';
|
||||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||||
import { isChatPrivate } from '../../helpers';
|
import { isChatPrivate } from '../../helpers';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { selectChat, selectUser } from '../../selectors';
|
import { selectChat, selectUser } from '../../selectors';
|
||||||
import {
|
import {
|
||||||
addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers,
|
addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers,
|
||||||
|
updateUserSearch, updateUserSearchFetchingStatus,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
|
|
||||||
const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true);
|
const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true);
|
||||||
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
||||||
|
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
||||||
|
|
||||||
addReducer('loadFullUser', (global, actions, payload) => {
|
addReducer('loadFullUser', (global, actions, payload) => {
|
||||||
const { userId } = payload!;
|
const { userId } = payload!;
|
||||||
@ -200,3 +202,44 @@ addReducer('loadProfilePhotos', (global, actions, payload) => {
|
|||||||
setGlobal(newGlobal);
|
setGlobal(newGlobal);
|
||||||
})();
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
addReducer('setUserSearchQuery', (global, actions, payload) => {
|
||||||
|
const { query } = payload!;
|
||||||
|
|
||||||
|
if (!query) return;
|
||||||
|
|
||||||
|
void runThrottledForSearch(() => {
|
||||||
|
searchUsers(query);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function searchUsers(query: string) {
|
||||||
|
const result = await callApi('searchChats', { query });
|
||||||
|
|
||||||
|
let global = getGlobal();
|
||||||
|
const currentSearchQuery = global.userSearch.query;
|
||||||
|
|
||||||
|
if (!result || !currentSearchQuery || (query !== currentSearchQuery)) {
|
||||||
|
setGlobal(updateUserSearchFetchingStatus(global, false));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { localUsers, globalUsers } = result;
|
||||||
|
|
||||||
|
let localUserIds;
|
||||||
|
let globalUserIds;
|
||||||
|
if (localUsers.length) {
|
||||||
|
global = addUsers(global, buildCollectionByKey(localUsers, 'id'));
|
||||||
|
localUserIds = localUsers.map(({ id }) => id);
|
||||||
|
}
|
||||||
|
if (globalUsers.length) {
|
||||||
|
global = addUsers(global, buildCollectionByKey(globalUsers, 'id'));
|
||||||
|
globalUserIds = globalUsers.map(({ id }) => id);
|
||||||
|
}
|
||||||
|
|
||||||
|
global = updateUserSearchFetchingStatus(global, false);
|
||||||
|
global = updateUserSearch(global, { localUserIds, globalUserIds });
|
||||||
|
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { addReducer, setGlobal } from '../../../lib/teact/teactn';
|
import { addReducer, setGlobal } from '../../../lib/teact/teactn';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
exitMessageSelectMode, replaceThreadParam, updateCurrentMessageList,
|
exitMessageSelectMode, replaceThreadParam, updateCurrentMessageList,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
@ -55,6 +56,13 @@ addReducer('resetChatCreation', (global) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('setNewChatMembersDialogState', (global, actions, payload) => {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
newChatMembersProgress: payload,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
addReducer('openNextChat', (global, actions, payload) => {
|
addReducer('openNextChat', (global, actions, payload) => {
|
||||||
const { targetIndexDelta, orderedIds } = payload;
|
const { targetIndexDelta, orderedIds } = payload;
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { addReducer } from '../../../lib/teact/teactn';
|
|||||||
|
|
||||||
import { GlobalState } from '../../../global/types';
|
import { GlobalState } from '../../../global/types';
|
||||||
|
|
||||||
import { updateSelectedUserId } from '../../reducers';
|
import { updateSelectedUserId, updateUserSearch } from '../../reducers';
|
||||||
|
|
||||||
addReducer('openUserInfo', (global, actions, payload) => {
|
addReducer('openUserInfo', (global, actions, payload) => {
|
||||||
const { id } = payload!;
|
const { id } = payload!;
|
||||||
@ -13,3 +13,14 @@ addReducer('openUserInfo', (global, actions, payload) => {
|
|||||||
const clearSelectedUserId = (global: GlobalState) => updateSelectedUserId(global, undefined);
|
const clearSelectedUserId = (global: GlobalState) => updateSelectedUserId(global, undefined);
|
||||||
|
|
||||||
addReducer('openChat', clearSelectedUserId);
|
addReducer('openChat', clearSelectedUserId);
|
||||||
|
|
||||||
|
addReducer('setUserSearchQuery', (global, actions, payload) => {
|
||||||
|
const { query } = payload!;
|
||||||
|
|
||||||
|
return updateUserSearch(global, {
|
||||||
|
globalUserIds: undefined,
|
||||||
|
localUserIds: undefined,
|
||||||
|
fetchingStatus: Boolean(query),
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -148,3 +148,24 @@ export function deleteUser(global: GlobalState, userId: number): GlobalState {
|
|||||||
|
|
||||||
return replaceUsers(global, byId);
|
return replaceUsers(global, byId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateUserSearch(
|
||||||
|
global: GlobalState,
|
||||||
|
searchStatePartial: Partial<GlobalState['userSearch']>,
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
userSearch: {
|
||||||
|
...global.userSearch,
|
||||||
|
...searchStatePartial,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateUserSearchFetchingStatus(
|
||||||
|
global: GlobalState, newState: boolean,
|
||||||
|
) {
|
||||||
|
return updateUserSearch(global, {
|
||||||
|
fetchingStatus: newState,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { GlobalState } from '../../global/types';
|
import { GlobalState } from '../../global/types';
|
||||||
import { RightColumnContent } from '../../types';
|
import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
||||||
|
|
||||||
import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||||
import { selectCurrentMessageList, selectIsPollResultsOpen } from './messages';
|
import { selectCurrentMessageList, selectIsPollResultsOpen } from './messages';
|
||||||
@ -17,8 +17,10 @@ export function selectRightColumnContentKey(global: GlobalState) {
|
|||||||
const {
|
const {
|
||||||
users,
|
users,
|
||||||
isChatInfoShown,
|
isChatInfoShown,
|
||||||
|
newChatMembersProgress,
|
||||||
} = global;
|
} = global;
|
||||||
|
|
||||||
|
const isAddingChatMembersShown = newChatMembersProgress !== NewChatMembersProgress.Closed;
|
||||||
const isPollResults = selectIsPollResultsOpen(global);
|
const isPollResults = selectIsPollResultsOpen(global);
|
||||||
const isSearch = Boolean(!IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global));
|
const isSearch = Boolean(!IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global));
|
||||||
const isManagement = selectCurrentManagement(global);
|
const isManagement = selectCurrentManagement(global);
|
||||||
@ -43,6 +45,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
|
|||||||
RightColumnContent.StickerSearch
|
RightColumnContent.StickerSearch
|
||||||
) : isGifSearch ? (
|
) : isGifSearch ? (
|
||||||
RightColumnContent.GifSearch
|
RightColumnContent.GifSearch
|
||||||
|
) : isAddingChatMembersShown ? (
|
||||||
|
RightColumnContent.AddingMembers
|
||||||
) : isUserInfo ? (
|
) : isUserInfo ? (
|
||||||
RightColumnContent.UserInfo
|
RightColumnContent.UserInfo
|
||||||
) : isChatInfo ? (
|
) : isChatInfo ? (
|
||||||
|
|||||||
@ -215,6 +215,7 @@ export enum RightColumnContent {
|
|||||||
StickerSearch,
|
StickerSearch,
|
||||||
GifSearch,
|
GifSearch,
|
||||||
PollResults,
|
PollResults,
|
||||||
|
AddingMembers,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaViewerOrigin {
|
export enum MediaViewerOrigin {
|
||||||
@ -249,6 +250,12 @@ export enum ManagementProgress {
|
|||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum NewChatMembersProgress {
|
||||||
|
Closed,
|
||||||
|
InProgress,
|
||||||
|
Loading,
|
||||||
|
}
|
||||||
|
|
||||||
export type ProfileTabType = 'members' | 'media' | 'documents' | 'links' | 'audio';
|
export type ProfileTabType = 'members' | 'media' | 'documents' | 'links' | 'audio';
|
||||||
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio';
|
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio';
|
||||||
export type ApiPrivacyKey = 'phoneNumber' | 'lastSeen' | 'profilePhoto' | 'forwards' | 'chatInvite';
|
export type ApiPrivacyKey = 'phoneNumber' | 'lastSeen' | 'profilePhoto' | 'forwards' | 'chatInvite';
|
||||||
|
|||||||
@ -14,6 +14,7 @@ interface LangFn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FALLBACK_LANG_CODE = 'en';
|
const FALLBACK_LANG_CODE = 'en';
|
||||||
|
const SUBSTITUTION_REGEX = /%\d?\$?[sdf@]/g;
|
||||||
const PLURAL_OPTIONS = ['value', 'zeroValue', 'oneValue', 'twoValue', 'fewValue', 'manyValue', 'otherValue'] as const;
|
const PLURAL_OPTIONS = ['value', 'zeroValue', 'oneValue', 'twoValue', 'fewValue', 'manyValue', 'otherValue'] as const;
|
||||||
const PLURAL_RULES = {
|
const PLURAL_RULES = {
|
||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
@ -55,7 +56,8 @@ let currentLangCode: string | undefined;
|
|||||||
|
|
||||||
export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') => {
|
export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') => {
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
const cached = cache.get(`${key}_${value}_${format}`);
|
const cacheValue = Array.isArray(value) ? JSON.stringify(value) : value;
|
||||||
|
const cached = cache.get(`${key}_${cacheValue}_${format}`);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
@ -84,7 +86,8 @@ export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') =
|
|||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
const formattedValue = format === 'i' ? formatInteger(value) : value;
|
const formattedValue = format === 'i' ? formatInteger(value) : value;
|
||||||
const result = processTemplate(template, formattedValue);
|
const result = processTemplate(template, formattedValue);
|
||||||
cache.set(`${key}_${value}_${format}`, result);
|
const cacheValue = Array.isArray(value) ? JSON.stringify(value) : value;
|
||||||
|
cache.set(`${key}_${cacheValue}_${format}`, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,5 +161,11 @@ function getPluralOption(amount: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function processTemplate(template: string, value: any) {
|
function processTemplate(template: string, value: any) {
|
||||||
return template.replace(/%\d?\$?[sdf@]/, String(value));
|
value = Array.isArray(value) ? value : [value];
|
||||||
|
const translationSlices = template.split(SUBSTITUTION_REGEX);
|
||||||
|
const initialValue = translationSlices.shift();
|
||||||
|
|
||||||
|
return translationSlices.reduce((result, str, index) => {
|
||||||
|
return `${result}${String(value[index] || '')}${str}`;
|
||||||
|
}, initialValue || '');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { callApi } from '../api/gramjs';
|
import { callApi } from '../api/gramjs';
|
||||||
import { ApiChat, ApiMessage } from '../api/types';
|
import { ApiChat, ApiMessage, ApiUser } from '../api/types';
|
||||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||||
import { DEBUG } from '../config';
|
import { DEBUG } from '../config';
|
||||||
import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn';
|
import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn';
|
||||||
@ -221,10 +221,13 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||||
: undefined;
|
: undefined;
|
||||||
const {
|
const {
|
||||||
targetUserId: actionTargetUserId,
|
targetUserIds: actionTargetUserIds,
|
||||||
targetChatId: actionTargetChatId,
|
targetChatId: actionTargetChatId,
|
||||||
} = messageAction || {};
|
} = messageAction || {};
|
||||||
const actionTargetUser = actionTargetUserId ? selectUser(global, actionTargetUserId) : undefined;
|
|
||||||
|
const actionTargetUsers = actionTargetUserIds
|
||||||
|
? actionTargetUserIds.map((userId) => selectUser(global, userId)).filter<ApiUser>(Boolean as any)
|
||||||
|
: undefined;
|
||||||
const privateChatUserId = getPrivateChatUserId(chat);
|
const privateChatUserId = getPrivateChatUserId(chat);
|
||||||
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
const privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||||
let body: string;
|
let body: string;
|
||||||
@ -236,7 +239,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
|||||||
getTranslation,
|
getTranslation,
|
||||||
message,
|
message,
|
||||||
actionOrigin,
|
actionOrigin,
|
||||||
actionTargetUser,
|
actionTargetUsers,
|
||||||
actionTargetMessage,
|
actionTargetMessage,
|
||||||
actionTargetChatId,
|
actionTargetChatId,
|
||||||
{ asPlain: true },
|
{ asPlain: true },
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user