Management / Join Requests: Add animated stickers, various fixes (#1679)

This commit is contained in:
Alexander Zinchuk 2022-02-02 22:49:18 +01:00
parent 71ef93e993
commit bb0d846838
20 changed files with 255 additions and 60 deletions

View File

@ -46,6 +46,7 @@ type EmojiWithSkins = Record<number, Emoji>;
type AllEmojis = Record<string, Emoji | EmojiWithSkins>; type AllEmojis = Record<string, Emoji | EmojiWithSkins>;
declare module '*.png'; declare module '*.png';
declare module '*.tgs';
declare module 'pako/dist/pako_inflate' { declare module 'pako/dist/pako_inflate' {
function inflate(...args: any[]): string; function inflate(...args: any[]): string;

Binary file not shown.

Binary file not shown.

View File

@ -2,43 +2,31 @@ import { ApiMediaFormat } from '../../../api/types';
import * as mediaLoader from '../../../util/mediaLoader'; import * as mediaLoader from '../../../util/mediaLoader';
// @ts-ignore
import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs'; import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs';
// @ts-ignore
import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs';
// @ts-ignore
import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs'; import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.tgs';
// @ts-ignore
import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs'; import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs';
// @ts-ignore
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs'; import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
// @ts-ignore
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs'; import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
// @ts-ignore
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
// @ts-ignore
import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs'; import CameraFlip from '../../../assets/tgs/calls/CameraFlip.tgs';
// @ts-ignore
import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs'; import HandFilled from '../../../assets/tgs/calls/HandFilled.tgs';
// @ts-ignore
import HandOutline from '../../../assets/tgs/calls/HandOutline.tgs'; import HandOutline from '../../../assets/tgs/calls/HandOutline.tgs';
// @ts-ignore
import Speaker from '../../../assets/tgs/calls/Speaker.tgs'; import Speaker from '../../../assets/tgs/calls/Speaker.tgs';
// @ts-ignore
import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs'; import VoiceAllowTalk from '../../../assets/tgs/calls/VoiceAllowTalk.tgs';
// @ts-ignore
import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs';
// @ts-ignore
import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs';
// @ts-ignore
import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs';
// @ts-ignore
import Peach from '../../../assets/tgs/animatedEmojis/Peach.tgs'; import Peach from '../../../assets/tgs/animatedEmojis/Peach.tgs';
// @ts-ignore
import Eggplant from '../../../assets/tgs/animatedEmojis/Eggplant.tgs'; import Eggplant from '../../../assets/tgs/animatedEmojis/Eggplant.tgs';
// @ts-ignore
import Cumshot from '../../../assets/tgs/animatedEmojis/Cumshot.tgs'; import Cumshot from '../../../assets/tgs/animatedEmojis/Cumshot.tgs';
import JoinRequest from '../../../assets/tgs/invites/Requests.tgs';
import Invite from '../../../assets/tgs/invites/Invite.tgs';
export const ANIMATED_STICKERS_PATHS = { export const ANIMATED_STICKERS_PATHS = {
MonkeyIdle, MonkeyIdle,
MonkeyTracking, MonkeyTracking,
@ -58,6 +46,8 @@ export const ANIMATED_STICKERS_PATHS = {
Peach, Peach,
Eggplant, Eggplant,
Cumshot, Cumshot,
JoinRequest,
Invite,
}; };
export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) { export default function getAnimationData(name: keyof typeof ANIMATED_STICKERS_PATHS) {

View File

@ -22,7 +22,12 @@ type StateProps = {
}; };
const Dialogs: FC<StateProps> = ({ dialogs }) => { const Dialogs: FC<StateProps> = ({ dialogs }) => {
const { dismissDialog, acceptInviteConfirmation, sendMessage } = getDispatch(); const {
dismissDialog,
acceptInviteConfirmation,
sendMessage,
showNotification,
} = getDispatch();
const [isModalOpen, openModal, closeModal] = useFlag(); const [isModalOpen, openModal, closeModal] = useFlag();
const lang = useLang(); const lang = useLang();
@ -60,6 +65,9 @@ const Dialogs: FC<StateProps> = ({ dialogs }) => {
acceptInviteConfirmation({ acceptInviteConfirmation({
hash, hash,
}); });
showNotification({
message: isChannel ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'),
});
closeModal(); closeModal();
}; };
@ -79,8 +87,8 @@ const Dialogs: FC<StateProps> = ({ dialogs }) => {
header={renderInviteHeader(title, photo)} header={renderInviteHeader(title, photo)}
onCloseAnimationEnd={dismissDialog} onCloseAnimationEnd={dismissDialog}
> >
{about && <p className="modal-about">{renderText(about)}</p>} {participantsCount !== undefined && <p className="modal-help">{participantsText}</p>}
{participantsCount !== undefined && <p>{participantsText}</p>} {about && <p className="modal-about">{renderText(about, ['br'])}</p>}
{isRequestNeeded && ( {isRequestNeeded && (
<p className="modal-help"> <p className="modal-help">
{isChannel {isChannel

View File

@ -22,12 +22,15 @@ import {
} from '../../modules/helpers'; } from '../../modules/helpers';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useFlag from '../../hooks/useFlag';
import { getDayStartAt } from '../../util/dateFormat';
import SearchInput from '../ui/SearchInput'; import SearchInput from '../ui/SearchInput';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import ConfirmDialog from '../ui/ConfirmDialog';
import './RightHeader.scss'; import './RightHeader.scss';
import { getDayStartAt } from '../../util/dateFormat';
type OwnProps = { type OwnProps = {
chatId?: string; chatId?: string;
@ -131,6 +134,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const backButtonRef = useRef<HTMLDivElement>(null); const backButtonRef = useRef<HTMLDivElement>(null);
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const handleEditInviteClick = useCallback(() => { const handleEditInviteClick = useCallback(() => {
setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! }); setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! });
@ -140,7 +144,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const handleDeleteInviteClick = useCallback(() => { const handleDeleteInviteClick = useCallback(() => {
deleteExportedChatInvite({ chatId: chatId!, link: currentInviteInfo!.link }); deleteExportedChatInvite({ chatId: chatId!, link: currentInviteInfo!.link });
onScreenSelect(ManagementScreens.Invites); onScreenSelect(ManagementScreens.Invites);
}, [chatId, currentInviteInfo, deleteExportedChatInvite, onScreenSelect]); closeDeleteDialog();
}, [chatId, closeDeleteDialog, currentInviteInfo, deleteExportedChatInvite, onScreenSelect]);
const handleMessageSearchQueryChange = useCallback((query: string) => { const handleMessageSearchQueryChange = useCallback((query: string) => {
setLocalTextSearchQuery({ query }); setLocalTextSearchQuery({ query });
@ -305,15 +310,26 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</Button> </Button>
)} )}
{currentInviteInfo && currentInviteInfo.isRevoked && ( {currentInviteInfo && currentInviteInfo.isRevoked && (
<Button <>
round <Button
color="danger" round
size="smaller" color="danger"
ariaLabel={lang('Delete')} size="smaller"
onClick={handleDeleteInviteClick} ariaLabel={lang('Delete')}
> onClick={openDeleteDialog}
<i className="icon-delete" /> >
</Button> <i className="icon-delete" />
</Button>
<ConfirmDialog
isOpen={isDeleteDialogOpen}
onClose={closeDeleteDialog}
title={lang('DeleteLink')}
text={lang('DeleteLinkHelp')}
confirmIsDestructive
confirmLabel={lang('Delete')}
confirmHandler={handleDeleteInviteClick}
/>
</>
)} )}
</section> </section>
</> </>

View File

@ -21,17 +21,29 @@
&__user { &__user {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
min-width: 0;
overflow: hidden;
} }
&__user-info { &__user-info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
overflow: hidden;
margin-left: 1rem; margin-left: 1rem;
} }
&__user-subtitle { &__user-subtitle {
color: var(--color-text-secondary); color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&__user-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
&__date { &__date {

View File

@ -50,7 +50,7 @@ const JoinRequest: FC<OwnProps & StateProps> = ({
? formatTime(lang, fixedDate) : formatHumanDate(lang, fixedDate, true, false, true); ? formatTime(lang, fixedDate) : formatHumanDate(lang, fixedDate, true, false, true);
const handleUserClick = () => { const handleUserClick = () => {
openUserInfo({ userId }); openUserInfo({ id: userId });
}; };
const handleAcceptRequest = useCallback(() => { const handleAcceptRequest = useCallback(() => {

View File

@ -111,8 +111,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
return true; return true;
} }
return !user.isSelf return (isChannel || user.canBeInvitedToGroup || !isUserBot(user))
&& (isChannel || user.canBeInvitedToGroup || !isUserBot(user))
&& (!noAdmins || !adminIds.includes(contactId)); && (!noAdmins || !adminIds.includes(contactId));
}), }),
chatsById, chatsById,

View File

@ -59,6 +59,7 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
const [selectedExpireOption, setSelectedExpireOption] = useState('unlimited'); const [selectedExpireOption, setSelectedExpireOption] = useState('unlimited');
const [customUsageLimit, setCustomUsageLimit] = useState<number | undefined>(10); const [customUsageLimit, setCustomUsageLimit] = useState<number | undefined>(10);
const [selectedUsageOption, setSelectedUsageOption] = useState('0'); const [selectedUsageOption, setSelectedUsageOption] = useState('0');
const [isSubmitBlocked, setIsSubmitBlocked] = useState(false);
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
@ -81,8 +82,9 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
setCustomUsageLimit(usageLimit); setCustomUsageLimit(usageLimit);
} }
if (expireDate) { if (expireDate) {
const minSafeDate = getServerTime(serverTimeOffset) + DEFAULT_CUSTOM_EXPIRE_DATE;
setSelectedExpireOption('custom'); setSelectedExpireOption('custom');
setCustomExpireDate(expireDate * 1000); setCustomExpireDate(Math.max(expireDate, minSafeDate) * 1000);
} }
if (editingIsRequestNeeded) { if (editingIsRequestNeeded) {
setIsRequestNeeded(true); setIsRequestNeeded(true);
@ -108,6 +110,7 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
}, [closeCalendar]); }, [closeCalendar]);
const handleSaveClick = useCallback(() => { const handleSaveClick = useCallback(() => {
setIsSubmitBlocked(true);
const usageLimit = selectedUsageOption === 'custom' ? customUsageLimit : selectedUsageOption; const usageLimit = selectedUsageOption === 'custom' ? customUsageLimit : selectedUsageOption;
let expireDate; let expireDate;
switch (selectedExpireOption) { switch (selectedExpireOption) {
@ -240,6 +243,7 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
<FloatingActionButton <FloatingActionButton
isShown isShown
onClick={handleSaveClick} onClick={handleSaveClick}
disabled={isSubmitBlocked}
ariaLabel={editingInvite ? lang('SaveLink') : lang('CreateLink')} ariaLabel={editingInvite ? lang('SaveLink') : lang('CreateLink')}
> >
<i className="icon-check" /> <i className="icon-check" />

View File

@ -10,10 +10,13 @@ import useLang from '../../../hooks/useLang';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { formatFullDate, formatMediaDateTime, formatTime } from '../../../util/dateFormat'; import { formatFullDate, formatMediaDateTime, formatTime } from '../../../util/dateFormat';
import { isChatChannel } from '../../../modules/helpers';
import { selectChat } from '../../../modules/selectors';
import PrivateChatInfo from '../../common/PrivateChatInfo'; import PrivateChatInfo from '../../common/PrivateChatInfo';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Spinner from '../../ui/Spinner';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
@ -24,7 +27,9 @@ type OwnProps = {
type StateProps = { type StateProps = {
invite?: ApiExportedInvite; invite?: ApiExportedInvite;
importers?: ApiChatInviteImporter[]; importers?: ApiChatInviteImporter[];
requesters?: ApiChatInviteImporter[];
admin?: ApiUser; admin?: ApiUser;
isChannel?: boolean;
serverTimeOffset: number; serverTimeOffset: number;
}; };
@ -32,6 +37,8 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
chatId, chatId,
invite, invite,
importers, importers,
requesters,
isChannel,
isActive, isActive,
serverTimeOffset, serverTimeOffset,
onClose, onClose,
@ -39,6 +46,7 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
const { const {
showNotification, showNotification,
loadChatInviteImporters, loadChatInviteImporters,
loadChatInviteRequesters,
openUserInfo, openUserInfo,
} = getDispatch(); } = getDispatch();
@ -50,8 +58,11 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
const isExpired = ((invite?.expireDate || 0) - getServerTime(serverTimeOffset)) < 0; const isExpired = ((invite?.expireDate || 0) - getServerTime(serverTimeOffset)) < 0;
useEffect(() => { useEffect(() => {
if (link) loadChatInviteImporters({ chatId, link }); if (link) {
}, [chatId, link, loadChatInviteImporters]); loadChatInviteImporters({ chatId, link });
loadChatInviteRequesters({ chatId, link });
}
}, [chatId, link, loadChatInviteImporters, loadChatInviteRequesters]);
const handleCopyClicked = useCallback(() => { const handleCopyClicked = useCallback(() => {
copyTextToClipboard(invite!.link); copyTextToClipboard(invite!.link);
@ -63,8 +74,8 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
const renderImporters = () => { const renderImporters = () => {
if (invite?.isRevoked) return undefined; if (!importers?.length && requesters?.length) return undefined;
if (!importers) return <p className="text-muted">{lang('Loading')}</p>; if (!importers) return <Spinner />;
return ( return (
<div className="section"> <div className="section">
<p>{importers.length ? lang('PeopleJoined', usage) : lang('NoOneJoined')}</p> <p>{importers.length ? lang('PeopleJoined', usage) : lang('NoOneJoined')}</p>
@ -89,6 +100,31 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
); );
}; };
const renderRequesters = () => {
if (invite?.isRevoked) return undefined;
if (!requesters && importers) return <Spinner />;
if (!requesters?.length) return undefined;
return (
<div className="section">
<p>{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</p>
<p className="text-muted">
{requesters.map((requester) => (
<ListItem
className="chat-item-clickable scroll-item small-icon"
onClick={() => openUserInfo({ id: requester.userId })}
>
<PrivateChatInfo
userId={requester.userId}
status={formatMediaDateTime(lang, requester.date * 1000, true)}
forceShowSelf
/>
</ListItem>
))}
</p>
</div>
);
};
return ( return (
<div className="Management ManageInviteInfo"> <div className="Management ManageInviteInfo">
<div className="custom-scroll"> <div className="custom-scroll">
@ -98,7 +134,7 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
{invite && ( {invite && (
<> <>
<div className="section"> <div className="section">
<h3>{invite.title || invite.link}</h3> <h3 className="link-title">{invite.title || invite.link}</h3>
<input <input
className="form-control" className="form-control"
value={invite.link} value={invite.link}
@ -130,6 +166,7 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
</div> </div>
)} )}
{renderImporters()} {renderImporters()}
{renderRequesters()}
</> </>
)} )}
</div> </div>
@ -140,12 +177,15 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const { inviteInfo } = global.management.byChatId[chatId]; const { inviteInfo } = global.management.byChatId[chatId];
const invite = inviteInfo?.invite; const { invite, importers, requesters } = inviteInfo || {};
const importers = inviteInfo?.importers; const chat = selectChat(global, chatId);
const isChannel = chat && isChatChannel(chat);
return { return {
invite, invite,
importers, importers,
requesters,
isChannel,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
}; };
}, },

View File

@ -1,11 +1,13 @@
import React, { import React, {
FC, memo, useCallback, useMemo, useState, FC, memo, useCallback, useEffect, useMemo, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
import { ApiChat, ApiExportedInvite } from '../../../api/types'; import { ApiChat, ApiExportedInvite } from '../../../api/types';
import { ManagementScreens } from '../../../types'; import { ManagementScreens } from '../../../types';
import { STICKER_SIZE_INVITES } from '../../../config';
import getAnimationData from '../../common/helpers/animatedAssets';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/dateFormat'; import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/dateFormat';
@ -16,6 +18,7 @@ import { copyTextToClipboard } from '../../../util/clipboard';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment'; import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { isChatChannel } from '../../../modules/helpers';
import ListItem from '../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import NothingFound from '../../common/NothingFound'; import NothingFound from '../../common/NothingFound';
@ -23,6 +26,7 @@ import Button from '../../ui/Button';
import DropdownMenu from '../../ui/DropdownMenu'; import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
import AnimatedSticker from '../../common/AnimatedSticker';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
@ -33,6 +37,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
isChannel?: boolean;
exportedInvites?: ApiExportedInvite[]; exportedInvites?: ApiExportedInvite[];
revokedExportedInvites?: ApiExportedInvite[]; revokedExportedInvites?: ApiExportedInvite[];
serverTimeOffset: number; serverTimeOffset: number;
@ -54,6 +59,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
exportedInvites, exportedInvites,
revokedExportedInvites, revokedExportedInvites,
isActive, isActive,
isChannel,
serverTimeOffset, serverTimeOffset,
onClose, onClose,
onScreenSelect, onScreenSelect,
@ -66,14 +72,26 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
deleteRevokedExportedChatInvites, deleteRevokedExportedChatInvites,
setOpenedInviteInfo, setOpenedInviteInfo,
} = getDispatch(); } = getDispatch();
const lang = useLang();
const [isDeleteRevokeAllDialogOpen, openDeleteRevokeAllDialog, closeDeleteRevokeAllDialog] = useFlag(); const [isDeleteRevokeAllDialogOpen, openDeleteRevokeAllDialog, closeDeleteRevokeAllDialog] = useFlag();
const [isRevokeDialogOpen, openRevokeDialog, closeRevokeDialog] = useFlag(); const [isRevokeDialogOpen, openRevokeDialog, closeRevokeDialog] = useFlag();
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>(); const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>(); const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
const [animationData, setAnimationData] = useState<string>();
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
useEffect(() => {
if (!animationData) {
getAnimationData('Invite').then(setAnimationData);
}
}, [animationData]);
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
const lang = useLang();
const hasDetailedCountdown = useMemo(() => { const hasDetailedCountdown = useMemo(() => {
if (!exportedInvites) return undefined; if (!exportedInvites) return undefined;
@ -265,6 +283,20 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
return ( return (
<div className="Management ManageInvites"> <div className="Management ManageInvites">
<div className="custom-scroll"> <div className="custom-scroll">
<div className="section">
<div className="section-icon">
{animationData && (
<AnimatedSticker
id="inviteDuck"
size={STICKER_SIZE_INVITES}
animationData={animationData}
play={isAnimationLoaded}
onLoad={handleAnimationLoad}
/>
)}
</div>
<p className='text-muted'>{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p>
</div>
{primaryInviteLink && ( {primaryInviteLink && (
<div className="section"> <div className="section">
<p className="text-muted"> <p className="text-muted">
@ -347,6 +379,8 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
onClose={closeDeleteRevokeAllDialog} onClose={closeDeleteRevokeAllDialog}
title={lang('DeleteAllRevokedLinks')} title={lang('DeleteAllRevokedLinks')}
text={lang('DeleteAllRevokedLinkHelp')} text={lang('DeleteAllRevokedLinkHelp')}
confirmIsDestructive
confirmLabel={lang('DeleteAll')}
confirmHandler={handleDeleteAllRevoked} confirmHandler={handleDeleteAllRevoked}
/> />
<ConfirmDialog <ConfirmDialog
@ -354,6 +388,8 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
onClose={closeRevokeDialog} onClose={closeRevokeDialog}
title={lang('RevokeLink')} title={lang('RevokeLink')}
text={lang('RevokeAlert')} text={lang('RevokeAlert')}
confirmIsDestructive
confirmLabel={lang('RevokeButton')}
confirmHandler={handleRevoke} confirmHandler={handleRevoke}
/> />
<ConfirmDialog <ConfirmDialog
@ -361,6 +397,8 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
onClose={closeDeleteDialog} onClose={closeDeleteDialog}
title={lang('DeleteLink')} title={lang('DeleteLink')}
text={lang('DeleteLinkHelp')} text={lang('DeleteLinkHelp')}
confirmIsDestructive
confirmLabel={lang('Delete')}
confirmHandler={handleDelete} confirmHandler={handleDelete}
/> />
</div> </div>
@ -371,12 +409,14 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const { invites, revokedInvites } = global.management.byChatId[chatId]; const { invites, revokedInvites } = global.management.byChatId[chatId];
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const isChannel = chat && isChatChannel(chat);
return { return {
exportedInvites: invites, exportedInvites: invites,
revokedExportedInvites: revokedInvites, revokedExportedInvites: revokedInvites,
chat, chat,
serverTimeOffset: global.serverTimeOffset, serverTimeOffset: global.serverTimeOffset,
isChannel,
}; };
}, },
)(ManageInvites)); )(ManageInvites));

View File

@ -1,19 +1,23 @@
import React, { import React, {
FC, memo, useCallback, useEffect, FC, memo, useCallback, useEffect, useState,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getDispatch, withGlobal } from '../../../lib/teact/teactn'; import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
import { ApiChat } from '../../../api/types'; import { ApiChat } from '../../../api/types';
import { STICKER_SIZE_JOIN_REQUESTS } from '../../../config';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import { selectChat } from '../../../modules/selectors'; import { selectChat } from '../../../modules/selectors';
import { isChatChannel } from '../../../modules/helpers'; import { isChatChannel, isUserId } from '../../../modules/helpers';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import getAnimationData from '../../common/helpers/animatedAssets';
import JoinRequest from './JoinRequest'; import JoinRequest from './JoinRequest';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
import AnimatedSticker from '../../common/AnimatedSticker';
import Spinner from '../../ui/Spinner';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
@ -40,10 +44,20 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const [animationData, setAnimationData] = useState<string>();
const [isAnimationLoaded, setIsAnimationLoaded] = useState(false);
const handleAnimationLoad = useCallback(() => setIsAnimationLoaded(true), []);
useEffect(() => {
if (!animationData) {
getAnimationData('JoinRequest').then(setAnimationData);
}
}, [animationData]);
useHistoryBack(isActive, onClose); useHistoryBack(isActive, onClose);
useEffect(() => { useEffect(() => {
if (!chat?.joinRequests) { if (!chat?.joinRequests && !isUserId(chatId)) {
loadChatJoinRequests({ chatId }); loadChatJoinRequests({ chatId });
} }
}, [chat, chatId, loadChatJoinRequests]); }, [chat, chatId, loadChatJoinRequests]);
@ -60,17 +74,34 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
return ( return (
<div className="Management ManageJoinRequests"> <div className="Management ManageJoinRequests">
{Boolean(chat?.joinRequests?.length) && ( <div className="section">
<div className="section bulk-actions"> <div className="section-icon">
<Button className="bulk-action-button" onClick={openAcceptAllDialog}>Accept all</Button> {animationData && (
<Button className="bulk-action-button" onClick={openRejectAllDialog} isText>Dismiss all</Button> <AnimatedSticker
id="joinRequestDucks"
size={STICKER_SIZE_JOIN_REQUESTS}
animationData={animationData}
play={isAnimationLoaded}
onLoad={handleAnimationLoad}
/>
)}
</div> </div>
)} {Boolean(chat?.joinRequests?.length) && (
<div className="bulk-actions">
<Button className="bulk-action-button" onClick={openAcceptAllDialog}>Accept all</Button>
<Button className="bulk-action-button" onClick={openRejectAllDialog} isText>Dismiss all</Button>
</div>
)}
</div>
<div className="section"> <div className="section">
<div className="custom-scroll" teactFastList> <div className="custom-scroll" teactFastList>
<p key="title"> <p key="title">
{chat?.joinRequests?.length ? lang('JoinRequests', chat?.joinRequests?.length) : lang('NoMemberRequests')} {!chat?.joinRequests ? lang('Loading') : chat.joinRequests.length
? lang('JoinRequests', chat.joinRequests.length) : lang('NoMemberRequests')}
</p> </p>
{!chat?.joinRequests && (
<Spinner key="loading" />
)}
{chat?.joinRequests?.length === 0 && ( {chat?.joinRequests?.length === 0 && (
<p className="text-muted" key="empty"> <p className="text-muted" key="empty">
{isChannel ? lang('NoSubscribeRequestsDescription') : lang('NoMemberRequestsDescription')} {isChannel ? lang('NoSubscribeRequestsDescription') : lang('NoMemberRequestsDescription')}

View File

@ -183,6 +183,10 @@
box-shadow: none; box-shadow: none;
} }
} }
.Spinner {
margin: 2rem auto;
}
} }
.ManageGroupMembers { .ManageGroupMembers {
@ -197,6 +201,7 @@
.primary-link-input { .primary-link-input {
cursor: pointer; cursor: pointer;
margin-bottom: 1rem; margin-bottom: 1rem;
padding-right: 3rem;
} }
.primary-link-more-menu { .primary-link-more-menu {
@ -230,6 +235,10 @@
-webkit-appearance: none; -webkit-appearance: none;
} }
} }
.custom-scroll {
padding-bottom: 4rem;
}
} }
.ManageInviteInfo { .ManageInviteInfo {
@ -237,6 +246,10 @@
margin-top: 1rem; margin-top: 1rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.link-title {
text-align: center;
}
} }
.ManageJoinRequests { .ManageJoinRequests {
@ -251,7 +264,8 @@
} }
} }
.ManageInvite, .ManageInvites { .ManageInvite,
.ManageInvites {
.hint { .hint {
font-size: 0.875rem; font-size: 0.875rem;
margin-bottom: 0; margin-bottom: 0;

View File

@ -114,7 +114,6 @@ const ListItem: FC<OwnProps> = ({
const handleSecondaryIconClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => { const handleSecondaryIconClick = (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (disabled || e.button !== 0 || (!onSecondaryIconClick && !contextActions)) return; if (disabled || e.button !== 0 || (!onSecondaryIconClick && !contextActions)) return;
e.stopPropagation(); e.stopPropagation();
if (onSecondaryIconClick) { if (onSecondaryIconClick) {
onSecondaryIconClick(e); onSecondaryIconClick(e);
@ -188,7 +187,8 @@ const ListItem: FC<OwnProps> = ({
round round
color="translucent" color="translucent"
size="smaller" size="smaller"
onMouseDown={handleSecondaryIconClick} onClick={IS_TOUCH_ENV ? handleSecondaryIconClick : undefined}
onMouseDown={!IS_TOUCH_ENV ? handleSecondaryIconClick : undefined}
> >
<i className={`icon-${secondaryIcon}`} /> <i className={`icon-${secondaryIcon}`} />
</Button> </Button>

View File

@ -140,7 +140,11 @@
} }
.modal-about { .modal-about {
font-weight: bold; display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 5;
text-overflow: ellipsis;
overflow: hidden;
} }
.modal-help { .modal-help {

View File

@ -121,8 +121,10 @@ export const STICKER_SIZE_SEARCH = 64;
export const STICKER_SIZE_MODAL = 64; export const STICKER_SIZE_MODAL = 64;
export const STICKER_SIZE_TWO_FA = 160; export const STICKER_SIZE_TWO_FA = 160;
export const STICKER_SIZE_DISCUSSION_GROUPS = 140; export const STICKER_SIZE_DISCUSSION_GROUPS = 140;
export const STICKER_SIZE_FOLDER_SETTINGS = 80; export const STICKER_SIZE_FOLDER_SETTINGS = 100;
export const STICKER_SIZE_INLINE_BOT_RESULT = 100; export const STICKER_SIZE_INLINE_BOT_RESULT = 100;
export const STICKER_SIZE_JOIN_REQUESTS = 140;
export const STICKER_SIZE_INVITES = 140;
export const RECENT_STICKERS_LIMIT = 20; export const RECENT_STICKERS_LIMIT = 20;
export const MEMOJI_STICKER_ID = 'MEMOJI_STICKER'; export const MEMOJI_STICKER_ID = 'MEMOJI_STICKER';

View File

@ -558,6 +558,7 @@ export type ActionTypes = (
'setEditingExportedInvite' | 'loadExportedChatInvites' | 'editExportedChatInvite' | 'exportChatInvite' | 'setEditingExportedInvite' | 'loadExportedChatInvites' | 'editExportedChatInvite' | 'exportChatInvite' |
'deleteExportedChatInvite' | 'deleteRevokedExportedChatInvites' | 'setOpenedInviteInfo' | 'loadChatInviteImporters' | 'deleteExportedChatInvite' | 'deleteRevokedExportedChatInvites' | 'setOpenedInviteInfo' | 'loadChatInviteImporters' |
'loadChatJoinRequests' | 'hideChatJoinRequest' | 'hideAllChatJoinRequests' | 'requestNextManagementScreen' | 'loadChatJoinRequests' | 'hideChatJoinRequest' | 'hideAllChatJoinRequests' | 'requestNextManagementScreen' |
'loadChatInviteRequesters' |
// groups // groups
'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' | 'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' |
'acceptInviteConfirmation' | 'acceptInviteConfirmation' |

View File

@ -243,7 +243,7 @@ addReducer('loadChatInviteImporters', (global, actions, payload) => {
} }
global = getGlobal(); global = getGlobal();
const currentInviteInfo = global.management.byChatId[chatId]?.inviteInfo; const currentInviteInfo = global.management.byChatId[chatId]?.inviteInfo;
if (!currentInviteInfo?.invite) return; if (!currentInviteInfo?.invite || currentInviteInfo.invite.link !== link) return;
setGlobal(updateManagement(global, chatId, { setGlobal(updateManagement(global, chatId, {
inviteInfo: { inviteInfo: {
...currentInviteInfo, ...currentInviteInfo,
@ -253,6 +253,38 @@ addReducer('loadChatInviteImporters', (global, actions, payload) => {
})(); })();
}); });
addReducer('loadChatInviteRequesters', (global, actions, payload) => {
const {
chatId, link, offsetDate, offsetUserId, limit,
} = payload!;
const peer = selectChat(global, chatId);
const offsetUser = selectUser(global, offsetUserId);
if (!peer || (offsetUserId && !offsetUser)) return;
(async () => {
const result = await callApi('fetchChatInviteImporters', {
peer,
link,
offsetDate,
offsetUser,
limit,
isRequested: true,
});
if (!result) {
return;
}
global = getGlobal();
const currentInviteInfo = global.management.byChatId[chatId]?.inviteInfo;
if (!currentInviteInfo?.invite || currentInviteInfo.invite.link !== link) return;
setGlobal(updateManagement(global, chatId, {
inviteInfo: {
...currentInviteInfo,
requesters: result,
},
}));
})();
});
addReducer('loadChatJoinRequests', (global, actions, payload) => { addReducer('loadChatJoinRequests', (global, actions, payload) => {
const { const {
chatId, offsetDate, offsetUserId, limit, chatId, offsetDate, offsetUserId, limit,

View File

@ -293,6 +293,7 @@ export interface ManagementState {
inviteInfo?: { inviteInfo?: {
invite: ApiExportedInvite; invite: ApiExportedInvite;
importers?: ApiChatInviteImporter[]; importers?: ApiChatInviteImporter[];
requesters?: ApiChatInviteImporter[];
}; };
} }