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 = '';
|
||||
const translationValues = [];
|
||||
let type: ApiAction['type'] = 'other';
|
||||
let photo: ApiPhoto | undefined;
|
||||
|
||||
const targetUserId = 'users' in action
|
||||
// Api returns array of userIds, but no action currently has multiple users in it
|
||||
? action.users && action.users[0]
|
||||
: ('userId' in action && action.userId) || undefined;
|
||||
const targetUserIds = 'users' in action
|
||||
? action.users && action.users
|
||||
: ('userId' in action && [action.userId]) || [];
|
||||
let targetChatId: number | undefined;
|
||||
|
||||
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) {
|
||||
text = isChannelPost
|
||||
? `Channel renamed to «${action.title}»`
|
||||
: `%action_origin% changed group name to «${action.title}»`;
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessageTitleUpdated';
|
||||
translationValues.push(action.title);
|
||||
} else {
|
||||
text = 'Notification.ChangedGroupName';
|
||||
translationValues.push('%action_origin%', action.title);
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
||||
text = isChannelPost
|
||||
? 'Channel photo updated'
|
||||
: '%action_origin% updated group photo';
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessagePhotoUpdated';
|
||||
} else {
|
||||
text = 'Notification.ChangedGroupPhoto';
|
||||
translationValues.push('%action_origin%');
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
||||
text = isChannelPost
|
||||
? 'Channel photo was deleted'
|
||||
: 'Chat photo was deleted';
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessagePhotoRemoved';
|
||||
} else {
|
||||
text = 'Group.MessagePhotoRemoved';
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatAddUser) {
|
||||
text = !senderId || senderId === targetUserId
|
||||
? '%target_user% joined the group'
|
||||
: '%action_origin% added %target_user% to the group';
|
||||
if (!senderId || targetUserIds.includes(senderId)) {
|
||||
text = 'Notification.JoinedChat';
|
||||
translationValues.push('%target_user%');
|
||||
} else {
|
||||
text = 'Notification.Invited';
|
||||
translationValues.push('%action_origin%', '%target_user%');
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
||||
text = !senderId || senderId === targetUserId
|
||||
? '%target_user% left the group'
|
||||
: '%action_origin% removed %target_user% from the group';
|
||||
if (!senderId || targetUserIds.includes(senderId)) {
|
||||
text = 'Notification.LeftChat';
|
||||
translationValues.push('%target_user%');
|
||||
} else {
|
||||
text = 'Notification.Kicked';
|
||||
translationValues.push('%action_origin%', '%target_user%');
|
||||
}
|
||||
} 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) {
|
||||
text = 'Channel created';
|
||||
text = 'Notification.CreatedChannel';
|
||||
} else if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||
text = 'Migrated to %target_chat%';
|
||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||
translationValues.push('%target_chat%');
|
||||
} else if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
||||
text = 'Migrated from %target_chat%';
|
||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||
text = 'Migrated from %target_chat%';
|
||||
translationValues.push('%target_chat%');
|
||||
} 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) {
|
||||
text = 'Chat history was cleared';
|
||||
text = 'HistoryCleared';
|
||||
type = 'historyClear';
|
||||
} 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) {
|
||||
const mins = Math.max(Math.round(action.duration / 60), 1);
|
||||
text += ` (${mins} min${mins > 1 ? 's' : ''})`;
|
||||
if (withDuration) {
|
||||
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
||||
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionContactSignUp) {
|
||||
text = '%action_origin% joined Telegram';
|
||||
text = 'Notification.Joined';
|
||||
translationValues.push('%action_origin%');
|
||||
} else if (action instanceof GramJs.MessageActionPaymentSent) {
|
||||
const currencySign = getCurrencySign(action.currency);
|
||||
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) {
|
||||
if (action.duration) {
|
||||
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 {
|
||||
text = 'Voice chat started';
|
||||
text = 'Notification.VoiceChatStartedChannel';
|
||||
}
|
||||
} else {
|
||||
text = '%ACTION_NOT_IMPLEMENTED%';
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
|
||||
if ('photo' in action && action.photo instanceof GramJs.Photo) {
|
||||
@ -651,9 +681,10 @@ function buildAction(
|
||||
return {
|
||||
text,
|
||||
type,
|
||||
targetUserId,
|
||||
targetUserIds,
|
||||
targetChatId,
|
||||
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
|
||||
? String(mtpUser.photo.photoId)
|
||||
: undefined;
|
||||
const userType = buildApiUserType(mtpUser);
|
||||
|
||||
return {
|
||||
id,
|
||||
@ -33,8 +34,9 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
...(mtpUser.self && { isSelf: true }),
|
||||
...(mtpUser.verified && { isVerified: true }),
|
||||
...((mtpUser.contact || mtpUser.mutualContact) && { isContact: true }),
|
||||
type: buildApiUserType(mtpUser),
|
||||
type: userType,
|
||||
...(firstName && { firstName }),
|
||||
...(userType === 'userTypeBot' && { canBeInvitedToGroup: !mtpUser.botNochats }),
|
||||
...(lastName && { lastName }),
|
||||
username: mtpUser.username || '',
|
||||
phoneNumber: mtpUser.phone || '',
|
||||
|
||||
@ -172,7 +172,7 @@ export async function searchChats({ query }: { query: string }) {
|
||||
updateLocalDb(result);
|
||||
|
||||
const localPeerIds = result.myResults.map(getApiChatIdFromMtpPeer);
|
||||
const allChats = [...result.chats, ...result.users]
|
||||
const allChats = result.chats.concat(result.users)
|
||||
.map((user) => buildApiChatFromPreview(user))
|
||||
.filter<ApiChat>(Boolean as any);
|
||||
const allUsers = result.users.map(buildApiUser).filter((user) => !!user && !user.isSelf) as ApiUser[];
|
||||
@ -756,15 +756,15 @@ export function updateChatDefaultBannedRights({
|
||||
}
|
||||
|
||||
export function updateChatMemberBannedRights({
|
||||
chat, user, bannedRights,
|
||||
}: { chat: ApiChat; user: ApiUser; bannedRights: ApiChatBannedRights }) {
|
||||
chat, user, bannedRights, untilDate,
|
||||
}: { chat: ApiChat; user: ApiUser; bannedRights: ApiChatBannedRights; untilDate?: number }) {
|
||||
const channel = buildInputEntity(chat.id, chat.accessHash) as GramJs.InputChannel;
|
||||
const participant = buildInputPeer(user.id, user.accessHash) as GramJs.InputUser;
|
||||
|
||||
return invokeRequest(new GramJs.channels.EditBanned({
|
||||
channel,
|
||||
participant,
|
||||
bannedRights: buildChatBannedRights(bannedRights),
|
||||
bannedRights: buildChatBannedRights(bannedRights, untilDate),
|
||||
}), true);
|
||||
}
|
||||
|
||||
@ -957,6 +957,51 @@ export async function openChatByInvite(hash: string) {
|
||||
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(
|
||||
result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs,
|
||||
) {
|
||||
|
||||
@ -14,7 +14,7 @@ export {
|
||||
fetchChatFolders, editChatFolder, deleteChatFolder, fetchRecommendedChatFolders,
|
||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
|
||||
@ -213,6 +213,14 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
if (update._entities && update._entities.some((e): e is GramJs.User => (
|
||||
e instanceof GramJs.User && !!e.self && e.id === action.userId
|
||||
))) {
|
||||
onUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: message.chatId,
|
||||
chat: {
|
||||
isRestricted: true,
|
||||
},
|
||||
});
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateChatLeave',
|
||||
id: message.chatId,
|
||||
|
||||
@ -145,10 +145,11 @@ export type ApiNewPoll = {
|
||||
|
||||
export interface ApiAction {
|
||||
text: string;
|
||||
targetUserId?: number;
|
||||
targetUserIds?: number[];
|
||||
targetChatId?: number;
|
||||
type: 'historyClear' | 'other';
|
||||
photo?: ApiPhoto;
|
||||
translationValues: string[];
|
||||
}
|
||||
|
||||
export interface ApiWebPage {
|
||||
|
||||
@ -15,6 +15,7 @@ export interface ApiUser {
|
||||
accessHash?: string;
|
||||
avatarHash?: string;
|
||||
photos?: ApiPhoto[];
|
||||
canBeInvitedToGroup?: boolean;
|
||||
|
||||
// Obtained from GetFullUser / UserFullInfo
|
||||
fullInfo?: ApiUserFullInfo;
|
||||
|
||||
@ -165,7 +165,7 @@ const DeleteChatModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{renderMessage()}
|
||||
{canDeleteForAll && (
|
||||
<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 color="danger" className="confirm-dialog-button" isText onClick={handleDeleteChat}>
|
||||
|
||||
@ -98,7 +98,7 @@ const DeleteMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
)}
|
||||
{canDeleteForAll && (
|
||||
<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')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -26,6 +26,7 @@ type OwnProps = {
|
||||
notFoundText?: string;
|
||||
searchInputId?: string;
|
||||
isLoading?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
onSelectedIdsChange: (ids: number[]) => void;
|
||||
onFilterChange: (value: string) => void;
|
||||
onLoadMore?: () => void;
|
||||
@ -45,6 +46,7 @@ const Picker: FC<OwnProps> = ({
|
||||
notFoundText,
|
||||
searchInputId,
|
||||
isLoading,
|
||||
noScrollRestore,
|
||||
onSelectedIdsChange,
|
||||
onFilterChange,
|
||||
onLoadMore,
|
||||
@ -107,6 +109,7 @@ const Picker: FC<OwnProps> = ({
|
||||
className="picker-list custom-scroll"
|
||||
items={viewportIds}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={noScrollRestore}
|
||||
>
|
||||
{viewportIds.map((id) => (
|
||||
<ListItem
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
} from '../../modules/helpers';
|
||||
import { pick } from '../../util/iteratees';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import Modal from '../ui/Modal';
|
||||
import Button from '../ui/Button';
|
||||
@ -91,7 +92,7 @@ const PinMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{canPinForAll && (
|
||||
<Button className="confirm-dialog-button" isText onClick={handlePinMessageForAll}>
|
||||
{contactName
|
||||
? lang('Conversation.PinMessagesFor', contactName)
|
||||
? renderText(lang('Conversation.PinMessagesFor', contactName))
|
||||
: lang('Conversation.PinMessageAlert.PinAndNotifyMembers')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -29,7 +29,7 @@ export function renderActionMessageText(
|
||||
lang: LangFn,
|
||||
message: ApiMessage,
|
||||
actionOrigin?: ApiUser | ApiChat,
|
||||
targetUser?: ApiUser,
|
||||
targetUsers?: ApiUser[],
|
||||
targetMessage?: ApiMessage,
|
||||
targetChatId?: number,
|
||||
options: ActionMessageTextOptions = {},
|
||||
@ -37,13 +37,13 @@ export function renderActionMessageText(
|
||||
if (!message.content.action) {
|
||||
return [];
|
||||
}
|
||||
const { text } = message.content.action;
|
||||
const { text, translationValues } = message.content.action;
|
||||
const content: TextPart[] = [];
|
||||
const textOptions: ActionMessageTextOptions = { ...options, maxTextLength: 16 };
|
||||
|
||||
let unprocessed: string;
|
||||
let processed = processPlaceholder(
|
||||
text,
|
||||
lang(text, translationValues && translationValues.length ? translationValues : undefined),
|
||||
'%action_origin%',
|
||||
actionOrigin
|
||||
? (!options.isEmbedded && renderOriginContent(lang, actionOrigin, options.asPlain)) || NBSP
|
||||
@ -56,8 +56,8 @@ export function renderActionMessageText(
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%target_user%',
|
||||
targetUser
|
||||
? renderUserContent(targetUser, options.asPlain)
|
||||
targetUsers
|
||||
? targetUsers.map((user) => renderUserContent(user, options.asPlain)).filter<TextPart>(Boolean as any)
|
||||
: 'User',
|
||||
);
|
||||
|
||||
@ -180,7 +180,7 @@ function renderMigratedContent(chatId: number, asPlain?: boolean): string | Text
|
||||
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);
|
||||
if (placeholderPosition < 0 || !replaceValue) {
|
||||
return [text];
|
||||
@ -188,7 +188,16 @@ function processPlaceholder(text: string, placeholder: string, replaceValue?: Te
|
||||
|
||||
const content: TextPart[] = [];
|
||||
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));
|
||||
|
||||
return content;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useLayoutEffect, useRef,
|
||||
FC, memo, useCallback, useLayoutEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
@ -66,7 +66,8 @@ type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isMuted?: boolean;
|
||||
privateChatUser?: ApiUser;
|
||||
actionTargetUser?: ApiUser;
|
||||
usersById?: Record<number, ApiUser>;
|
||||
actionTargetUserIds?: number[];
|
||||
actionTargetMessage?: ApiMessage;
|
||||
actionTargetChatId?: number;
|
||||
lastMessageSender?: ApiUser;
|
||||
@ -91,8 +92,9 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isPinned,
|
||||
chat,
|
||||
isMuted,
|
||||
usersById,
|
||||
privateChatUser,
|
||||
actionTargetUser,
|
||||
actionTargetUserIds,
|
||||
lastMessageSender,
|
||||
lastMessageOutgoingStatus,
|
||||
actionTargetMessage,
|
||||
@ -122,6 +124,12 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const mediaBlobUrl = useMedia(lastMessage ? getMessageMediaHash(lastMessage, 'micro') : undefined);
|
||||
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.
|
||||
useLayoutEffect(() => {
|
||||
const element = ref.current;
|
||||
@ -221,7 +229,7 @@ const Chat: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
lang,
|
||||
lastMessage,
|
||||
actionOrigin,
|
||||
actionTargetUser,
|
||||
actionTargetUsers,
|
||||
actionTargetMessage,
|
||||
actionTargetChatId,
|
||||
{ asPlain: true },
|
||||
@ -322,8 +330,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||
: undefined;
|
||||
const { targetUserId: actionTargetUserId, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||
const privateChatUserId = getPrivateChatUserId(chat);
|
||||
const { byId: usersById } = global.users;
|
||||
const {
|
||||
chatId: currentChatId,
|
||||
threadId: currentThreadId,
|
||||
@ -336,7 +345,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
lastMessageSender,
|
||||
...(isOutgoing && { lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage) }),
|
||||
...(privateChatUserId && { privateChatUser: selectUser(global, privateChatUserId) }),
|
||||
...(actionTargetUserId && { actionTargetUser: selectUser(global, actionTargetUserId) }),
|
||||
usersById,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
actionTargetMessage,
|
||||
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
||||
|
||||
@ -9,7 +9,7 @@ import { ApiChat, ApiUser } from '../../../api/types';
|
||||
import { pick, unique } from '../../../util/iteratees';
|
||||
import { throttle } from '../../../util/schedulers';
|
||||
import searchWords from '../../../util/searchWords';
|
||||
import { getUserFullName, sortChatIds } from '../../../modules/helpers';
|
||||
import { getUserFullName, isUserBot, sortChatIds } from '../../../modules/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
@ -98,7 +98,11 @@ const NewChatStep1: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
...foundContactIds,
|
||||
...(localUserIds || []),
|
||||
...(globalUserIds || []),
|
||||
]),
|
||||
]).filter((contactId) => {
|
||||
const user = usersById[contactId];
|
||||
|
||||
return !user || !isUserBot(user) || user.canBeInvitedToGroup;
|
||||
}),
|
||||
chatsById,
|
||||
false,
|
||||
selectedMemberIds,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
FC, memo, useEffect, useRef,
|
||||
FC, memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
@ -36,8 +36,9 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
usersById: Record<number, ApiUser>;
|
||||
sender?: ApiUser | ApiChat;
|
||||
targetUser?: ApiUser;
|
||||
targetUserIds?: number[];
|
||||
targetMessage?: ApiMessage;
|
||||
targetChatId?: number;
|
||||
isFocused: boolean;
|
||||
@ -53,8 +54,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
isEmbedded,
|
||||
appearanceOrder = 0,
|
||||
isLastInList,
|
||||
usersById,
|
||||
sender,
|
||||
targetUser,
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
isFocused,
|
||||
@ -81,11 +83,17 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
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(
|
||||
lang,
|
||||
message,
|
||||
sender,
|
||||
targetUser,
|
||||
targetUsers,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
isEmbedded ? { isEmbedded: true, asPlain: true } : undefined,
|
||||
@ -140,8 +148,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const { byId: usersById } = global.users;
|
||||
const userId = message.senderId;
|
||||
const { targetUserId, targetChatId } = message.content.action || {};
|
||||
const { targetUserIds, targetChatId } = message.content.action || {};
|
||||
const targetMessageId = message.replyToMessageId;
|
||||
const targetMessage = targetMessageId
|
||||
? selectChatMessage(global, message.chatId, targetMessageId)
|
||||
@ -156,9 +165,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
: userId ? selectUser(global, userId) : undefined;
|
||||
|
||||
return {
|
||||
usersById,
|
||||
sender,
|
||||
...(targetUserId && { targetUser: selectUser(global, targetUserId) }),
|
||||
targetChatId,
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
isFocused,
|
||||
...(isFocused && { focusDirection, noFocusHighlight }),
|
||||
|
||||
@ -90,7 +90,7 @@ const DeleteSelectedMessageModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{canDeleteForAll && (
|
||||
<Button color="danger" className="confirm-dialog-button" isText onClick={handleDeleteMessageForAll}>
|
||||
{contactName
|
||||
? lang('ChatList.DeleteForEveryone', renderText(contactName))
|
||||
? renderText(lang('ChatList.DeleteForEveryone', contactName))
|
||||
: lang('Conversation.DeleteMessagesForEveryone')}
|
||||
</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 {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
|
||||
@supports (overflow-y: overlay) {
|
||||
overflow-y: overlay !important;
|
||||
}
|
||||
|
||||
|
||||
> .profile-info > .ChatInfo {
|
||||
grid-area: chat_info;
|
||||
|
||||
@ -124,7 +125,14 @@
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: .5rem 0;
|
||||
.ListItem.chat-item-clickable {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.FloatingActionButton {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,7 @@ import {
|
||||
} from '../../api/types';
|
||||
import { GlobalActions } from '../../global/types';
|
||||
import {
|
||||
ISettings,
|
||||
MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType,
|
||||
NewChatMembersProgress, ISettings, MediaViewerOrigin, ProfileState, ProfileTabType, SharedMediaType,
|
||||
} from '../../types';
|
||||
|
||||
import {
|
||||
@ -23,7 +22,7 @@ import {
|
||||
} from '../../config';
|
||||
import { IS_TOUCH_ENV } from '../../util/environment';
|
||||
import {
|
||||
isChatAdmin, isChatChannel, isChatGroup, isChatPrivate,
|
||||
getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isChatPrivate,
|
||||
} from '../../modules/helpers';
|
||||
import {
|
||||
selectChatMessages,
|
||||
@ -54,6 +53,8 @@ import ChatExtra from './ChatExtra';
|
||||
import Media from '../common/Media';
|
||||
import WebLink from '../common/WebLink';
|
||||
import NothingFound from '../common/NothingFound';
|
||||
import FloatingActionButton from '../ui/FloatingActionButton';
|
||||
import DeleteMemberModal from './DeleteMemberModal';
|
||||
|
||||
import './Profile.scss';
|
||||
|
||||
@ -67,12 +68,15 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
theme: ISettings['theme'];
|
||||
isChannel?: boolean;
|
||||
currentUserId?: number;
|
||||
resolvedUserId?: number;
|
||||
chatMessages?: Record<number, ApiMessage>;
|
||||
foundIds?: number[];
|
||||
mediaSearchType?: SharedMediaType;
|
||||
hasMembersTab?: boolean;
|
||||
areMembersHidden?: boolean;
|
||||
canAddMembers?: boolean;
|
||||
canDeleteMembers?: boolean;
|
||||
members?: ApiChatMember[];
|
||||
usersById?: Record<number, ApiUser>;
|
||||
isRightColumnShown: boolean;
|
||||
@ -83,7 +87,7 @@ type StateProps = {
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'setLocalMediaSearchType' | 'loadMoreMembers' | 'searchMediaMessagesLocal' | 'openMediaViewer' |
|
||||
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos'
|
||||
'openAudioPlayer' | 'openUserInfo' | 'focusMessage' | 'loadProfilePhotos' | 'setNewChatMembersDialogState'
|
||||
)>;
|
||||
|
||||
const TABS = [
|
||||
@ -102,11 +106,14 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
theme,
|
||||
isChannel,
|
||||
resolvedUserId,
|
||||
currentUserId,
|
||||
chatMessages,
|
||||
foundIds,
|
||||
mediaSearchType,
|
||||
hasMembersTab,
|
||||
areMembersHidden,
|
||||
canAddMembers,
|
||||
canDeleteMembers,
|
||||
members,
|
||||
usersById,
|
||||
isRightColumnShown,
|
||||
@ -120,6 +127,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
openUserInfo,
|
||||
focusMessage,
|
||||
loadProfilePhotos,
|
||||
setNewChatMembersDialogState,
|
||||
serverTimeOffset,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -128,6 +136,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
const transitionRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const [deletingUserId, setDeletingUserId] = useState<number | undefined>();
|
||||
|
||||
const tabs = useMemo(() => ([
|
||||
...(hasMembersTab ? [{
|
||||
@ -154,6 +163,10 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
resetCacheBuster();
|
||||
}, [releaseTransitionFix, resetCacheBuster]);
|
||||
|
||||
const handleNewMemberDialogOpen = useCallback(() => {
|
||||
setNewChatMembersDialogState(NewChatMembersProgress.InProgress);
|
||||
}, [setNewChatMembersDialogState]);
|
||||
|
||||
// Update search type when switching tabs
|
||||
useEffect(() => {
|
||||
setLocalMediaSearchType({ mediaType: tabType });
|
||||
@ -188,6 +201,10 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
focusMessage({ chatId: profileId, messageId });
|
||||
}, [profileId, focusMessage]);
|
||||
|
||||
const handleDeleteMembersModalClose = useCallback(() => {
|
||||
setDeletingUserId(undefined);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transitionRef.current || !IS_TOUCH_ENV) {
|
||||
return undefined;
|
||||
@ -215,9 +232,19 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
}
|
||||
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() {
|
||||
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;
|
||||
|
||||
return (
|
||||
@ -308,6 +335,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
teactOrderKey={i}
|
||||
className="chat-item-clickable scroll-item"
|
||||
onClick={() => handleMemberClick(id)}
|
||||
contextActions={getMemberContextAction(id)}
|
||||
>
|
||||
<PrivateChatInfo userId={id} forceShowSelf />
|
||||
</ListItem>
|
||||
@ -334,7 +362,9 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
>
|
||||
{!noProfileInfo && renderProfileInfo(chatId, resolvedUserId)}
|
||||
{!isRestricted && (
|
||||
<div className="shared-media">
|
||||
<div
|
||||
className="shared-media"
|
||||
>
|
||||
<Transition
|
||||
ref={transitionRef}
|
||||
name={lang.isRtl ? 'slide-reversed' : 'slide'}
|
||||
@ -348,8 +378,26 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
{renderSharedMedia}
|
||||
</Transition>
|
||||
<TabList big activeTab={activeTab} tabs={tabs} onSwitchTab={setActiveTab} />
|
||||
|
||||
</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>
|
||||
);
|
||||
};
|
||||
@ -390,6 +438,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const hasMembersTab = isGroup || (isChannel && isChatAdmin(chat!));
|
||||
const members = chat && chat.fullInfo && chat.fullInfo.members;
|
||||
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;
|
||||
if (userId) {
|
||||
@ -407,10 +457,13 @@ export default memo(withGlobal<OwnProps>(
|
||||
mediaSearchType,
|
||||
hasMembersTab,
|
||||
areMembersHidden,
|
||||
canAddMembers,
|
||||
canDeleteMembers,
|
||||
...(hasMembersTab && members && {
|
||||
members,
|
||||
usersById,
|
||||
}),
|
||||
currentUserId: global.currentUserId,
|
||||
isRightColumnShown: selectIsRightColumnShown(global),
|
||||
isRestricted: chat && chat.isRestricted,
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
@ -426,5 +479,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
'openUserInfo',
|
||||
'focusMessage',
|
||||
'loadProfilePhotos',
|
||||
'setNewChatMembersDialogState',
|
||||
]),
|
||||
)(Profile));
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
|
||||
// @optimization
|
||||
&:not(:hover) {
|
||||
.chat-item-clickable:nth-child(n + 18) {
|
||||
.chat-item-clickable:not(.picker-list-item):nth-child(n + 18) {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,9 @@ import React, {
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
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 captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
@ -27,6 +29,7 @@ import Management from './management/Management.async';
|
||||
import StickerSearch from './StickerSearch.async';
|
||||
import GifSearch from './GifSearch.async';
|
||||
import PollResults from './PollResults.async';
|
||||
import AddChatMembers from './AddChatMembers';
|
||||
|
||||
import './RightColumn.scss';
|
||||
|
||||
@ -40,8 +43,8 @@ type StateProps = {
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, (
|
||||
'toggleChatInfo' | 'toggleManagement' | 'openUserInfo' |
|
||||
'closeLocalTextSearch' | 'closePollResults' |
|
||||
'toggleChatInfo' | 'toggleManagement' | 'openUserInfo' | 'setNewChatMembersDialogState' |
|
||||
'closeLocalTextSearch' | 'closePollResults' | 'addChatMembers' |
|
||||
'setStickerSearchQuery' | 'setGifSearchQuery'
|
||||
)>;
|
||||
|
||||
@ -69,6 +72,8 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
setStickerSearchQuery,
|
||||
setGifSearchQuery,
|
||||
closePollResults,
|
||||
addChatMembers,
|
||||
setNewChatMembersDialogState,
|
||||
shouldSkipHistoryAnimations,
|
||||
}) => {
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
@ -85,6 +90,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
const isStickerSearch = contentKey === RightColumnContent.StickerSearch;
|
||||
const isGifSearch = contentKey === RightColumnContent.GifSearch;
|
||||
const isPollResults = contentKey === RightColumnContent.PollResults;
|
||||
const isAddingChatMembers = contentKey === RightColumnContent.AddingMembers;
|
||||
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
|
||||
|
||||
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
|
||||
@ -93,6 +99,9 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
|
||||
const close = useCallback((shouldScrollUp = true) => {
|
||||
switch (contentKey) {
|
||||
case RightColumnContent.AddingMembers:
|
||||
setNewChatMembersDialogState(NewChatMembersProgress.Closed);
|
||||
break;
|
||||
case RightColumnContent.ChatInfo:
|
||||
if (isScrolledDown && shouldScrollUp) {
|
||||
setProfileState(ProfileState.Profile);
|
||||
@ -155,7 +164,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
break;
|
||||
}
|
||||
}, [
|
||||
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults,
|
||||
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults, setNewChatMembersDialogState,
|
||||
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
||||
]);
|
||||
|
||||
@ -164,6 +173,10 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
setIsPromotedByCurrentUser(isPromoted);
|
||||
}, []);
|
||||
|
||||
const handleAppendingChatMembers = useCallback((memberIds: number[]) => {
|
||||
addChatMembers({ chatId, memberIds });
|
||||
}, [addChatMembers, chatId]);
|
||||
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(close) : undefined), [isOpen, close]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -194,7 +207,8 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
|
||||
|
||||
useHistoryBack(isChatSelected && (contentKey === RightColumnContent.ChatInfo
|
||||
|| contentKey === RightColumnContent.UserInfo || contentKey === RightColumnContent.Management),
|
||||
|| contentKey === RightColumnContent.UserInfo || contentKey === RightColumnContent.Management
|
||||
|| contentKey === RightColumnContent.AddingMembers),
|
||||
() => close(false), toggleChatInfo);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
@ -204,6 +218,15 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
}
|
||||
|
||||
switch (renderingContentKey) {
|
||||
case RightColumnContent.AddingMembers:
|
||||
return (
|
||||
<AddChatMembers
|
||||
chatId={chatId!}
|
||||
onNextStep={handleAppendingChatMembers}
|
||||
isActive={isOpen && isActive}
|
||||
onClose={close}
|
||||
/>
|
||||
);
|
||||
case RightColumnContent.ChatInfo:
|
||||
case RightColumnContent.UserInfo:
|
||||
return (
|
||||
@ -258,6 +281,7 @@ const RightColumn: FC<StateProps & DispatchProps> = ({
|
||||
isStickerSearch={isStickerSearch}
|
||||
isGifSearch={isGifSearch}
|
||||
isPollResults={isPollResults}
|
||||
isAddingChatMembers={isAddingChatMembers}
|
||||
profileState={profileState}
|
||||
managementScreen={managementScreen}
|
||||
onClose={close}
|
||||
@ -299,5 +323,7 @@ export default memo(withGlobal(
|
||||
'setStickerSearchQuery',
|
||||
'setGifSearchQuery',
|
||||
'closePollResults',
|
||||
'addChatMembers',
|
||||
'setNewChatMembersDialogState',
|
||||
]),
|
||||
)(RightColumn));
|
||||
|
||||
@ -36,6 +36,7 @@ type OwnProps = {
|
||||
isStickerSearch?: boolean;
|
||||
isGifSearch?: boolean;
|
||||
isPollResults?: boolean;
|
||||
isAddingChatMembers?: boolean;
|
||||
shouldSkipAnimation?: boolean;
|
||||
profileState?: ProfileState;
|
||||
managementScreen?: ManagementScreens;
|
||||
@ -79,6 +80,7 @@ enum HeaderContent {
|
||||
StickerSearch,
|
||||
GifSearch,
|
||||
PollResults,
|
||||
AddingMembers,
|
||||
}
|
||||
|
||||
const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
@ -89,6 +91,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
isStickerSearch,
|
||||
isGifSearch,
|
||||
isPollResults,
|
||||
isAddingChatMembers,
|
||||
profileState,
|
||||
managementScreen,
|
||||
canManage,
|
||||
@ -149,6 +152,8 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
HeaderContent.StickerSearch
|
||||
) : isGifSearch ? (
|
||||
HeaderContent.GifSearch
|
||||
) : isAddingChatMembers ? (
|
||||
HeaderContent.AddingMembers
|
||||
) : isManagement ? (
|
||||
managementScreen === ManagementScreens.Initial ? (
|
||||
HeaderContent.ManageInitial
|
||||
@ -206,6 +211,8 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
case HeaderContent.AddingMembers:
|
||||
return <h3>{lang('GroupAddMembers')}</h3>;
|
||||
case HeaderContent.ManageInitial:
|
||||
return <h3>{lang('Edit')}</h3>;
|
||||
case HeaderContent.ManageChatPrivacyType:
|
||||
@ -275,6 +282,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
IS_SINGLE_COLUMN_LAYOUT
|
||||
|| contentKey === HeaderContent.SharedMedia
|
||||
|| contentKey === HeaderContent.MemberList
|
||||
|| contentKey === HeaderContent.AddingMembers
|
||||
|| isManagement
|
||||
);
|
||||
|
||||
|
||||
@ -320,14 +320,15 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, chatId)!;
|
||||
const { progress } = global.management;
|
||||
const hasLinkedChannel = Boolean(chat.fullInfo && chat.fullInfo.linkedChatId);
|
||||
const isBasicGroup = isChatBasicGroup(chat);
|
||||
|
||||
return {
|
||||
chat,
|
||||
progress,
|
||||
isBasicGroup: isChatBasicGroup(chat),
|
||||
isBasicGroup,
|
||||
hasLinkedChannel,
|
||||
canChangeInfo: getHasAdminRight(chat, 'changeInfo'),
|
||||
canBanUsers: getHasAdminRight(chat, 'banUsers'),
|
||||
canChangeInfo: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'changeInfo'),
|
||||
canBanUsers: isBasicGroup ? chat.isCreator : getHasAdminRight(chat, 'banUsers'),
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
|
||||
@ -147,5 +147,6 @@
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
font-weight: 500;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { GlobalState } from './types';
|
||||
import { NewChatMembersProgress } from '../types';
|
||||
|
||||
import {
|
||||
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 = {
|
||||
isLeftColumnShown: true,
|
||||
isChatInfoShown: false,
|
||||
newChatMembersProgress: NewChatMembersProgress.Closed,
|
||||
uiReadyState: 0,
|
||||
serverTimeOffset: 0,
|
||||
|
||||
@ -73,6 +75,8 @@ export const INITIAL_STATE: GlobalState = {
|
||||
|
||||
globalSearch: {},
|
||||
|
||||
userSearch: {},
|
||||
|
||||
localTextSearch: {
|
||||
byChatThreadKey: {},
|
||||
},
|
||||
|
||||
@ -40,6 +40,7 @@ import {
|
||||
NotifyException,
|
||||
LangCode,
|
||||
EmojiKeywords,
|
||||
NewChatMembersProgress,
|
||||
} from '../types';
|
||||
|
||||
export type MessageListType = 'thread' | 'pinned' | 'scheduled';
|
||||
@ -65,6 +66,7 @@ export type GlobalState = {
|
||||
isChatInfoShown: boolean;
|
||||
isLeftColumnShown: boolean;
|
||||
isPollModalOpen?: boolean;
|
||||
newChatMembersProgress?: NewChatMembersProgress;
|
||||
uiReadyState: 0 | 1 | 2;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
connectionState?: ApiUpdateConnectionStateType;
|
||||
@ -248,6 +250,13 @@ export type GlobalState = {
|
||||
}>>;
|
||||
};
|
||||
|
||||
userSearch: {
|
||||
query?: string;
|
||||
fetchingStatus?: boolean;
|
||||
localUserIds?: number[];
|
||||
globalUserIds?: number[];
|
||||
};
|
||||
|
||||
localTextSearch: {
|
||||
byChatThreadKey: Record<string, {
|
||||
isActive: boolean;
|
||||
@ -405,8 +414,8 @@ export type ActionTypes = (
|
||||
'showNotification' | 'dismissNotification' | 'showDialog' | 'dismissDialog' |
|
||||
// ui
|
||||
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
|
||||
'toggleSafeLinkModal' | 'disableHistoryAnimations' | 'openHistoryCalendar' | 'closeHistoryCalendar' |
|
||||
'disableContextMenuHint' |
|
||||
'toggleSafeLinkModal' | 'openHistoryCalendar' | 'closeHistoryCalendar' | 'disableContextMenuHint' |
|
||||
'setNewChatMembersDialogState' | 'disableHistoryAnimations' |
|
||||
// auth
|
||||
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
|
||||
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'goToAuthQrCode' | 'clearCache' |
|
||||
@ -418,6 +427,7 @@ export type ActionTypes = (
|
||||
'loadChatFolders' | 'loadRecommendedChatFolders' | 'editChatFolder' | 'addChatFolder' | 'deleteChatFolder' |
|
||||
'updateChat' | 'toggleSignatures' | 'loadGroupsForDiscussion' | 'linkDiscussionGroup' | 'unlinkDiscussionGroup' |
|
||||
'loadProfilePhotos' | 'loadMoreMembers' | 'setActiveChatFolder' | 'openNextChat' |
|
||||
'addChatMembers' | 'deleteChatMember' |
|
||||
// messages
|
||||
'loadViewportMessages' | 'selectMessage' | 'sendMessage' | 'cancelSendingMessage' | 'pinMessage' | 'deleteMessages' |
|
||||
'markMessageListRead' | 'markMessagesRead' | 'loadMessage' | 'focusMessage' | 'focusLastMessage' | 'sendPollVote' |
|
||||
@ -446,7 +456,7 @@ export type ActionTypes = (
|
||||
'acceptInviteConfirmation' |
|
||||
// users
|
||||
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' | 'loadCurrentUser' |
|
||||
'updateProfile' | 'checkUsername' | 'updateContact' | 'deleteUser' | 'loadUser' |
|
||||
'updateProfile' | 'checkUsername' | 'updateContact' | 'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
|
||||
// Channel / groups creation
|
||||
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
|
||||
// settings
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import { useState, useEffect } from '../lib/teact/teact';
|
||||
import { IAnchorPosition } from '../types';
|
||||
|
||||
const MENU_POSITION_VISUAL_COMFORT_SPACE_PX = 16;
|
||||
|
||||
export default (
|
||||
anchor: IAnchorPosition | undefined,
|
||||
getTriggerElement: () => HTMLElement | null,
|
||||
@ -31,16 +33,18 @@ export default (
|
||||
const menuRect = menuEl ? { width: menuEl.offsetWidth, height: menuEl.offsetHeight } : emptyRect;
|
||||
const rootRect = rootEl ? rootEl.getBoundingClientRect() : emptyRect;
|
||||
|
||||
let horizontalPostition: 'left' | 'right';
|
||||
if (x + menuRect.width + extraPaddingX < rootRect.width + rootRect.left) {
|
||||
setPositionX('left');
|
||||
x += 3;
|
||||
horizontalPostition = 'left';
|
||||
} else if (x - menuRect.width > 0) {
|
||||
setPositionX('right');
|
||||
horizontalPostition = 'right';
|
||||
x -= 3;
|
||||
} else {
|
||||
setPositionX('left');
|
||||
horizontalPostition = 'left';
|
||||
x = 16;
|
||||
}
|
||||
setPositionX(horizontalPostition);
|
||||
|
||||
if (y + menuRect.height < rootRect.height + rootRect.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,
|
||||
getMenuElement, getRootElement, getTriggerElement,
|
||||
|
||||
@ -987,6 +987,7 @@ messages.getChats#3c6aa187 id:Vector<int> = messages.Chats;
|
||||
messages.getFullChat#3b831c66 chat_id:int = messages.ChatFull;
|
||||
messages.editChatTitle#dc452855 chat_id:int title:string = 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.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||
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.editChatTitle#dc452855 chat_id:int title:string = 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.createChat#9cb126e users:Vector<InputUser> title:string = Updates;
|
||||
messages.getDhConfig#26cf8950 version:int random_length:int = messages.DhConfig;
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
import {
|
||||
ApiChat, ApiUser, ApiChatFolder, MAIN_THREAD_ID,
|
||||
} from '../../../api/types';
|
||||
import { ChatCreationProgress, ManagementProgress } from '../../../types';
|
||||
import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types';
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
|
||||
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) {
|
||||
const result = await callApi('fetchChats', {
|
||||
limit: CHAT_LIST_LOAD_SLICE,
|
||||
|
||||
@ -5,17 +5,19 @@ import {
|
||||
import { ApiUser } from '../../../api/types';
|
||||
import { ManagementProgress } from '../../../types';
|
||||
|
||||
import { debounce } from '../../../util/schedulers';
|
||||
import { debounce, throttle } from '../../../util/schedulers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { isChatPrivate } from '../../helpers';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { selectChat, selectUser } from '../../selectors';
|
||||
import {
|
||||
addChats, addUsers, updateChat, updateManagementProgress, updateUser, updateUsers,
|
||||
updateUserSearch, updateUserSearchFetchingStatus,
|
||||
} from '../../reducers';
|
||||
|
||||
const runDebouncedForFetchFullUser = debounce((cb) => cb(), 500, false, true);
|
||||
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
|
||||
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
|
||||
|
||||
addReducer('loadFullUser', (global, actions, payload) => {
|
||||
const { userId } = payload!;
|
||||
@ -200,3 +202,44 @@ addReducer('loadProfilePhotos', (global, actions, payload) => {
|
||||
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 {
|
||||
exitMessageSelectMode, replaceThreadParam, updateCurrentMessageList,
|
||||
} from '../../reducers';
|
||||
@ -55,6 +56,13 @@ addReducer('resetChatCreation', (global) => {
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('setNewChatMembersDialogState', (global, actions, payload) => {
|
||||
return {
|
||||
...global,
|
||||
newChatMembersProgress: payload,
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('openNextChat', (global, actions, payload) => {
|
||||
const { targetIndexDelta, orderedIds } = payload;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { addReducer } from '../../../lib/teact/teactn';
|
||||
|
||||
import { GlobalState } from '../../../global/types';
|
||||
|
||||
import { updateSelectedUserId } from '../../reducers';
|
||||
import { updateSelectedUserId, updateUserSearch } from '../../reducers';
|
||||
|
||||
addReducer('openUserInfo', (global, actions, payload) => {
|
||||
const { id } = payload!;
|
||||
@ -13,3 +13,14 @@ addReducer('openUserInfo', (global, actions, payload) => {
|
||||
const clearSelectedUserId = (global: GlobalState) => updateSelectedUserId(global, undefined);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 { RightColumnContent } from '../../types';
|
||||
import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
||||
|
||||
import { getSystemTheme, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { selectCurrentMessageList, selectIsPollResultsOpen } from './messages';
|
||||
@ -17,8 +17,10 @@ export function selectRightColumnContentKey(global: GlobalState) {
|
||||
const {
|
||||
users,
|
||||
isChatInfoShown,
|
||||
newChatMembersProgress,
|
||||
} = global;
|
||||
|
||||
const isAddingChatMembersShown = newChatMembersProgress !== NewChatMembersProgress.Closed;
|
||||
const isPollResults = selectIsPollResultsOpen(global);
|
||||
const isSearch = Boolean(!IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global));
|
||||
const isManagement = selectCurrentManagement(global);
|
||||
@ -43,6 +45,8 @@ export function selectRightColumnContentKey(global: GlobalState) {
|
||||
RightColumnContent.StickerSearch
|
||||
) : isGifSearch ? (
|
||||
RightColumnContent.GifSearch
|
||||
) : isAddingChatMembersShown ? (
|
||||
RightColumnContent.AddingMembers
|
||||
) : isUserInfo ? (
|
||||
RightColumnContent.UserInfo
|
||||
) : isChatInfo ? (
|
||||
|
||||
@ -215,6 +215,7 @@ export enum RightColumnContent {
|
||||
StickerSearch,
|
||||
GifSearch,
|
||||
PollResults,
|
||||
AddingMembers,
|
||||
}
|
||||
|
||||
export enum MediaViewerOrigin {
|
||||
@ -249,6 +250,12 @@ export enum ManagementProgress {
|
||||
Error,
|
||||
}
|
||||
|
||||
export enum NewChatMembersProgress {
|
||||
Closed,
|
||||
InProgress,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export type ProfileTabType = 'members' | 'media' | 'documents' | 'links' | 'audio';
|
||||
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio';
|
||||
export type ApiPrivacyKey = 'phoneNumber' | 'lastSeen' | 'profilePhoto' | 'forwards' | 'chatInvite';
|
||||
|
||||
@ -14,6 +14,7 @@ interface LangFn {
|
||||
}
|
||||
|
||||
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_RULES = {
|
||||
/* eslint-disable max-len */
|
||||
@ -55,7 +56,8 @@ let currentLangCode: string | undefined;
|
||||
|
||||
export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') => {
|
||||
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) {
|
||||
return cached;
|
||||
}
|
||||
@ -84,7 +86,8 @@ export const getTranslation: LangFn = (key: string, value?: any, format?: 'i') =
|
||||
if (value !== undefined) {
|
||||
const formattedValue = format === 'i' ? formatInteger(value) : value;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -158,5 +161,11 @@ function getPluralOption(amount: number) {
|
||||
}
|
||||
|
||||
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 { ApiChat, ApiMessage } from '../api/types';
|
||||
import { ApiChat, ApiMessage, ApiUser } from '../api/types';
|
||||
import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText';
|
||||
import { DEBUG } from '../config';
|
||||
import { getDispatch, getGlobal, setGlobal } from '../lib/teact/teactn';
|
||||
@ -221,10 +221,13 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||
: undefined;
|
||||
const {
|
||||
targetUserId: actionTargetUserId,
|
||||
targetUserIds: actionTargetUserIds,
|
||||
targetChatId: actionTargetChatId,
|
||||
} = 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 privateChatUser = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
|
||||
let body: string;
|
||||
@ -236,7 +239,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage) {
|
||||
getTranslation,
|
||||
message,
|
||||
actionOrigin,
|
||||
actionTargetUser,
|
||||
actionTargetUsers,
|
||||
actionTargetMessage,
|
||||
actionTargetChatId,
|
||||
{ asPlain: true },
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user