mirror of
https://github.com/danog/telegram-tt.git
synced 2025-01-21 21:01:29 +01:00
Management: Manage Join Requests and revoked invites (#1667)
This commit is contained in:
parent
871d83b951
commit
97891c02a5
@ -9,6 +9,7 @@ import {
|
||||
ApiChatMember,
|
||||
ApiRestrictionReason,
|
||||
ApiExportedInvite,
|
||||
ApiChatInviteImporter,
|
||||
} from '../../types';
|
||||
import { pick, pickTruthy } from '../../../util/iteratees';
|
||||
import {
|
||||
@ -388,7 +389,7 @@ export function buildApiChatBotCommands(botInfos: GramJs.BotInfo[]) {
|
||||
}, [] as ApiBotCommand[]);
|
||||
}
|
||||
|
||||
export function buildApiExportedInvite(invite: GramJs.ChatInviteExported) : ApiExportedInvite {
|
||||
export function buildApiExportedInvite(invite: GramJs.ChatInviteExported): ApiExportedInvite {
|
||||
const {
|
||||
revoked,
|
||||
date,
|
||||
@ -401,6 +402,7 @@ export function buildApiExportedInvite(invite: GramJs.ChatInviteExported) : ApiE
|
||||
requested,
|
||||
requestNeeded,
|
||||
title,
|
||||
adminId,
|
||||
} = invite;
|
||||
return {
|
||||
isRevoked: revoked,
|
||||
@ -414,5 +416,21 @@ export function buildApiExportedInvite(invite: GramJs.ChatInviteExported) : ApiE
|
||||
isRequestNeeded: requestNeeded,
|
||||
requested,
|
||||
title,
|
||||
adminId: buildApiPeerId(adminId, 'user'),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildChatInviteImporter(importer: GramJs.ChatInviteImporter): ApiChatInviteImporter {
|
||||
const {
|
||||
userId,
|
||||
date,
|
||||
about,
|
||||
requested,
|
||||
} = importer;
|
||||
return {
|
||||
userId: buildApiPeerId(userId, 'user'),
|
||||
date,
|
||||
about,
|
||||
isRequested: requested,
|
||||
};
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
buildApiChatFolder,
|
||||
buildApiChatFolderFromSuggested,
|
||||
buildApiChatBotCommands,
|
||||
buildApiExportedInvite,
|
||||
} from '../apiBuilders/chats';
|
||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||
@ -343,6 +342,8 @@ async function getFullChatInfo(chatId: string): Promise<{
|
||||
botInfo,
|
||||
call,
|
||||
availableReactions,
|
||||
recentRequesters,
|
||||
requestsPending,
|
||||
} = result.fullChat;
|
||||
|
||||
const members = buildChatMembers(participants);
|
||||
@ -361,6 +362,8 @@ async function getFullChatInfo(chatId: string): Promise<{
|
||||
}),
|
||||
groupCallId: call?.id.toString(),
|
||||
enabledReactions: availableReactions,
|
||||
requestsPending,
|
||||
recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')),
|
||||
},
|
||||
users: result.users.map(buildApiUser).filter<ApiUser>(Boolean as any),
|
||||
groupCall: call ? {
|
||||
@ -408,6 +411,8 @@ async function getFullChannelInfo(
|
||||
botInfo,
|
||||
availableReactions,
|
||||
defaultSendAs,
|
||||
requestsPending,
|
||||
recentRequesters,
|
||||
} = result.fullChat;
|
||||
|
||||
const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported
|
||||
@ -460,6 +465,8 @@ async function getFullChannelInfo(
|
||||
botCommands,
|
||||
enabledReactions: availableReactions,
|
||||
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
|
||||
requestsPending,
|
||||
recentRequesterIds: recentRequesters?.map((userId) => buildApiPeerId(userId, 'user')),
|
||||
},
|
||||
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
|
||||
groupCall: call ? {
|
||||
@ -1012,7 +1019,7 @@ export async function openChatByInvite(hash: string) {
|
||||
|
||||
if (result instanceof GramJs.ChatInvite) {
|
||||
const {
|
||||
photo, participantsCount, title, channel, requestNeeded, about,
|
||||
photo, participantsCount, title, channel, requestNeeded, about, megagroup,
|
||||
} = result;
|
||||
|
||||
if (photo instanceof GramJs.Photo) {
|
||||
@ -1026,7 +1033,7 @@ export async function openChatByInvite(hash: string) {
|
||||
about,
|
||||
hash,
|
||||
participantsCount,
|
||||
isChannel: channel,
|
||||
isChannel: channel && !megagroup,
|
||||
isRequestNeeded: requestNeeded,
|
||||
...(photo instanceof GramJs.Photo && { photo: buildApiPhoto(photo) }),
|
||||
},
|
||||
@ -1139,84 +1146,6 @@ function updateLocalDb(result: (
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchExportedChatInvites({
|
||||
peer, admin, limit = 0, isRevoked,
|
||||
}: { peer: ApiChat; admin: ApiUser; limit: number; isRevoked?: boolean }) {
|
||||
const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
|
||||
limit,
|
||||
revoked: isRevoked || undefined,
|
||||
}));
|
||||
|
||||
if (!exportedInvites) return undefined;
|
||||
|
||||
return exportedInvites.invites.map(buildApiExportedInvite);
|
||||
}
|
||||
|
||||
export async function editExportedChatInvite({
|
||||
peer, isRevoked, link, expireDate, usageLimit, isRequestNeeded, title,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
isRevoked?: boolean;
|
||||
link: string;
|
||||
expireDate?: number;
|
||||
usageLimit?: number;
|
||||
isRequestNeeded?: boolean;
|
||||
title?: string;
|
||||
}) {
|
||||
const invite = await invokeRequest(new GramJs.messages.EditExportedChatInvite({
|
||||
link,
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
expireDate,
|
||||
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||
requestNeeded: isRequestNeeded,
|
||||
title,
|
||||
revoked: isRevoked || undefined,
|
||||
}));
|
||||
|
||||
if (!invite) return undefined;
|
||||
|
||||
if (invite instanceof GramJs.messages.ExportedChatInvite) {
|
||||
const replaceInvite = buildApiExportedInvite(invite.invite);
|
||||
return {
|
||||
oldInvite: replaceInvite,
|
||||
newInvite: replaceInvite,
|
||||
};
|
||||
}
|
||||
|
||||
if (invite instanceof GramJs.messages.ExportedChatInviteReplaced) {
|
||||
const oldInvite = buildApiExportedInvite(invite.invite);
|
||||
const newInvite = buildApiExportedInvite(invite.newInvite);
|
||||
return {
|
||||
oldInvite,
|
||||
newInvite,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function exportChatInvite({
|
||||
peer, expireDate, usageLimit, isRequestNeeded, title,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
expireDate?: number;
|
||||
usageLimit?: number;
|
||||
isRequestNeeded?: boolean;
|
||||
title?: string;
|
||||
}) {
|
||||
const invite = await invokeRequest(new GramJs.messages.ExportChatInvite({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
expireDate,
|
||||
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||
requestNeeded: isRequestNeeded || undefined,
|
||||
title,
|
||||
}));
|
||||
|
||||
if (!invite) return undefined;
|
||||
return buildApiExportedInvite(invite);
|
||||
}
|
||||
|
||||
export async function importChatInvite({ hash }: { hash: string }) {
|
||||
const updates = await invokeRequest(new GramJs.messages.ImportChatInvite({ hash }));
|
||||
if (!(updates instanceof GramJs.Updates) || !updates.chats.length) {
|
||||
|
@ -15,7 +15,6 @@ export {
|
||||
getChatByUsername, togglePreHistoryHidden, updateChatDefaultBannedRights, updateChatMemberBannedRights,
|
||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||
fetchExportedChatInvites, editExportedChatInvite, exportChatInvite,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
@ -40,6 +39,8 @@ export {
|
||||
|
||||
export {
|
||||
checkChatUsername, setChatUsername, updatePrivateLink,
|
||||
fetchExportedChatInvites, editExportedChatInvite, exportChatInvite, deleteExportedChatInvite,
|
||||
deleteRevokedExportedChatInvites, fetchChatInviteImporters, hideChatJoinRequest, hideAllChatJoinRequests,
|
||||
} from './management';
|
||||
|
||||
export {
|
||||
|
@ -2,7 +2,9 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import { invokeRequest } from './client';
|
||||
import { buildInputEntity, buildInputPeer } from '../gramjsBuilders';
|
||||
import { ApiChat, OnApiUpdate } from '../../types';
|
||||
import { ApiChat, ApiUser, OnApiUpdate } from '../../types';
|
||||
import { addEntitiesWithPhotosToLocalDb } from '../helpers';
|
||||
import { buildApiExportedInvite, buildChatInviteImporter } from '../apiBuilders/chats';
|
||||
|
||||
let onUpdate: OnApiUpdate;
|
||||
|
||||
@ -59,3 +61,160 @@ export async function updatePrivateLink({
|
||||
|
||||
return result.link;
|
||||
}
|
||||
|
||||
export async function fetchExportedChatInvites({
|
||||
peer, admin, limit = 0, isRevoked,
|
||||
}: { peer: ApiChat; admin: ApiUser; limit: number; isRevoked?: boolean }) {
|
||||
const exportedInvites = await invokeRequest(new GramJs.messages.GetExportedChatInvites({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
|
||||
limit,
|
||||
revoked: isRevoked || undefined,
|
||||
}));
|
||||
|
||||
if (!exportedInvites) return undefined;
|
||||
addEntitiesWithPhotosToLocalDb(exportedInvites.users);
|
||||
return exportedInvites.invites.map(buildApiExportedInvite);
|
||||
}
|
||||
|
||||
export async function editExportedChatInvite({
|
||||
peer, isRevoked, link, expireDate, usageLimit, isRequestNeeded, title,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
isRevoked?: boolean;
|
||||
link: string;
|
||||
expireDate?: number;
|
||||
usageLimit?: number;
|
||||
isRequestNeeded?: boolean;
|
||||
title?: string;
|
||||
}) {
|
||||
const invite = await invokeRequest(new GramJs.messages.EditExportedChatInvite({
|
||||
link,
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
expireDate,
|
||||
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||
requestNeeded: isRequestNeeded,
|
||||
title,
|
||||
revoked: isRevoked || undefined,
|
||||
}));
|
||||
|
||||
if (!invite) return undefined;
|
||||
|
||||
addEntitiesWithPhotosToLocalDb(invite.users);
|
||||
if (invite instanceof GramJs.messages.ExportedChatInvite) {
|
||||
const replaceInvite = buildApiExportedInvite(invite.invite);
|
||||
return {
|
||||
oldInvite: replaceInvite,
|
||||
newInvite: replaceInvite,
|
||||
};
|
||||
}
|
||||
|
||||
if (invite instanceof GramJs.messages.ExportedChatInviteReplaced) {
|
||||
const oldInvite = buildApiExportedInvite(invite.invite);
|
||||
const newInvite = buildApiExportedInvite(invite.newInvite);
|
||||
return {
|
||||
oldInvite,
|
||||
newInvite,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function exportChatInvite({
|
||||
peer, expireDate, usageLimit, isRequestNeeded, title,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
expireDate?: number;
|
||||
usageLimit?: number;
|
||||
isRequestNeeded?: boolean;
|
||||
title?: string;
|
||||
}) {
|
||||
const invite = await invokeRequest(new GramJs.messages.ExportChatInvite({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
expireDate,
|
||||
usageLimit: !isRequestNeeded ? usageLimit : undefined,
|
||||
requestNeeded: isRequestNeeded || undefined,
|
||||
title,
|
||||
}));
|
||||
|
||||
if (!invite) return undefined;
|
||||
return buildApiExportedInvite(invite);
|
||||
}
|
||||
|
||||
export async function deleteExportedChatInvite({
|
||||
peer, link,
|
||||
}: {
|
||||
peer: ApiChat; link: string;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.DeleteExportedChatInvite({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
link,
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function deleteRevokedExportedChatInvites({
|
||||
peer, admin,
|
||||
}: {
|
||||
peer: ApiChat; admin: ApiUser;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.DeleteRevokedExportedChatInvites({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
adminId: buildInputEntity(admin.id, admin.accessHash) as GramJs.InputUser,
|
||||
}));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function fetchChatInviteImporters({
|
||||
peer, link, offsetDate = 0, offsetUser, limit = 0, isRequested,
|
||||
}: {
|
||||
peer: ApiChat; link?: string; offsetDate: number; offsetUser?: ApiUser; limit: number; isRequested?: boolean;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.messages.GetChatInviteImporters({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
link,
|
||||
offsetDate,
|
||||
offsetUser: offsetUser
|
||||
? buildInputEntity(offsetUser.id, offsetUser.accessHash) as GramJs.InputUser : new GramJs.InputUserEmpty(),
|
||||
limit,
|
||||
requested: isRequested || undefined,
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
addEntitiesWithPhotosToLocalDb(result.users);
|
||||
return result.importers.map((importer) => buildChatInviteImporter(importer));
|
||||
}
|
||||
|
||||
export function hideChatJoinRequest({
|
||||
peer,
|
||||
user,
|
||||
isApproved,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
user: ApiUser;
|
||||
isApproved: boolean;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.HideChatJoinRequest({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
userId: buildInputEntity(user.id, user.accessHash) as GramJs.InputUser,
|
||||
approved: isApproved || undefined,
|
||||
}), true);
|
||||
}
|
||||
|
||||
export function hideAllChatJoinRequests({
|
||||
peer,
|
||||
isApproved,
|
||||
link,
|
||||
}: {
|
||||
peer: ApiChat;
|
||||
isApproved: boolean;
|
||||
link?: string;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.HideAllChatJoinRequests({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
approved: isApproved || undefined,
|
||||
link,
|
||||
}), true);
|
||||
}
|
||||
|
@ -879,6 +879,20 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) {
|
||||
groupCallId: getGroupCallId(update.call),
|
||||
participants: update.participants.map(buildApiGroupCallParticipant),
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdatePendingJoinRequests) {
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
const entities = update._entities;
|
||||
if (entities) {
|
||||
addEntitiesWithPhotosToLocalDb(entities);
|
||||
dispatchUserAndChatUpdates(entities);
|
||||
}
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updatePendingJoinRequests',
|
||||
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||
recentRequesterIds: update.recentRequesters.map((id) => buildApiPeerId(id, 'user')),
|
||||
requestsPending: update.requestsPending,
|
||||
});
|
||||
} else if (DEBUG) {
|
||||
const params = typeof update === 'object' && 'className' in update ? update.className : update;
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ApiMessage, ApiPhoto } from './messages';
|
||||
import { ApiBotCommand } from './bots';
|
||||
import { ApiChatInviteImporter } from './misc';
|
||||
|
||||
type ApiChatType = (
|
||||
'chatTypePrivate' | 'chatTypeSecret' |
|
||||
@ -57,6 +58,7 @@ export interface ApiChat {
|
||||
// Obtained with UpdateUserTyping or UpdateChatUserTyping updates
|
||||
typingStatus?: ApiTypingStatus;
|
||||
|
||||
joinRequests?: ApiChatInviteImporter[];
|
||||
sendAsIds?: string[];
|
||||
}
|
||||
|
||||
@ -89,6 +91,8 @@ export interface ApiChatFullInfo {
|
||||
botCommands?: ApiBotCommand[];
|
||||
enabledReactions?: string[];
|
||||
sendAsId?: string;
|
||||
recentRequesterIds?: string[];
|
||||
requestsPending?: number;
|
||||
}
|
||||
|
||||
export interface ApiChatMember {
|
||||
|
@ -109,6 +109,14 @@ export type ApiExportedInvite = {
|
||||
isRequestNeeded?: boolean;
|
||||
requested?: number;
|
||||
title?: string;
|
||||
adminId: string;
|
||||
};
|
||||
|
||||
export type ApiChatInviteImporter = {
|
||||
userId: string;
|
||||
date: number;
|
||||
isRequested?: boolean;
|
||||
about?: string;
|
||||
};
|
||||
|
||||
export interface ApiCountry {
|
||||
|
@ -420,6 +420,13 @@ export type ApiUpdateGroupCallParticipants = {
|
||||
nextOffset?: string;
|
||||
};
|
||||
|
||||
export type ApiUpdatePendingJoinRequests = {
|
||||
'@type': 'updatePendingJoinRequests';
|
||||
chatId: string;
|
||||
recentRequesterIds: string[];
|
||||
requestsPending: number;
|
||||
};
|
||||
|
||||
export type ApiUpdateGroupCallConnection = {
|
||||
'@type': 'updateGroupCallConnection';
|
||||
data: GroupCallConnectionData;
|
||||
@ -459,7 +466,8 @@ export type ApiUpdate = (
|
||||
ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions |
|
||||
ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams |
|
||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId
|
||||
ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId |
|
||||
ApiUpdatePendingJoinRequests
|
||||
);
|
||||
|
||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||
|
@ -10,7 +10,7 @@ import { getDispatch, withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { MessageListType } from '../../global/types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { IAnchorPosition } from '../../types';
|
||||
import { IAnchorPosition, ManagementScreens } from '../../types';
|
||||
|
||||
import {
|
||||
ARE_CALLS_SUPPORTED, IS_MAC_OS, IS_PWA, IS_SINGLE_COLUMN_LAYOUT,
|
||||
@ -53,6 +53,7 @@ interface StateProps {
|
||||
canLeave?: boolean;
|
||||
canEnterVoiceChat?: boolean;
|
||||
canCreateVoiceChat?: boolean;
|
||||
pendingJoinRequests?: number;
|
||||
}
|
||||
|
||||
// Chrome breaks layout when focusing input during transition
|
||||
@ -72,6 +73,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
canLeave,
|
||||
canEnterVoiceChat,
|
||||
canCreateVoiceChat,
|
||||
pendingJoinRequests,
|
||||
isRightColumnShown,
|
||||
canExpandActions,
|
||||
}) => {
|
||||
@ -81,6 +83,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
openLocalTextSearch,
|
||||
restartBot,
|
||||
openCallFallbackConfirm,
|
||||
requestNextManagementScreen,
|
||||
} = getDispatch();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -114,6 +117,10 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
restartBot({ chatId });
|
||||
}, [chatId, restartBot]);
|
||||
|
||||
const handleJoinRequestsClick = useCallback(() => {
|
||||
requestNextManagementScreen({ screen: ManagementScreens.JoinRequests });
|
||||
}, [requestNextManagementScreen]);
|
||||
|
||||
const handleSearchClick = useCallback(() => {
|
||||
openLocalTextSearch();
|
||||
|
||||
@ -213,6 +220,20 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{Boolean(pendingJoinRequests) && (
|
||||
<Button
|
||||
round
|
||||
className="badge-button"
|
||||
ripple={isRightColumnShown}
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
onClick={handleJoinRequestsClick}
|
||||
ariaLabel={isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}
|
||||
>
|
||||
<i className="icon-user" />
|
||||
<div className="badge">{pendingJoinRequests}</div>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
ref={menuButtonRef}
|
||||
className={isMenuOpen ? 'active' : ''}
|
||||
@ -282,6 +303,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && chat.isCallActive;
|
||||
const canCreateVoiceChat = ARE_CALLS_SUPPORTED && !chat.isCallActive
|
||||
&& (chat.adminRights?.manageCall || (chat.isCreator && isChatBasicGroup(chat)));
|
||||
const pendingJoinRequests = chat.fullInfo?.requestsPending;
|
||||
|
||||
return {
|
||||
noMenu: false,
|
||||
@ -296,6 +318,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canLeave,
|
||||
canEnterVoiceChat,
|
||||
canCreateVoiceChat,
|
||||
pendingJoinRequests,
|
||||
};
|
||||
},
|
||||
)(HeaderActions));
|
||||
|
@ -540,6 +540,27 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-button {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
min-width: 1rem;
|
||||
height: 1rem;
|
||||
background: var(--color-primary);
|
||||
border-radius: 0.5rem;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.HeaderPinnedMessage-wrapper, .HeaderActions {
|
||||
|
@ -38,6 +38,7 @@ type StateProps = {
|
||||
currentProfileUserId?: string;
|
||||
isChatSelected: boolean;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
nextManagementScreen?: ManagementScreens;
|
||||
};
|
||||
|
||||
const COLUMN_CLOSE_DELAY_MS = 300;
|
||||
@ -58,6 +59,7 @@ const RightColumn: FC<StateProps> = ({
|
||||
currentProfileUserId,
|
||||
isChatSelected,
|
||||
shouldSkipHistoryAnimations,
|
||||
nextManagementScreen,
|
||||
}) => {
|
||||
const {
|
||||
toggleChatInfo,
|
||||
@ -70,6 +72,8 @@ const RightColumn: FC<StateProps> = ({
|
||||
addChatMembers,
|
||||
setNewChatMembersDialogState,
|
||||
setEditingExportedInvite,
|
||||
setOpenedInviteInfo,
|
||||
requestNextManagementScreen,
|
||||
} = getDispatch();
|
||||
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
@ -126,6 +130,7 @@ const RightColumn: FC<StateProps> = ({
|
||||
case ManagementScreens.GroupMembers:
|
||||
case ManagementScreens.Invites:
|
||||
case ManagementScreens.Reactions:
|
||||
case ManagementScreens.JoinRequests:
|
||||
setManagementScreen(ManagementScreens.Initial);
|
||||
break;
|
||||
case ManagementScreens.GroupUserPermissionsCreate:
|
||||
@ -142,7 +147,9 @@ const RightColumn: FC<StateProps> = ({
|
||||
setManagementScreen(ManagementScreens.ChatAdministrators);
|
||||
break;
|
||||
case ManagementScreens.EditInvite:
|
||||
case ManagementScreens.InviteInfo:
|
||||
setManagementScreen(ManagementScreens.Invites);
|
||||
setOpenedInviteInfo({ invite: undefined });
|
||||
setEditingExportedInvite({ chatId, invite: undefined });
|
||||
break;
|
||||
}
|
||||
@ -170,7 +177,7 @@ const RightColumn: FC<StateProps> = ({
|
||||
}, [
|
||||
contentKey, isScrolledDown, toggleChatInfo, openUserInfo, closePollResults, setNewChatMembersDialogState,
|
||||
managementScreen, toggleManagement, closeLocalTextSearch, setStickerSearchQuery, setGifSearchQuery,
|
||||
setEditingExportedInvite, chatId,
|
||||
setEditingExportedInvite, chatId, setOpenedInviteInfo,
|
||||
]);
|
||||
|
||||
const handleSelectChatMember = useCallback((memberId, isPromoted) => {
|
||||
@ -190,6 +197,13 @@ const RightColumn: FC<StateProps> = ({
|
||||
}, COLUMN_CLOSE_DELAY_MS);
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (nextManagementScreen) {
|
||||
setManagementScreen(nextManagementScreen);
|
||||
requestNextManagementScreen(undefined);
|
||||
}
|
||||
}, [nextManagementScreen, requestNextManagementScreen]);
|
||||
|
||||
// Close Right Column when it transforms into overlayed state on screen resize
|
||||
useEffect(() => {
|
||||
if (isOpen && isOverlaying) {
|
||||
@ -290,6 +304,7 @@ const RightColumn: FC<StateProps> = ({
|
||||
managementScreen={managementScreen}
|
||||
onClose={close}
|
||||
shouldSkipAnimation={shouldSkipTransition || shouldSkipHistoryAnimations}
|
||||
onScreenSelect={setManagementScreen}
|
||||
/>
|
||||
<Transition
|
||||
name={(shouldSkipTransition || shouldSkipHistoryAnimations) ? 'none' : 'zoom-fade'}
|
||||
@ -309,6 +324,7 @@ export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const { chatId, threadId } = selectCurrentMessageList(global) || {};
|
||||
const areActiveChatsLoaded = selectAreActiveChatsLoaded(global);
|
||||
const nextManagementScreen = chatId ? global.management.byChatId[chatId]?.nextScreen : undefined;
|
||||
|
||||
return {
|
||||
contentKey: selectRightColumnContentKey(global),
|
||||
@ -317,6 +333,7 @@ export default memo(withGlobal(
|
||||
currentProfileUserId: global.users.selectedId,
|
||||
isChatSelected: Boolean(chatId && areActiveChatsLoaded),
|
||||
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,
|
||||
nextManagementScreen,
|
||||
};
|
||||
},
|
||||
)(RightColumn));
|
||||
|
@ -4,6 +4,7 @@ import React, {
|
||||
import { getDispatch, withGlobal } from '../../lib/teact/teactn';
|
||||
|
||||
import { ManagementScreens, ProfileState } from '../../types';
|
||||
import { ApiExportedInvite } from '../../api/types';
|
||||
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
@ -42,6 +43,7 @@ type OwnProps = {
|
||||
profileState?: ProfileState;
|
||||
managementScreen?: ManagementScreens;
|
||||
onClose: () => void;
|
||||
onScreenSelect: (screen: ManagementScreens) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -53,6 +55,7 @@ type StateProps = {
|
||||
stickerSearchQuery?: string;
|
||||
gifSearchQuery?: string;
|
||||
isEditingInvite?: boolean;
|
||||
currentInviteInfo?: ApiExportedInvite;
|
||||
};
|
||||
|
||||
const COLUMN_CLOSE_DELAY_MS = 300;
|
||||
@ -85,9 +88,12 @@ enum HeaderContent {
|
||||
ManageInvites,
|
||||
ManageEditInvite,
|
||||
ManageReactions,
|
||||
ManageInviteInfo,
|
||||
ManageJoinRequests,
|
||||
}
|
||||
|
||||
const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
isColumnOpen,
|
||||
isProfile,
|
||||
isSearch,
|
||||
@ -103,11 +109,13 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
canManage,
|
||||
isChannel,
|
||||
onClose,
|
||||
onScreenSelect,
|
||||
messageSearchQuery,
|
||||
stickerSearchQuery,
|
||||
gifSearchQuery,
|
||||
shouldSkipAnimation,
|
||||
isEditingInvite,
|
||||
currentInviteInfo,
|
||||
}) => {
|
||||
const {
|
||||
setLocalTextSearchQuery,
|
||||
@ -115,12 +123,25 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
setGifSearchQuery,
|
||||
searchTextMessagesLocal,
|
||||
toggleManagement,
|
||||
openHistoryCalendar, addContact,
|
||||
openHistoryCalendar,
|
||||
addContact,
|
||||
setEditingExportedInvite,
|
||||
deleteExportedChatInvite,
|
||||
} = getDispatch();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const backButtonRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleEditInviteClick = useCallback(() => {
|
||||
setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! });
|
||||
onScreenSelect(ManagementScreens.EditInvite);
|
||||
}, [chatId, currentInviteInfo, onScreenSelect, setEditingExportedInvite]);
|
||||
|
||||
const handleDeleteInviteClick = useCallback(() => {
|
||||
deleteExportedChatInvite({ chatId: chatId!, link: currentInviteInfo!.link });
|
||||
onScreenSelect(ManagementScreens.Invites);
|
||||
}, [chatId, currentInviteInfo, deleteExportedChatInvite, onScreenSelect]);
|
||||
|
||||
const handleMessageSearchQueryChange = useCallback((query: string) => {
|
||||
setLocalTextSearchQuery({ query });
|
||||
|
||||
@ -203,6 +224,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
HeaderContent.ManageGroupAddAdmins
|
||||
) : managementScreen === ManagementScreens.Reactions ? (
|
||||
HeaderContent.ManageReactions
|
||||
) : managementScreen === ManagementScreens.InviteInfo ? (
|
||||
HeaderContent.ManageInviteInfo
|
||||
) : managementScreen === ManagementScreens.JoinRequests ? (
|
||||
HeaderContent.ManageJoinRequests
|
||||
) : undefined // Never reached
|
||||
) : undefined; // When column is closed
|
||||
|
||||
@ -263,6 +288,38 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
||||
return <h3>{lang('lng_group_invite_title')}</h3>;
|
||||
case HeaderContent.ManageEditInvite:
|
||||
return <h3>{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
|
||||
case HeaderContent.ManageInviteInfo:
|
||||
return (
|
||||
<>
|
||||
<h3>{lang('InviteLink')}</h3>
|
||||
<section className="tools">
|
||||
{currentInviteInfo && !currentInviteInfo.isRevoked && (
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel={lang('Edit')}
|
||||
onClick={handleEditInviteClick}
|
||||
>
|
||||
<i className="icon-edit" />
|
||||
</Button>
|
||||
)}
|
||||
{currentInviteInfo && currentInviteInfo.isRevoked && (
|
||||
<Button
|
||||
round
|
||||
color="danger"
|
||||
size="smaller"
|
||||
ariaLabel={lang('Delete')}
|
||||
onClick={handleDeleteInviteClick}
|
||||
>
|
||||
<i className="icon-delete" />
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
case HeaderContent.ManageJoinRequests:
|
||||
return <h3>{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>;
|
||||
case HeaderContent.ManageGroupAddAdmins:
|
||||
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
|
||||
case HeaderContent.StickerSearch:
|
||||
@ -381,6 +438,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
&& (isUserId(chat.id) || ((isChatAdmin(chat) || chat.isCreator) && !chat.isNotJoined)),
|
||||
);
|
||||
const isEditingInvite = Boolean(chatId && global.management.byChatId[chatId]?.editingInvite);
|
||||
const currentInviteInfo = chatId ? global.management.byChatId[chatId]?.inviteInfo?.invite : undefined;
|
||||
|
||||
return {
|
||||
canManage,
|
||||
@ -391,6 +449,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
stickerSearchQuery,
|
||||
gifSearchQuery,
|
||||
isEditingInvite,
|
||||
currentInviteInfo,
|
||||
};
|
||||
},
|
||||
)(RightHeader));
|
||||
|
55
src/components/right/management/JoinRequest.scss
Normal file
55
src/components/right/management/JoinRequest.scss
Normal file
@ -0,0 +1,55 @@
|
||||
.JoinRequest {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 1rem 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&__user {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&__user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&__user-subtitle {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
&__date {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.875rem;
|
||||
margin-left: 1rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
&__button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
101
src/components/right/management/JoinRequest.tsx
Normal file
101
src/components/right/management/JoinRequest.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import React, { FC, memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiUser } from '../../../api/types';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { getUserFullName } from '../../../modules/helpers';
|
||||
import { selectUser } from '../../../modules/selectors';
|
||||
import { formatHumanDate, formatTime, isToday } from '../../../util/dateFormat';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { createClassNameBuilder } from '../../../util/buildClassName';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import './JoinRequest.scss';
|
||||
|
||||
type OwnProps = {
|
||||
userId: string;
|
||||
about?: string;
|
||||
isChannel?: boolean;
|
||||
date: number;
|
||||
chatId: string;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
user?: ApiUser;
|
||||
isSavedMessages?: boolean;
|
||||
serverTimeOffset: number;
|
||||
};
|
||||
|
||||
const JoinRequest: FC<OwnProps & StateProps> = ({
|
||||
userId,
|
||||
about,
|
||||
date,
|
||||
isChannel,
|
||||
user,
|
||||
serverTimeOffset,
|
||||
chatId,
|
||||
}) => {
|
||||
const { openUserInfo, hideChatJoinRequest } = getDispatch();
|
||||
|
||||
const buildClassName = createClassNameBuilder('JoinRequest');
|
||||
const lang = useLang();
|
||||
|
||||
const fullName = getUserFullName(user);
|
||||
const fixedDate = (date - getServerTime(serverTimeOffset)) * 1000 + Date.now();
|
||||
|
||||
const dateString = isToday(new Date(fixedDate))
|
||||
? formatTime(lang, fixedDate) : formatHumanDate(lang, fixedDate, true, false, true);
|
||||
|
||||
const handleUserClick = () => {
|
||||
openUserInfo({ userId });
|
||||
};
|
||||
|
||||
const handleAcceptRequest = useCallback(() => {
|
||||
hideChatJoinRequest({ chatId, userId, isApproved: true });
|
||||
}, [chatId, hideChatJoinRequest, userId]);
|
||||
|
||||
const handleRejectRequest = useCallback(() => {
|
||||
hideChatJoinRequest({ chatId, userId, isApproved: false });
|
||||
}, [chatId, hideChatJoinRequest, userId]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName('&')}>
|
||||
<div className={buildClassName('top')}>
|
||||
<div className={buildClassName('user')} onClick={handleUserClick}>
|
||||
<Avatar
|
||||
key={userId}
|
||||
size="medium"
|
||||
user={user}
|
||||
/>
|
||||
<div className={buildClassName('user-info')}>
|
||||
<div className={buildClassName('user-name')}>{fullName}</div>
|
||||
<div className={buildClassName('user-subtitle')}>{about}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={buildClassName('date')}>{dateString}</div>
|
||||
</div>
|
||||
<div className={buildClassName('buttons')}>
|
||||
<Button className={buildClassName('button')} onClick={handleAcceptRequest}>
|
||||
{isChannel ? lang('ChannelAddToChannel') : lang('ChannelAddToGroup')}
|
||||
</Button>
|
||||
<Button className={buildClassName('button')} isText onClick={handleRejectRequest}>
|
||||
{lang('DismissRequest')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { userId }): StateProps => {
|
||||
const user = selectUser(global, userId);
|
||||
|
||||
return {
|
||||
user,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
};
|
||||
},
|
||||
)(JoinRequest));
|
@ -67,6 +67,7 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
deleteChannel,
|
||||
openChat,
|
||||
loadExportedChatInvites,
|
||||
loadChatJoinRequests,
|
||||
} = getDispatch();
|
||||
|
||||
const currentTitle = chat ? (chat.title || '') : '';
|
||||
@ -88,8 +89,10 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (lastSyncTime) {
|
||||
loadExportedChatInvites({ chatId });
|
||||
loadExportedChatInvites({ chatId, isRevoked: true });
|
||||
loadChatJoinRequests({ chatId });
|
||||
}
|
||||
}, [chatId, loadExportedChatInvites, lastSyncTime]);
|
||||
}, [chatId, loadExportedChatInvites, lastSyncTime, loadChatJoinRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === ManagementProgress.Complete) {
|
||||
@ -116,9 +119,13 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
onScreenSelect(ManagementScreens.ChatAdministrators);
|
||||
}, [onScreenSelect]);
|
||||
|
||||
const handleClickInvites = useCallback(() => {
|
||||
const handleClickInvites = () => {
|
||||
onScreenSelect(ManagementScreens.Invites);
|
||||
}, [onScreenSelect]);
|
||||
};
|
||||
|
||||
const handleClickRequests = () => {
|
||||
onScreenSelect(ManagementScreens.JoinRequests);
|
||||
};
|
||||
|
||||
const handleSetPhoto = useCallback((file: File) => {
|
||||
setPhoto(file);
|
||||
@ -241,6 +248,18 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
|
||||
</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{Boolean(chat.joinRequests?.length) && (
|
||||
<ListItem
|
||||
icon="add-user-filled"
|
||||
onClick={handleClickRequests}
|
||||
multiline
|
||||
>
|
||||
<span className="title">{lang('SubscribeRequests')}</span>
|
||||
<span className="subtitle">
|
||||
{formatInteger(chat.joinRequests!.length)}
|
||||
</span>
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem
|
||||
icon="reactions"
|
||||
multiline
|
||||
|
@ -79,6 +79,7 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
closeManagement,
|
||||
openChat,
|
||||
loadExportedChatInvites,
|
||||
loadChatJoinRequests,
|
||||
} = getDispatch();
|
||||
|
||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||
@ -99,8 +100,10 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (lastSyncTime && canInvite) {
|
||||
loadExportedChatInvites({ chatId });
|
||||
loadExportedChatInvites({ chatId, isRevoked: true });
|
||||
loadChatJoinRequests({ chatId });
|
||||
}
|
||||
}, [chatId, loadExportedChatInvites, lastSyncTime, canInvite]);
|
||||
}, [chatId, loadExportedChatInvites, lastSyncTime, canInvite, loadChatJoinRequests]);
|
||||
|
||||
useEffect(() => {
|
||||
if (progress === ManagementProgress.Complete) {
|
||||
@ -129,9 +132,13 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
onScreenSelect(ManagementScreens.ChatAdministrators);
|
||||
}, [onScreenSelect]);
|
||||
|
||||
const handleClickInvites = useCallback(() => {
|
||||
const handleClickInvites = () => {
|
||||
onScreenSelect(ManagementScreens.Invites);
|
||||
}, [onScreenSelect]);
|
||||
};
|
||||
|
||||
const handleClickRequests = () => {
|
||||
onScreenSelect(ManagementScreens.JoinRequests);
|
||||
};
|
||||
|
||||
const handleSetPhoto = useCallback((file: File) => {
|
||||
setPhoto(file);
|
||||
@ -317,6 +324,18 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
|
||||
</span>
|
||||
</ListItem>
|
||||
)}
|
||||
{Boolean(chat.joinRequests?.length) && (
|
||||
<ListItem
|
||||
icon="add-user-filled"
|
||||
onClick={handleClickRequests}
|
||||
multiline
|
||||
>
|
||||
<span className="title">{lang('MemberRequests')}</span>
|
||||
<span className="subtitle">
|
||||
{formatInteger(chat.joinRequests!.length)}
|
||||
</span>
|
||||
</ListItem>
|
||||
)}
|
||||
</div>
|
||||
<div className="section">
|
||||
<ListItem icon="group" multiline onClick={handleClickMembers}>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChangeEvent } from 'react';
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect, useState,
|
||||
FC, memo, useCallback, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
@ -18,6 +18,7 @@ import InputText from '../../ui/InputText';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import Button from '../../ui/Button';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import useOnChange from '../../../hooks/useOnChange';
|
||||
import CalendarModal from '../../common/CalendarModal';
|
||||
|
||||
const DEFAULT_USAGE_LIMITS = [1, 10, 100];
|
||||
@ -61,7 +62,8 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
|
||||
useEffect(() => {
|
||||
useOnChange(([oldEditingInvite]) => {
|
||||
if (oldEditingInvite === editingInvite) return;
|
||||
if (!editingInvite) {
|
||||
setTitle('');
|
||||
setSelectedExpireOption('unlimited');
|
||||
|
152
src/components/right/management/ManageInviteInfo.tsx
Normal file
152
src/components/right/management/ManageInviteInfo.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiChatInviteImporter, ApiExportedInvite, ApiUser } from '../../../api/types';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { formatFullDate, formatMediaDateTime, formatTime } from '../../../util/dateFormat';
|
||||
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
onClose: NoneToVoidFunction;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
invite?: ApiExportedInvite;
|
||||
importers?: ApiChatInviteImporter[];
|
||||
admin?: ApiUser;
|
||||
serverTimeOffset: number;
|
||||
};
|
||||
|
||||
const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
invite,
|
||||
importers,
|
||||
isActive,
|
||||
serverTimeOffset,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
showNotification,
|
||||
loadChatInviteImporters,
|
||||
openUserInfo,
|
||||
} = getDispatch();
|
||||
|
||||
const lang = useLang();
|
||||
const {
|
||||
usage = 0, usageLimit, link, adminId,
|
||||
} = invite || {};
|
||||
const expireDate = invite?.expireDate && (invite.expireDate - getServerTime(serverTimeOffset)) * 1000 + Date.now();
|
||||
const isExpired = ((invite?.expireDate || 0) - getServerTime(serverTimeOffset)) < 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (link) loadChatInviteImporters({ chatId, link });
|
||||
}, [chatId, link, loadChatInviteImporters]);
|
||||
|
||||
const handleCopyClicked = useCallback(() => {
|
||||
copyTextToClipboard(invite!.link);
|
||||
showNotification({
|
||||
message: lang('LinkCopied'),
|
||||
});
|
||||
}, [invite, lang, showNotification]);
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
|
||||
const renderImporters = () => {
|
||||
if (invite?.isRevoked) return undefined;
|
||||
if (!importers) return <p className="text-muted">{lang('Loading')}</p>;
|
||||
return (
|
||||
<div className="section">
|
||||
<p>{importers.length ? lang('PeopleJoined', usage) : lang('NoOneJoined')}</p>
|
||||
<p className="text-muted">
|
||||
{!importers.length && (
|
||||
usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')
|
||||
)}
|
||||
{importers.map((importer) => (
|
||||
<ListItem
|
||||
className="chat-item-clickable scroll-item small-icon"
|
||||
onClick={() => openUserInfo({ id: importer.userId })}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
userId={importer.userId}
|
||||
status={formatMediaDateTime(lang, importer.date * 1000, true)}
|
||||
forceShowSelf
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="Management ManageInviteInfo">
|
||||
<div className="custom-scroll">
|
||||
{!invite && (
|
||||
<p className="text-muted">{lang('Loading')}</p>
|
||||
)}
|
||||
{invite && (
|
||||
<>
|
||||
<div className="section">
|
||||
<h3>{invite.title || invite.link}</h3>
|
||||
<input
|
||||
className="form-control"
|
||||
value={invite.link}
|
||||
readOnly
|
||||
onClick={handleCopyClicked}
|
||||
/>
|
||||
<Button className="copy-link" onClick={handleCopyClicked}>{lang('CopyLink')}</Button>
|
||||
{expireDate && (
|
||||
<p className="text-muted">
|
||||
{isExpired
|
||||
? lang('ExpiredLink')
|
||||
: lang('LinkExpiresIn', `${formatFullDate(lang, expireDate)} ${formatTime(lang, expireDate)}`)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{adminId && (
|
||||
<div className="section">
|
||||
<p>{lang('LinkCreatedeBy')}</p>
|
||||
<ListItem
|
||||
className="chat-item-clickable scroll-item small-icon"
|
||||
onClick={() => openUserInfo({ id: adminId })}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
userId={adminId}
|
||||
status={formatMediaDateTime(lang, invite.date * 1000, true)}
|
||||
forceShowSelf
|
||||
/>
|
||||
</ListItem>
|
||||
</div>
|
||||
)}
|
||||
{renderImporters()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const { inviteInfo } = global.management.byChatId[chatId];
|
||||
const invite = inviteInfo?.invite;
|
||||
const importers = inviteInfo?.importers;
|
||||
|
||||
return {
|
||||
invite,
|
||||
importers,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
};
|
||||
},
|
||||
)(ManageInviteInfo));
|
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useMemo,
|
||||
FC, memo, useCallback, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
@ -15,12 +15,14 @@ import { selectChat } from '../../../modules/selectors';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import NothingFound from '../../common/NothingFound';
|
||||
import Button from '../../ui/Button';
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -32,6 +34,7 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
exportedInvites?: ApiExportedInvite[];
|
||||
revokedExportedInvites?: ApiExportedInvite[];
|
||||
serverTimeOffset: number;
|
||||
};
|
||||
|
||||
@ -49,12 +52,26 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
chat,
|
||||
exportedInvites,
|
||||
revokedExportedInvites,
|
||||
isActive,
|
||||
serverTimeOffset,
|
||||
onClose,
|
||||
onScreenSelect,
|
||||
}) => {
|
||||
const { setEditingExportedInvite, showNotification, editExportedChatInvite } = getDispatch();
|
||||
const {
|
||||
setEditingExportedInvite,
|
||||
showNotification,
|
||||
editExportedChatInvite,
|
||||
deleteExportedChatInvite,
|
||||
deleteRevokedExportedChatInvites,
|
||||
setOpenedInviteInfo,
|
||||
} = getDispatch();
|
||||
const [isDeleteRevokeAllDialogOpen, openDeleteRevokeAllDialog, closeDeleteRevokeAllDialog] = useFlag();
|
||||
const [isRevokeDialogOpen, openRevokeDialog, closeRevokeDialog] = useFlag();
|
||||
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
|
||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
const lang = useLang();
|
||||
|
||||
@ -74,8 +91,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
const primaryInviteLink = chat?.username ? `t.me/${chat.username}` : primaryInvite?.link;
|
||||
const temporalInvites = useMemo(() => {
|
||||
const invites = chat?.username ? exportedInvites : exportedInvites?.filter(({ isPermanent }) => !isPermanent);
|
||||
return invites?.filter(({ isRevoked }) => !isRevoked)
|
||||
.sort(inviteComparator);
|
||||
return invites?.sort(inviteComparator);
|
||||
}, [chat?.username, exportedInvites]);
|
||||
|
||||
const editInvite = (invite: ApiExportedInvite) => {
|
||||
@ -98,15 +114,53 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}, [chatId, editExportedChatInvite]);
|
||||
|
||||
const askToRevoke = useCallback((invite: ApiExportedInvite) => {
|
||||
setRevokingInvite(invite);
|
||||
openRevokeDialog();
|
||||
}, [openRevokeDialog]);
|
||||
|
||||
const handleRevoke = useCallback(() => {
|
||||
if (!revokingInvite) return;
|
||||
revokeInvite(revokingInvite);
|
||||
setRevokingInvite(undefined);
|
||||
closeRevokeDialog();
|
||||
}, [closeRevokeDialog, revokeInvite, revokingInvite]);
|
||||
|
||||
const handleCreateNewClick = useCallback(() => {
|
||||
onScreenSelect(ManagementScreens.EditInvite);
|
||||
}, [onScreenSelect]);
|
||||
|
||||
const handlePrimaryRevoke = useCallback(() => {
|
||||
if (primaryInvite) {
|
||||
revokeInvite(primaryInvite);
|
||||
askToRevoke(primaryInvite);
|
||||
}
|
||||
}, [primaryInvite, revokeInvite]);
|
||||
}, [askToRevoke, primaryInvite]);
|
||||
|
||||
const handleDeleteAllRevoked = useCallback(() => {
|
||||
deleteRevokedExportedChatInvites({ chatId });
|
||||
closeDeleteRevokeAllDialog();
|
||||
}, [chatId, closeDeleteRevokeAllDialog, deleteRevokedExportedChatInvites]);
|
||||
|
||||
const showInviteInfo = useCallback((invite: ApiExportedInvite) => {
|
||||
setOpenedInviteInfo({ chatId, invite });
|
||||
onScreenSelect(ManagementScreens.InviteInfo);
|
||||
}, [chatId, onScreenSelect, setOpenedInviteInfo]);
|
||||
|
||||
const deleteInvite = useCallback((invite: ApiExportedInvite) => {
|
||||
deleteExportedChatInvite({ chatId, link: invite.link });
|
||||
}, [chatId, deleteExportedChatInvite]);
|
||||
|
||||
const askToDelete = useCallback((invite: ApiExportedInvite) => {
|
||||
setDeletingInvite(invite);
|
||||
openDeleteDialog();
|
||||
}, [openDeleteDialog]);
|
||||
|
||||
const handleDelete = useCallback(() => {
|
||||
if (!deletingInvite) return;
|
||||
deleteInvite(deletingInvite);
|
||||
setDeletingInvite(undefined);
|
||||
closeDeleteDialog();
|
||||
}, [closeDeleteDialog, deleteInvite, deletingInvite]);
|
||||
|
||||
const copyLink = useCallback((link: string) => {
|
||||
copyTextToClipboard(link);
|
||||
@ -121,10 +175,10 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const prepareUsageText = (invite: ApiExportedInvite) => {
|
||||
const {
|
||||
usage = 0, usageLimit, expireDate, isPermanent, requested,
|
||||
usage = 0, usageLimit, expireDate, isPermanent, requested, isRevoked,
|
||||
} = invite;
|
||||
let text = '';
|
||||
if (usageLimit && usage < usageLimit) {
|
||||
if (!isRevoked && usageLimit && usage < usageLimit) {
|
||||
text = lang('CanJoin', usageLimit - usage);
|
||||
} else if (usage) {
|
||||
text = lang('PeopleJoined', usage);
|
||||
@ -132,6 +186,11 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
text = lang('NoOneJoined');
|
||||
}
|
||||
|
||||
if (isRevoked) {
|
||||
text += ` ${BULLET} ${lang('Revoked')}`;
|
||||
return text;
|
||||
}
|
||||
|
||||
if (requested) {
|
||||
text += ` ${BULLET} ${lang('JoinRequests', requested)}`;
|
||||
}
|
||||
@ -160,19 +219,30 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
icon: 'copy',
|
||||
handler: () => copyLink(invite.link),
|
||||
});
|
||||
if (!invite.isPermanent) {
|
||||
|
||||
if (!invite.isPermanent && !invite.isRevoked) {
|
||||
actions.push({
|
||||
title: lang('Edit'),
|
||||
icon: lang('edit'),
|
||||
icon: 'edit',
|
||||
handler: () => editInvite(invite),
|
||||
});
|
||||
}
|
||||
actions.push({
|
||||
title: lang('RevokeButton'),
|
||||
icon: lang('delete'),
|
||||
handler: () => revokeInvite(invite),
|
||||
destructive: true,
|
||||
});
|
||||
|
||||
if (!invite.isRevoked) {
|
||||
actions.push({
|
||||
title: lang('RevokeButton'),
|
||||
icon: 'delete',
|
||||
handler: () => askToRevoke(invite),
|
||||
destructive: true,
|
||||
});
|
||||
} else {
|
||||
actions.push({
|
||||
title: lang('DeleteLink'),
|
||||
icon: 'delete',
|
||||
handler: () => askToDelete(invite),
|
||||
destructive: true,
|
||||
});
|
||||
}
|
||||
return actions;
|
||||
};
|
||||
|
||||
@ -225,13 +295,13 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
|
||||
{lang('CreateNewLink')}
|
||||
</Button>
|
||||
{!temporalInvites && <NothingFound text="No links found" key="nothing" />}
|
||||
{(!temporalInvites || !temporalInvites.length) && <NothingFound text="No links found" key="nothing" />}
|
||||
{temporalInvites?.map((invite) => (
|
||||
<ListItem
|
||||
icon="link"
|
||||
secondaryIcon="more"
|
||||
multiline
|
||||
onClick={() => copyLink(invite.link)}
|
||||
onClick={() => showInviteInfo(invite)}
|
||||
contextActions={prepareContextActions(invite)}
|
||||
key={invite.link}
|
||||
>
|
||||
@ -243,18 +313,68 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
))}
|
||||
<p className="text-muted hint" key="links-hint">{lang('ManageLinksInfoHelp')}</p>
|
||||
</div>
|
||||
{revokedExportedInvites && Boolean(revokedExportedInvites.length) && (
|
||||
<div className="section" teactFastList>
|
||||
<p className="text-muted" key="title">{lang('RevokedLinks')}</p>
|
||||
<ListItem
|
||||
icon="delete"
|
||||
destructive
|
||||
key="delete"
|
||||
onClick={openDeleteRevokeAllDialog}
|
||||
>
|
||||
<span className="title">{lang('DeleteAllRevokedLinks')}</span>
|
||||
</ListItem>
|
||||
{revokedExportedInvites?.map((invite) => (
|
||||
<ListItem
|
||||
icon="link"
|
||||
secondaryIcon="more"
|
||||
multiline
|
||||
onClick={() => showInviteInfo(invite)}
|
||||
contextActions={prepareContextActions(invite)}
|
||||
key={invite.link}
|
||||
>
|
||||
<span className="title">{invite.title || invite.link}</span>
|
||||
<span className="subtitle" dir="auto">
|
||||
{prepareUsageText(invite)}
|
||||
</span>
|
||||
</ListItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
isOpen={isDeleteRevokeAllDialogOpen}
|
||||
onClose={closeDeleteRevokeAllDialog}
|
||||
title={lang('DeleteAllRevokedLinks')}
|
||||
text={lang('DeleteAllRevokedLinkHelp')}
|
||||
confirmHandler={handleDeleteAllRevoked}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isRevokeDialogOpen}
|
||||
onClose={closeRevokeDialog}
|
||||
title={lang('RevokeLink')}
|
||||
text={lang('RevokeAlert')}
|
||||
confirmHandler={handleRevoke}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isDeleteDialogOpen}
|
||||
onClose={closeDeleteDialog}
|
||||
title={lang('DeleteLink')}
|
||||
text={lang('DeleteLinkHelp')}
|
||||
confirmHandler={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const { invites } = global.management.byChatId[chatId];
|
||||
const { invites, revokedInvites } = global.management.byChatId[chatId];
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
return {
|
||||
exportedInvites: invites,
|
||||
revokedExportedInvites: revokedInvites,
|
||||
chat,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
};
|
||||
|
119
src/components/right/management/ManageJoinRequests.tsx
Normal file
119
src/components/right/management/ManageJoinRequests.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import React, {
|
||||
FC, memo, useCallback, useEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getDispatch, withGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiChat } from '../../../api/types';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import { selectChat } from '../../../modules/selectors';
|
||||
import { isChatChannel } from '../../../modules/helpers';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import JoinRequest from './JoinRequest';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
onClose: NoneToVoidFunction;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
isChannel?: boolean;
|
||||
serverTimeOffset: number;
|
||||
};
|
||||
|
||||
const ManageJoinRequests: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
chatId,
|
||||
isActive,
|
||||
isChannel,
|
||||
onClose,
|
||||
}) => {
|
||||
const { hideAllChatJoinRequests, loadChatJoinRequests } = getDispatch();
|
||||
const [isAcceptAllDialogOpen, openAcceptAllDialog, closeAcceptAllDialog] = useFlag();
|
||||
const [isRejectAllDialogOpen, openRejectAllDialog, closeRejectAllDialog] = useFlag();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useHistoryBack(isActive, onClose);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat?.joinRequests) {
|
||||
loadChatJoinRequests({ chatId });
|
||||
}
|
||||
}, [chat, chatId, loadChatJoinRequests]);
|
||||
|
||||
const handleAcceptAllRequests = useCallback(() => {
|
||||
hideAllChatJoinRequests({ chatId, isApproved: true });
|
||||
closeAcceptAllDialog();
|
||||
}, [hideAllChatJoinRequests, chatId, closeAcceptAllDialog]);
|
||||
|
||||
const handleRejectAllRequests = useCallback(() => {
|
||||
hideAllChatJoinRequests({ chatId, isApproved: false });
|
||||
closeRejectAllDialog();
|
||||
}, [hideAllChatJoinRequests, chatId, closeRejectAllDialog]);
|
||||
|
||||
return (
|
||||
<div className="Management ManageJoinRequests">
|
||||
{Boolean(chat?.joinRequests?.length) && (
|
||||
<div className="section 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 className="section">
|
||||
<div className="custom-scroll" teactFastList>
|
||||
<p key="title">
|
||||
{chat?.joinRequests?.length ? lang('JoinRequests', chat?.joinRequests?.length) : lang('NoMemberRequests')}
|
||||
</p>
|
||||
{chat?.joinRequests?.length === 0 && (
|
||||
<p className="text-muted" key="empty">
|
||||
{isChannel ? lang('NoSubscribeRequestsDescription') : lang('NoMemberRequestsDescription')}
|
||||
</p>
|
||||
)}
|
||||
{chat?.joinRequests?.map(({ userId, about, date }) => (
|
||||
<JoinRequest
|
||||
userId={userId}
|
||||
about={about}
|
||||
date={date}
|
||||
isChannel={isChannel}
|
||||
chatId={chatId}
|
||||
key={userId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
isOpen={isAcceptAllDialogOpen}
|
||||
onClose={closeAcceptAllDialog}
|
||||
title="Accept all requests?"
|
||||
text="Are you sure you want to accept all requests?"
|
||||
confirmHandler={handleAcceptAllRequests}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isRejectAllDialogOpen}
|
||||
onClose={closeRejectAllDialog}
|
||||
title="Reject all requests?"
|
||||
text="Are you sure you want to reject all requests?"
|
||||
confirmHandler={handleRejectAllRequests}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
return {
|
||||
chat,
|
||||
serverTimeOffset: global.serverTimeOffset,
|
||||
isChannel: chat && isChatChannel(chat),
|
||||
};
|
||||
},
|
||||
)(ManageJoinRequests));
|
@ -207,6 +207,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ManageInviteInfo {
|
||||
.copy-link {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ManageJoinRequests {
|
||||
.bulk-actions {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.bulk-action-button {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.ManageInvite, .ManageInvites {
|
||||
.hint {
|
||||
font-size: 0.875rem;
|
||||
|
@ -21,6 +21,8 @@ import ManageGroupUserPermissionsCreate from './ManageGroupUserPermissionsCreate
|
||||
import ManageInvites from './ManageInvites';
|
||||
import ManageInvite from './ManageInvite';
|
||||
import ManageReactions from './ManageReactions';
|
||||
import ManageInviteInfo from './ManageInviteInfo';
|
||||
import ManageJoinRequests from './ManageJoinRequests';
|
||||
|
||||
export type OwnProps = {
|
||||
chatId: string;
|
||||
@ -268,6 +270,22 @@ const Management: FC<OwnProps & StateProps> = ({
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case ManagementScreens.InviteInfo:
|
||||
return (
|
||||
<ManageInviteInfo
|
||||
chatId={chatId}
|
||||
isActive={isActive}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
case ManagementScreens.JoinRequests:
|
||||
return (
|
||||
<ManageJoinRequests
|
||||
chatId={chatId}
|
||||
isActive={isActive}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined; // Never reached
|
||||
|
@ -556,6 +556,8 @@ export type ActionTypes = (
|
||||
// management
|
||||
'toggleManagement' | 'closeManagement' | 'checkPublicLink' | 'updatePublicLink' | 'updatePrivateLink' |
|
||||
'setEditingExportedInvite' | 'loadExportedChatInvites' | 'editExportedChatInvite' | 'exportChatInvite' |
|
||||
'deleteExportedChatInvite' | 'deleteRevokedExportedChatInvites' | 'setOpenedInviteInfo' | 'loadChatInviteImporters' |
|
||||
'loadChatJoinRequests' | 'hideChatJoinRequest' | 'hideAllChatJoinRequests' | 'requestNextManagementScreen' |
|
||||
// groups
|
||||
'togglePreHistoryHidden' | 'updateChatDefaultBannedRights' | 'updateChatMemberBannedRights' | 'updateChatAdmin' |
|
||||
'acceptInviteConfirmation' |
|
||||
|
@ -1084,8 +1084,12 @@ messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||
messages.getExportedChatInvites#a2b5a3f6 flags:# revoked:flags.3?true peer:InputPeer admin_id:InputUser offset_date:flags.2?int offset_link:flags.2?string limit:int = messages.ExportedChatInvites;
|
||||
messages.editExportedChatInvite#bdca2f75 flags:# revoked:flags.2?true peer:InputPeer link:string expire_date:flags.0?int usage_limit:flags.1?int request_needed:flags.3?Bool title:flags.4?string = messages.ExportedChatInvite;
|
||||
messages.deleteRevokedExportedChatInvites#56987bd5 peer:InputPeer admin_id:InputUser = Bool;
|
||||
messages.deleteExportedChatInvite#d464a42b peer:InputPeer link:string = Bool;
|
||||
messages.getChatInviteImporters#df04dd4e flags:# requested:flags.0?true peer:InputPeer link:flags.1?string q:flags.2?string offset_date:int offset_user:InputUser limit:int = messages.ChatInviteImporters;
|
||||
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||
messages.hideChatJoinRequest#7fe7e815 flags:# approved:flags.0?true peer:InputPeer user_id:InputUser = Updates;
|
||||
messages.hideAllChatJoinRequests#e085f4ea flags:# approved:flags.0?true peer:InputPeer link:flags.1?string = Updates;
|
||||
messages.toggleNoForwards#b11eafa2 peer:InputPeer enabled:Bool = Updates;
|
||||
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
|
||||
messages.sendReaction#25690ce4 flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates;
|
||||
|
@ -77,7 +77,9 @@
|
||||
"messages.createChat",
|
||||
"messages.getExportedChatInvites",
|
||||
"messages.editExportedChatInvite",
|
||||
"messages.deleteRevokedExportedChatInvites",
|
||||
"messages.deleteExportedChatInvite",
|
||||
"messages.getChatInviteImporters",
|
||||
"messages.getDhConfig",
|
||||
"messages.readMessageContents",
|
||||
"messages.getStickers",
|
||||
@ -132,6 +134,8 @@
|
||||
"messages.unpinAllMessages",
|
||||
"messages.deleteChat",
|
||||
"messages.getMessageReadParticipants",
|
||||
"messages.hideChatJoinRequest",
|
||||
"messages.hideAllChatJoinRequests",
|
||||
"messages.toggleNoForwards",
|
||||
"messages.saveDefaultSendAs",
|
||||
"updates.getState",
|
||||
|
@ -30,7 +30,6 @@ import {
|
||||
updateChatListSecondaryInfo,
|
||||
updateManagementProgress,
|
||||
leaveChat,
|
||||
updateManagement,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectChat,
|
||||
@ -611,82 +610,6 @@ addReducer('acceptInviteConfirmation', (global, actions, payload) => {
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('loadExportedChatInvites', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, adminId, isRevoked, limit,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
const admin = selectUser(global, adminId || global.currentUserId);
|
||||
if (!peer || !admin) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('fetchExportedChatInvites', {
|
||||
peer, admin, isRevoked, limit,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
setGlobal(updateManagement(getGlobal(), chatId, { invites: result }));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('editExportedChatInvite', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, link, isRevoked, expireDate, usageLimit, isRequestNeeded, title,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('editExportedChatInvite', {
|
||||
peer,
|
||||
link,
|
||||
isRevoked,
|
||||
expireDate,
|
||||
usageLimit,
|
||||
isRequestNeeded,
|
||||
title,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
let invites = global.management.byChatId[chatId].invites || [];
|
||||
const { oldInvite, newInvite } = result;
|
||||
invites = invites.filter((current) => current.link !== oldInvite.link);
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
invites: [...invites, newInvite],
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('exportChatInvite', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, expireDate, usageLimit, isRequestNeeded, title,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('exportChatInvite', {
|
||||
peer,
|
||||
expireDate,
|
||||
usageLimit,
|
||||
isRequestNeeded,
|
||||
title,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
const invites = global.management.byChatId[chatId].invites || [];
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
invites: [...invites, result],
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('openChatByUsername', (global, actions, payload) => {
|
||||
const {
|
||||
username, messageId, commentId, startParam,
|
||||
|
@ -2,8 +2,8 @@ import { addReducer, getGlobal, setGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import { ManagementProgress } from '../../../types';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { updateManagement, updateManagementProgress } from '../../reducers';
|
||||
import { selectChat, selectCurrentMessageList } from '../../selectors';
|
||||
import { updateChat, updateManagement, updateManagementProgress } from '../../reducers';
|
||||
import { selectChat, selectCurrentMessageList, selectUser } from '../../selectors';
|
||||
import { isChatBasicGroup } from '../../helpers';
|
||||
|
||||
addReducer('checkPublicLink', (global, actions, payload) => {
|
||||
@ -82,3 +82,252 @@ addReducer('setEditingExportedInvite', (global, actions, payload) => {
|
||||
|
||||
setGlobal(updateManagement(global, chatId, { editingInvite: invite }));
|
||||
});
|
||||
|
||||
addReducer('setOpenedInviteInfo', (global, actions, payload) => {
|
||||
const { chatId, invite } = payload;
|
||||
|
||||
const update = invite ? { inviteInfo: { invite } } : { inviteInfo: undefined };
|
||||
|
||||
setGlobal(updateManagement(global, chatId, update));
|
||||
});
|
||||
|
||||
addReducer('loadExportedChatInvites', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, adminId, isRevoked, limit,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
const admin = selectUser(global, adminId || global.currentUserId);
|
||||
if (!peer || !admin) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('fetchExportedChatInvites', {
|
||||
peer, admin, isRevoked, limit,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
const update = isRevoked ? { revokedInvites: result } : { invites: result };
|
||||
|
||||
setGlobal(updateManagement(getGlobal(), chatId, update));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('editExportedChatInvite', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, link, isRevoked, expireDate, usageLimit, isRequestNeeded, title,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('editExportedChatInvite', {
|
||||
peer,
|
||||
link,
|
||||
isRevoked,
|
||||
expireDate,
|
||||
usageLimit,
|
||||
isRequestNeeded,
|
||||
title,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
let invites = global.management.byChatId[chatId].invites || [];
|
||||
const revokedInvites = global.management.byChatId[chatId].revokedInvites || [];
|
||||
const { oldInvite, newInvite } = result;
|
||||
invites = invites.filter((current) => current.link !== oldInvite.link);
|
||||
if (newInvite.isRevoked) {
|
||||
revokedInvites.unshift(newInvite);
|
||||
} else {
|
||||
invites.push(newInvite);
|
||||
}
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
invites,
|
||||
revokedInvites,
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('exportChatInvite', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, expireDate, usageLimit, isRequestNeeded, title,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('exportChatInvite', {
|
||||
peer,
|
||||
expireDate,
|
||||
usageLimit,
|
||||
isRequestNeeded,
|
||||
title,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
const invites = global.management.byChatId[chatId].invites || [];
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
invites: [...invites, result],
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('deleteExportedChatInvite', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, link,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('deleteExportedChatInvite', {
|
||||
peer,
|
||||
link,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
const managementState = global.management.byChatId[chatId];
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
invites: managementState?.invites?.filter((invite) => invite.link !== link),
|
||||
revokedInvites: managementState?.revokedInvites?.filter((invite) => invite.link !== link),
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('deleteRevokedExportedChatInvites', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, adminId,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
const admin = selectUser(global, adminId || global.currentUserId);
|
||||
if (!peer || !admin) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('deleteRevokedExportedChatInvites', {
|
||||
peer,
|
||||
admin,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
revokedInvites: [],
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('loadChatInviteImporters', (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,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
const currentInviteInfo = global.management.byChatId[chatId]?.inviteInfo;
|
||||
if (!currentInviteInfo?.invite) return;
|
||||
setGlobal(updateManagement(global, chatId, {
|
||||
inviteInfo: {
|
||||
...currentInviteInfo,
|
||||
importers: result,
|
||||
},
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('loadChatJoinRequests', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, 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,
|
||||
offsetDate,
|
||||
offsetUser,
|
||||
limit,
|
||||
isRequested: true,
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
global = getGlobal();
|
||||
setGlobal(updateChat(global, chatId, { joinRequests: result }));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('hideChatJoinRequest', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, userId, isApproved,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
const user = selectUser(global, userId);
|
||||
if (!peer || !user) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('hideChatJoinRequest', {
|
||||
peer,
|
||||
user,
|
||||
isApproved,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
const targetChat = selectChat(global, chatId);
|
||||
if (!targetChat) return;
|
||||
setGlobal(updateChat(global, chatId, {
|
||||
joinRequests: targetChat.joinRequests?.filter((importer) => importer.userId !== userId),
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('hideAllChatJoinRequests', (global, actions, payload) => {
|
||||
const {
|
||||
chatId, isApproved, link,
|
||||
} = payload!;
|
||||
const peer = selectChat(global, chatId);
|
||||
if (!peer) return;
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('hideAllChatJoinRequests', {
|
||||
peer,
|
||||
isApproved,
|
||||
link,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
global = getGlobal();
|
||||
const targetChat = selectChat(global, chatId);
|
||||
if (!targetChat) return;
|
||||
|
||||
setGlobal(updateChat(global, chatId, {
|
||||
joinRequests: [],
|
||||
fullInfo: {
|
||||
...targetChat.fullInfo,
|
||||
recentRequesterIds: [],
|
||||
requestsPending: 0,
|
||||
},
|
||||
}));
|
||||
})();
|
||||
});
|
||||
|
@ -382,5 +382,21 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => {
|
||||
actions.showDialog({ data });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updatePendingJoinRequests': {
|
||||
const { chatId, requestsPending, recentRequesterIds } = update;
|
||||
const chat = global.chats.byId[chatId];
|
||||
if (chat) {
|
||||
global = updateChat(global, chatId, {
|
||||
fullInfo: {
|
||||
...chat.fullInfo,
|
||||
requestsPending,
|
||||
recentRequesterIds,
|
||||
},
|
||||
});
|
||||
setGlobal(global);
|
||||
actions.loadChatJoinRequests({ chatId });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -54,6 +54,29 @@ addReducer('toggleManagement', (global): GlobalState | undefined => {
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('requestNextManagementScreen', (global, actions, payload): GlobalState | undefined => {
|
||||
const { screen } = payload || {};
|
||||
const { chatId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
if (!chatId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...global,
|
||||
management: {
|
||||
byChatId: {
|
||||
...global.management.byChatId,
|
||||
[chatId]: {
|
||||
...global.management.byChatId[chatId],
|
||||
isActive: true,
|
||||
nextScreen: screen,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('closeManagement', (global): GlobalState | undefined => {
|
||||
const { chatId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {
|
||||
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
|
||||
ApiChatInviteImporter,
|
||||
ApiExportedInvite,
|
||||
ApiLanguage, ApiMessage, ApiShippingAddress, ApiStickerSet,
|
||||
} from '../api/types';
|
||||
@ -283,10 +284,16 @@ export enum ManagementProgress {
|
||||
|
||||
export interface ManagementState {
|
||||
isActive: boolean;
|
||||
nextScreen?: ManagementScreens;
|
||||
isUsernameAvailable?: boolean;
|
||||
error?: string;
|
||||
invites?: ApiExportedInvite[];
|
||||
revokedInvites?: ApiExportedInvite[];
|
||||
editingInvite?: ApiExportedInvite;
|
||||
inviteInfo?: {
|
||||
invite: ApiExportedInvite;
|
||||
importers?: ApiChatInviteImporter[];
|
||||
};
|
||||
}
|
||||
|
||||
export enum NewChatMembersProgress {
|
||||
@ -334,6 +341,8 @@ export enum ManagementScreens {
|
||||
Invites,
|
||||
EditInvite,
|
||||
Reactions,
|
||||
InviteInfo,
|
||||
JoinRequests,
|
||||
}
|
||||
|
||||
export type ManagementType = 'user' | 'group' | 'channel';
|
||||
|
@ -12,6 +12,10 @@ const MAX_DAY_IN_MONTH = 31;
|
||||
const MAX_MONTH_IN_YEAR = 12;
|
||||
export const MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function isToday(date: Date) {
|
||||
return getDayStart(new Date()) === getDayStart(date);
|
||||
}
|
||||
|
||||
export function getDayStart(datetime: number | Date) {
|
||||
const date = new Date(datetime);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
Loading…
x
Reference in New Issue
Block a user