mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-22 05:11:55 +01:00
Block contacts and ban chat members; Auto-focus in Sticker and Gif Search (#1502)
This commit is contained in:
parent
a3668e8b3d
commit
f32195571e
@ -1,4 +1,4 @@
|
||||
.ForwardPicker {
|
||||
.ChatOrUserPicker {
|
||||
z-index: var(--z-media-viewer);
|
||||
|
||||
.modal-dialog {
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
.input-group {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
@ -69,6 +70,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
124
src/components/common/ChatOrUserPicker.tsx
Normal file
124
src/components/common/ChatOrUserPicker.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import { RefObject } from 'react';
|
||||
import React, {
|
||||
FC, memo, useRef, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
||||
import { isChatPrivate } from '../../modules/helpers';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import Modal from '../ui/Modal';
|
||||
import InputText from '../ui/InputText';
|
||||
import Button from '../ui/Button';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import GroupChatInfo from './GroupChatInfo';
|
||||
import PrivateChatInfo from './PrivateChatInfo';
|
||||
|
||||
import './ChatOrUserPicker.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
currentUserId?: number;
|
||||
chatOrUserIds: number[];
|
||||
isOpen: boolean;
|
||||
filterRef: RefObject<HTMLInputElement>;
|
||||
filterPlaceholder: string;
|
||||
filter: string;
|
||||
onFilterChange: (filter: string) => void;
|
||||
loadMore: NoneToVoidFunction;
|
||||
onSelectChatOrUser: (chatOrUserId: number) => void;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const ChatOrUserPicker: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
currentUserId,
|
||||
chatOrUserIds,
|
||||
filterRef,
|
||||
filter,
|
||||
filterPlaceholder,
|
||||
onFilterChange,
|
||||
onClose,
|
||||
loadMore,
|
||||
onSelectChatOrUser,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [viewportIds, getMore] = useInfiniteScroll(loadMore, chatOrUserIds, Boolean(filter));
|
||||
|
||||
useInputFocusOnOpen(filterRef, isOpen, () => { onFilterChange(''); });
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onFilterChange(e.currentTarget.value);
|
||||
}, [onFilterChange]);
|
||||
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
||||
if (viewportIds && viewportIds.length > 0) {
|
||||
onSelectChatOrUser(viewportIds[index === -1 ? 0 : index]);
|
||||
}
|
||||
}, '.ListItem-button', true);
|
||||
|
||||
const modalHeader = (
|
||||
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={onClose}
|
||||
>
|
||||
<i className="icon-close" />
|
||||
</Button>
|
||||
<InputText
|
||||
ref={filterRef}
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={filterPlaceholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
className="ChatOrUserPicker"
|
||||
header={modalHeader}
|
||||
>
|
||||
{viewportIds?.length ? (
|
||||
<InfiniteScroll
|
||||
className="picker-list custom-scroll"
|
||||
items={viewportIds}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={Boolean(filter)}
|
||||
ref={containerRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{viewportIds.map((id) => (
|
||||
<ListItem
|
||||
key={id}
|
||||
className="chat-item-clickable force-rounded-corners"
|
||||
onClick={() => onSelectChatOrUser(id)}
|
||||
>
|
||||
{isChatPrivate(id) ? (
|
||||
<PrivateChatInfo status={id === currentUserId ? lang('SavedMessagesInfo') : undefined} userId={id} />
|
||||
) : (
|
||||
<GroupChatInfo chatId={id} />
|
||||
)}
|
||||
</ListItem>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
) : viewportIds && !viewportIds.length ? (
|
||||
<p className="no-results">{lang('lng_blocked_list_not_found')}</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatOrUserPicker);
|
125
src/components/left/settings/BlockUserModal.tsx
Normal file
125
src/components/left/settings/BlockUserModal.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, {
|
||||
FC, useMemo, useState, memo, useRef, useCallback, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
import { ApiUser } from '../../../api/types';
|
||||
|
||||
import { getUserFullName } from '../../../modules/helpers';
|
||||
import searchWords from '../../../util/searchWords';
|
||||
import { pick, unique } from '../../../util/iteratees';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
usersById: Record<number, ApiUser>;
|
||||
blockedIds: number[];
|
||||
contactIds?: number[];
|
||||
localContactIds?: number[];
|
||||
currentUserId?: number;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'loadContactList' | 'setUserSearchQuery' | 'blockContact'>;
|
||||
|
||||
const BlockUserModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
usersById,
|
||||
blockedIds,
|
||||
contactIds,
|
||||
localContactIds,
|
||||
currentUserId,
|
||||
isOpen,
|
||||
onClose,
|
||||
loadContactList,
|
||||
setUserSearchQuery,
|
||||
blockContact,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [filter, setFilter] = useState('');
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const filterRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setUserSearchQuery({ query: filter });
|
||||
}, [filter, setUserSearchQuery]);
|
||||
|
||||
const filteredContactsId = useMemo(() => {
|
||||
const availableContactsId = (contactIds || []).concat(localContactIds || []).filter((contactId) => {
|
||||
return !blockedIds.includes(contactId) && contactId !== currentUserId;
|
||||
});
|
||||
|
||||
return unique(availableContactsId).reduce((acc, contactId) => {
|
||||
if (
|
||||
!filter
|
||||
|| !usersById[contactId]
|
||||
|| searchWords(getUserFullName(usersById[contactId]) || '', filter)
|
||||
|| usersById[contactId]?.username.toLowerCase().includes(filter)
|
||||
) {
|
||||
acc.push(contactId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as number[])
|
||||
.sort((firstId, secondId) => {
|
||||
const firstName = getUserFullName(usersById[firstId]) || '';
|
||||
const secondName = getUserFullName(usersById[secondId]) || '';
|
||||
|
||||
return firstName.localeCompare(secondName);
|
||||
});
|
||||
}, [blockedIds, contactIds, currentUserId, filter, localContactIds, usersById]);
|
||||
|
||||
const handleRemoveUser = useCallback((userId: number) => {
|
||||
const { id: contactId, accessHash } = usersById[userId] || {};
|
||||
if (!contactId || !accessHash) {
|
||||
return;
|
||||
}
|
||||
blockContact({ contactId, accessHash });
|
||||
onClose();
|
||||
}, [blockContact, onClose, usersById]);
|
||||
|
||||
return (
|
||||
<ChatOrUserPicker
|
||||
isOpen={isOpen}
|
||||
chatOrUserIds={filteredContactsId}
|
||||
filterRef={filterRef}
|
||||
filterPlaceholder={lang('BlockedUsers.BlockUser')}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
loadMore={loadContactList}
|
||||
onSelectChatOrUser={handleRemoveUser}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
users: {
|
||||
byId: usersById,
|
||||
},
|
||||
blocked: {
|
||||
ids: blockedIds,
|
||||
},
|
||||
contactList,
|
||||
currentUserId,
|
||||
} = global;
|
||||
|
||||
return {
|
||||
usersById,
|
||||
blockedIds,
|
||||
contactIds: contactList?.userIds,
|
||||
localContactIds: global.userSearch.localUserIds,
|
||||
currentUserId,
|
||||
};
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, [
|
||||
'loadContactList', 'setUserSearchQuery', 'blockContact',
|
||||
]),
|
||||
)(BlockUserModal));
|
@ -17,11 +17,13 @@ import renderText from '../../common/helpers/renderText';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Loading from '../../ui/Loading';
|
||||
import BlockUserModal from './BlockUserModal';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
@ -48,12 +50,12 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = (
|
||||
phoneCodeList,
|
||||
unblockContact,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
||||
const handleUnblockClick = useCallback((contactId: number) => {
|
||||
unblockContact({ contactId });
|
||||
}, [unblockContact]);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onReset, onScreenSelect, SettingsScreens.PrivacyBlockedUsers);
|
||||
|
||||
function renderContact(contactId: number, i: number, viewportOffset: number) {
|
||||
@ -110,9 +112,7 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = (
|
||||
{blockedIds!.map((contactId, i) => renderContact(contactId, i, 0))}
|
||||
</div>
|
||||
) : blockedIds && !blockedIds.length ? (
|
||||
<div className="no-results" dir="auto">
|
||||
List is empty
|
||||
</div>
|
||||
<div className="no-results" dir="auto">{lang('NoBlocked')}</div>
|
||||
) : (
|
||||
<Loading key="loading" />
|
||||
)}
|
||||
@ -121,13 +121,15 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps & DispatchProps> = (
|
||||
|
||||
<FloatingActionButton
|
||||
isShown
|
||||
onClick={() => {
|
||||
}}
|
||||
className="not-implemented"
|
||||
ariaLabel="Add a blocked user"
|
||||
onClick={openBlockUserModal}
|
||||
ariaLabel={lang('BlockContact')}
|
||||
>
|
||||
<i className="icon-add" />
|
||||
</FloatingActionButton>
|
||||
<BlockUserModal
|
||||
isOpen={isBlockUserModalOpen}
|
||||
onClose={closeBlockUserModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,31 +1,17 @@
|
||||
import React, {
|
||||
FC, useMemo, useState, memo, useRef, useEffect, useCallback,
|
||||
FC, useMemo, useState, memo, useRef, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { GlobalActions } from '../../global/types';
|
||||
import { ApiChat, MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import {
|
||||
getCanPostInChat, getChatTitle, isChatPrivate, sortChatIds,
|
||||
} from '../../modules/helpers';
|
||||
import { getCanPostInChat, getChatTitle, sortChatIds } from '../../modules/helpers';
|
||||
import searchWords from '../../util/searchWords';
|
||||
import { pick, unique } from '../../util/iteratees';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import Modal from '../ui/Modal';
|
||||
import InputText from '../ui/InputText';
|
||||
import Button from '../ui/Button';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import GroupChatInfo from '../common/GroupChatInfo';
|
||||
|
||||
import './ForwardPicker.scss';
|
||||
import ChatOrUserPicker from '../common/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
@ -42,10 +28,6 @@ type StateProps = {
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'setForwardChatId' | 'exitForwardMode' | 'loadMoreChats'>;
|
||||
|
||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||
const FOCUS_DELAY_MS = 500;
|
||||
const MODAL_HIDE_DELAY_MS = 300;
|
||||
|
||||
const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chatsById,
|
||||
pinnedIds,
|
||||
@ -57,33 +39,10 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
exitForwardMode,
|
||||
loadMoreChats,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [filter, setFilter] = useState('');
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}
|
||||
} else {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setFilter('');
|
||||
}, MODAL_HIDE_DELAY_MS);
|
||||
}
|
||||
}, [isOpen]);
|
||||
const filterRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const chatIds = useMemo(() => {
|
||||
const listIds = [
|
||||
@ -116,77 +75,23 @@ const ForwardPicker: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
], chatsById, undefined, priorityIds);
|
||||
}, [activeListIds, archivedListIds, chatsById, currentUserId, filter, lang, pinnedIds]);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(loadMoreChats, chatIds, Boolean(filter));
|
||||
|
||||
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFilter(e.currentTarget.value);
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const handleKeyDown = useKeyboardListNavigation(containerRef, isOpen, (index) => {
|
||||
if (viewportIds && viewportIds.length > 0) {
|
||||
setForwardChatId({ id: viewportIds[index === -1 ? 0 : index] });
|
||||
}
|
||||
}, '.ListItem-button', true);
|
||||
|
||||
const modalHeader = (
|
||||
<div className="modal-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={exitForwardMode}
|
||||
>
|
||||
<i className="icon-close" />
|
||||
</Button>
|
||||
<InputText
|
||||
ref={inputRef}
|
||||
value={filter}
|
||||
onChange={handleFilterChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={lang('ForwardTo')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
const handleSelectUser = useCallback((userId: number) => {
|
||||
setForwardChatId({ id: userId });
|
||||
}, [setForwardChatId]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ChatOrUserPicker
|
||||
currentUserId={currentUserId}
|
||||
isOpen={isOpen}
|
||||
chatOrUserIds={chatIds}
|
||||
filterRef={filterRef}
|
||||
filterPlaceholder={lang('ForwardTo')}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
loadMore={loadMoreChats}
|
||||
onSelectChatOrUser={handleSelectUser}
|
||||
onClose={exitForwardMode}
|
||||
className="ForwardPicker"
|
||||
header={modalHeader}
|
||||
>
|
||||
{viewportIds?.length ? (
|
||||
<InfiniteScroll
|
||||
className="picker-list custom-scroll"
|
||||
items={viewportIds}
|
||||
onLoadMore={getMore}
|
||||
noScrollRestore={Boolean(filter)}
|
||||
ref={containerRef}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{viewportIds.map((id) => (
|
||||
<ListItem
|
||||
key={id}
|
||||
className="chat-item-clickable force-rounded-corners"
|
||||
onClick={() => setForwardChatId({ id })}
|
||||
>
|
||||
{isChatPrivate(id) ? (
|
||||
<PrivateChatInfo status={id === currentUserId ? lang('SavedMessagesInfo') : undefined} userId={id} />
|
||||
) : (
|
||||
<GroupChatInfo chatId={id} />
|
||||
)}
|
||||
</ListItem>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
) : viewportIds && !viewportIds.length ? (
|
||||
<p className="no-results">Sorry, nothing found.</p>
|
||||
) : (
|
||||
<Loading />
|
||||
)}
|
||||
</Modal>
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -238,6 +238,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<SearchInput
|
||||
value={stickerSearchQuery}
|
||||
placeholder={lang('SearchStickersHint')}
|
||||
autoFocusSearch
|
||||
onChange={handleStickerSearchQueryChange}
|
||||
/>
|
||||
);
|
||||
@ -246,6 +247,7 @@ const RightHeader: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
<SearchInput
|
||||
value={gifSearchQuery}
|
||||
placeholder={lang('SearchGifsTitle')}
|
||||
autoFocusSearch
|
||||
onChange={handleGifSearchQueryChange}
|
||||
/>
|
||||
);
|
||||
|
@ -7,13 +7,16 @@ import { ApiChat, ApiChatMember, ApiUser } from '../../../api/types';
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
|
||||
import { selectChat } from '../../../modules/selectors';
|
||||
import { getUserFullName } from '../../../modules/helpers';
|
||||
import { getHasAdminRight, getUserFullName } from '../../../modules/helpers';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import RemoveGroupUserModal from './RemoveGroupUserModal';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: number;
|
||||
@ -24,6 +27,7 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
usersById: Record<number, ApiUser>;
|
||||
canDeleteMembers?: boolean;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'updateChatMemberBannedRights'>;
|
||||
@ -31,11 +35,13 @@ type DispatchProps = Pick<GlobalActions, 'updateChatMemberBannedRights'>;
|
||||
const ManageGroupRemovedUsers: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chat,
|
||||
usersById,
|
||||
canDeleteMembers,
|
||||
updateChatMemberBannedRights,
|
||||
onClose,
|
||||
isActive,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [isRemoveUserModalOpen, openRemoveUserModal, closeRemoveUserModal] = useFlag();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
|
||||
@ -96,6 +102,22 @@ const ManageGroupRemovedUsers: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{canDeleteMembers && (
|
||||
<FloatingActionButton
|
||||
isShown
|
||||
onClick={openRemoveUserModal}
|
||||
ariaLabel={lang('Channel.EditAdmin.Permission.BanUsers')}
|
||||
>
|
||||
<i className="icon-add-user-filled" />
|
||||
</FloatingActionButton>
|
||||
)}
|
||||
{chat && canDeleteMembers && (
|
||||
<RemoveGroupUserModal
|
||||
chat={chat}
|
||||
isOpen={isRemoveUserModalOpen}
|
||||
onClose={closeRemoveUserModal}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -106,8 +128,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const chat = selectChat(global, chatId);
|
||||
const { byId: usersById } = global.users;
|
||||
const canDeleteMembers = chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
|
||||
|
||||
return { chat, usersById };
|
||||
return { chat, usersById, canDeleteMembers };
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['updateChatMemberBannedRights']),
|
||||
)(ManageGroupRemovedUsers));
|
||||
|
93
src/components/right/management/RemoveGroupUserModal.tsx
Normal file
93
src/components/right/management/RemoveGroupUserModal.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, {
|
||||
FC, useMemo, useState, memo, useRef, useCallback,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { GlobalActions } from '../../../global/types';
|
||||
import { ApiChat, ApiUser } from '../../../api/types';
|
||||
|
||||
import { getUserFullName } from '../../../modules/helpers';
|
||||
import searchWords from '../../../util/searchWords';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ChatOrUserPicker from '../../common/ChatOrUserPicker';
|
||||
|
||||
export type OwnProps = {
|
||||
chat: ApiChat;
|
||||
isOpen: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
usersById: Record<number, ApiUser>;
|
||||
currentUserId?: number;
|
||||
};
|
||||
|
||||
type DispatchProps = Pick<GlobalActions, 'loadMoreMembers' | 'deleteChatMember'>;
|
||||
|
||||
const RemoveGroupUserModal: FC<OwnProps & StateProps & DispatchProps> = ({
|
||||
chat,
|
||||
usersById,
|
||||
currentUserId,
|
||||
isOpen,
|
||||
onClose,
|
||||
loadMoreMembers,
|
||||
deleteChatMember,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const [filter, setFilter] = useState('');
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const filterRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const usersId = useMemo(() => {
|
||||
const availableMembers = (chat.fullInfo?.members || []).filter((member) => {
|
||||
return !member.isAdmin && !member.isOwner && member.userId !== currentUserId;
|
||||
});
|
||||
|
||||
return availableMembers.reduce((acc, member) => {
|
||||
if (
|
||||
!filter
|
||||
|| !usersById[member.userId]
|
||||
|| searchWords(getUserFullName(usersById[member.userId]) || '', filter)
|
||||
) {
|
||||
acc.push(member.userId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as number[]);
|
||||
}, [chat.fullInfo?.members, currentUserId, filter, usersById]);
|
||||
|
||||
const handleRemoveUser = useCallback((userId: number) => {
|
||||
deleteChatMember({ chatId: chat.id, userId });
|
||||
onClose();
|
||||
}, [chat.id, deleteChatMember, onClose]);
|
||||
|
||||
return (
|
||||
<ChatOrUserPicker
|
||||
isOpen={isOpen}
|
||||
chatOrUserIds={usersId}
|
||||
filterRef={filterRef}
|
||||
filterPlaceholder={lang('ChannelBlockUser')}
|
||||
filter={filter}
|
||||
onFilterChange={setFilter}
|
||||
loadMore={loadMoreMembers}
|
||||
onSelectChatOrUser={handleRemoveUser}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const {
|
||||
users: {
|
||||
byId: usersById,
|
||||
},
|
||||
currentUserId,
|
||||
} = global;
|
||||
|
||||
return { usersById, currentUserId };
|
||||
},
|
||||
(setGlobal, actions): DispatchProps => pick(actions, ['loadMoreMembers', 'deleteChatMember']),
|
||||
)(RemoveGroupUserModal));
|
@ -6,6 +6,7 @@ import React, {
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
||||
|
||||
import Loading from './Loading';
|
||||
import Button from './Button';
|
||||
@ -25,6 +26,7 @@ type OwnProps = {
|
||||
disabled?: boolean;
|
||||
autoComplete?: string;
|
||||
canClose?: boolean;
|
||||
autoFocusSearch?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
onReset?: NoneToVoidFunction;
|
||||
onFocus?: NoneToVoidFunction;
|
||||
@ -44,6 +46,7 @@ const SearchInput: FC<OwnProps> = ({
|
||||
disabled,
|
||||
autoComplete,
|
||||
canClose,
|
||||
autoFocusSearch,
|
||||
onChange,
|
||||
onReset,
|
||||
onFocus,
|
||||
@ -57,6 +60,8 @@ const SearchInput: FC<OwnProps> = ({
|
||||
|
||||
const [isInputFocused, markInputFocused, unmarkInputFocused] = useFlag(focused);
|
||||
|
||||
useInputFocusOnOpen(inputRef, autoFocusSearch, unmarkInputFocused);
|
||||
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
|
35
src/hooks/useInputFocusOnOpen.ts
Normal file
35
src/hooks/useInputFocusOnOpen.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { RefObject } from 'react';
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
|
||||
|
||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||
const FOCUS_DELAY_MS = 500;
|
||||
const MODAL_HIDE_DELAY_MS = 300;
|
||||
|
||||
export default function useInputFocusOnOpen(
|
||||
inputRef: RefObject<HTMLInputElement | HTMLTextAreaElement>,
|
||||
isOpen?: boolean,
|
||||
onClose?: NoneToVoidFunction,
|
||||
) {
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (!IS_SINGLE_COLUMN_LAYOUT) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}
|
||||
} else {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.blur();
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
setTimeout(onClose, MODAL_HIDE_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}, [inputRef, isOpen, onClose]);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user