Introduce Chat Lists (#3046)
This commit is contained in:
parent
bfa9758c9b
commit
ce9e5b03d8
@ -10,7 +10,7 @@ import { DEFAULT_LIMITS } from '../../../config';
|
||||
type LimitType = 'default' | 'premium';
|
||||
type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit' | 'dialog_filters_chats_limit' |
|
||||
'dialog_filters_limit' | 'dialogs_folder_pinned_limit' | 'dialogs_pinned_limit' | 'caption_length_limit' |
|
||||
'channels_limit' | 'channels_public_limit' | 'about_length_limit';
|
||||
'channels_limit' | 'channels_public_limit' | 'about_length_limit' | 'chatlist_invites_limit' | 'chatlist_joined_limit';
|
||||
type LimitKey = `${Limit}_${LimitType}`;
|
||||
type LimitsConfig = Record<LimitKey, number>;
|
||||
|
||||
@ -99,6 +99,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
channels: getLimit(appConfig, 'channels_limit', 'channels'),
|
||||
channelsPublic: getLimit(appConfig, 'channels_public_limit', 'channelsPublic'),
|
||||
aboutLength: getLimit(appConfig, 'about_length_limit', 'aboutLength'),
|
||||
chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'),
|
||||
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
|
||||
},
|
||||
hash,
|
||||
};
|
||||
|
||||
@ -14,6 +14,8 @@ import type {
|
||||
ApiTopic,
|
||||
ApiSendAsPeerId,
|
||||
ApiChatReactions,
|
||||
ApiChatlistInvite,
|
||||
ApiChatlistExportedInvite,
|
||||
} from '../../types';
|
||||
import { pick, pickTruthy } from '../../../util/iteratees';
|
||||
import {
|
||||
@ -363,7 +365,20 @@ export function buildChatTypingStatus(
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiChatFolder(filter: GramJs.DialogFilter): ApiChatFolder {
|
||||
export function buildApiChatFolder(filter: GramJs.DialogFilter | GramJs.DialogFilterChatlist): ApiChatFolder {
|
||||
if (filter instanceof GramJs.DialogFilterChatlist) {
|
||||
return {
|
||||
...pickTruthy(filter, [
|
||||
'id', 'title', 'emoticon',
|
||||
]),
|
||||
excludedChatIds: [],
|
||||
includedChatIds: filter.includePeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
pinnedChatIds: filter.pinnedPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
hasMyInvites: filter.hasMyInvites,
|
||||
isChatList: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...pickTruthy(filter, [
|
||||
'id', 'title', 'emoticon', 'contacts', 'nonContacts', 'groups', 'bots',
|
||||
@ -382,7 +397,7 @@ export function buildApiChatFolderFromSuggested({
|
||||
filter: GramJs.TypeDialogFilter;
|
||||
description: string;
|
||||
}): ApiChatFolder | undefined {
|
||||
if (!(filter instanceof GramJs.DialogFilter)) return undefined;
|
||||
if (!(filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist)) return undefined;
|
||||
return {
|
||||
...buildApiChatFolder(filter),
|
||||
description,
|
||||
@ -441,12 +456,14 @@ export function buildChatInviteImporter(importer: GramJs.ChatInviteImporter): Ap
|
||||
date,
|
||||
about,
|
||||
requested,
|
||||
viaChatlist,
|
||||
} = importer;
|
||||
return {
|
||||
userId: buildApiPeerId(userId, 'user'),
|
||||
date,
|
||||
about,
|
||||
isRequested: requested,
|
||||
isFromChatList: viaChatlist,
|
||||
};
|
||||
}
|
||||
|
||||
@ -534,3 +551,45 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
|
||||
isMuted: silent || (muteUntil !== undefined ? muteUntil > 0 : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiChatlistInvite(
|
||||
invite: GramJs.chatlists.TypeChatlistInvite | undefined, slug: string,
|
||||
): ApiChatlistInvite | undefined {
|
||||
if (invite instanceof GramJs.chatlists.ChatlistInvite) {
|
||||
return {
|
||||
slug,
|
||||
title: invite.title,
|
||||
emoticon: invite.emoticon,
|
||||
peerIds: invite.peers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
if (invite instanceof GramJs.chatlists.ChatlistInviteAlready) {
|
||||
return {
|
||||
slug,
|
||||
folderId: invite.filterId,
|
||||
missingPeerIds: invite.missingPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
alreadyPeerIds: invite.alreadyPeers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function buildApiChatlistExportedInvite(
|
||||
invite: GramJs.TypeExportedChatlistInvite | undefined,
|
||||
): ApiChatlistExportedInvite | undefined {
|
||||
if (!(invite instanceof GramJs.ExportedChatlistInvite)) return undefined;
|
||||
|
||||
const {
|
||||
title,
|
||||
url,
|
||||
peers,
|
||||
} = invite;
|
||||
|
||||
return {
|
||||
title,
|
||||
url,
|
||||
peerIds: peers.map(getApiChatIdFromMtpPeer).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@ -225,7 +225,7 @@ export function buildInputPollFromExisting(poll: ApiPoll, shouldClose = false) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFilter {
|
||||
export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFilter | GramJs.DialogFilterChatlist {
|
||||
const {
|
||||
emoticon,
|
||||
contacts,
|
||||
@ -253,6 +253,17 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
||||
? excludedChatIds.map(buildInputPeerFromLocalDb).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (folder.isChatList) {
|
||||
return new GramJs.DialogFilterChatlist({
|
||||
id: folder.id,
|
||||
title: folder.title,
|
||||
emoticon: emoticon || undefined,
|
||||
pinnedPeers,
|
||||
includePeers,
|
||||
hasMyInvites: folder.hasMyInvites,
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.DialogFilter({
|
||||
id: folder.id,
|
||||
title: folder.title,
|
||||
|
||||
@ -24,6 +24,12 @@ export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
|
||||
return getApiChatIdFromMtpPeer(mtpMessage.peerId);
|
||||
}
|
||||
|
||||
export function isChatFolder(
|
||||
filter?: GramJs.TypeDialogFilter,
|
||||
): filter is GramJs.DialogFilter | GramJs.DialogFilterChatlist {
|
||||
return filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist;
|
||||
}
|
||||
|
||||
export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) {
|
||||
const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`;
|
||||
|
||||
|
||||
@ -40,6 +40,7 @@ import {
|
||||
buildApiChatSettings,
|
||||
buildApiChatReactions,
|
||||
buildApiTopic,
|
||||
buildApiChatlistInvite, buildApiChatlistExportedInvite,
|
||||
} from '../apiBuilders/chats';
|
||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||
@ -56,7 +57,9 @@ import {
|
||||
buildInputPhoto,
|
||||
generateRandomBigInt,
|
||||
} from '../gramjsBuilders';
|
||||
import { addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb } from '../helpers';
|
||||
import {
|
||||
addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb, isChatFolder,
|
||||
} from '../helpers';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
import { buildApiPhoto } from '../apiBuilders/common';
|
||||
import { buildStickerSet } from '../apiBuilders/symbols';
|
||||
@ -816,7 +819,7 @@ export async function fetchChatFolders() {
|
||||
}
|
||||
|
||||
const defaultFolderPosition = result.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault);
|
||||
const dialogFilters = result.filter((df): df is GramJs.DialogFilter => df instanceof GramJs.DialogFilter);
|
||||
const dialogFilters = result.filter(isChatFolder);
|
||||
const orderedIds = dialogFilters.map(({ id }) => id);
|
||||
if (defaultFolderPosition !== -1) {
|
||||
orderedIds.splice(defaultFolderPosition, 0, ALL_FOLDER_ID);
|
||||
@ -1559,3 +1562,149 @@ export function editTopic({
|
||||
hidden: isHidden,
|
||||
}), true);
|
||||
}
|
||||
|
||||
export async function checkChatlistInvite({
|
||||
slug,
|
||||
}: {
|
||||
slug: string;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.chatlists.CheckChatlistInvite({
|
||||
slug,
|
||||
}));
|
||||
|
||||
const invite = buildApiChatlistInvite(result, slug);
|
||||
|
||||
if (!result || !invite) return undefined;
|
||||
|
||||
updateLocalDb(result);
|
||||
|
||||
return {
|
||||
invite,
|
||||
users: result.users.map(buildApiUser).filter(Boolean),
|
||||
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export function joinChatlistInvite({
|
||||
slug,
|
||||
peers,
|
||||
}: {
|
||||
slug: string;
|
||||
peers: ApiChat[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.chatlists.JoinChatlistInvite({
|
||||
slug,
|
||||
peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
|
||||
}), true, true);
|
||||
}
|
||||
|
||||
export async function fetchLeaveChatlistSuggestions({
|
||||
folderId,
|
||||
}: {
|
||||
folderId: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.chatlists.GetLeaveChatlistSuggestions({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
return result.map(getApiChatIdFromMtpPeer);
|
||||
}
|
||||
|
||||
export function leaveChatlist({
|
||||
folderId,
|
||||
peers,
|
||||
}: {
|
||||
folderId: number;
|
||||
peers: ApiChat[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.chatlists.LeaveChatlist({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
|
||||
}), true);
|
||||
}
|
||||
|
||||
export async function createChalistInvite({
|
||||
folderId, title, peers,
|
||||
}: {
|
||||
folderId: number;
|
||||
title?: string;
|
||||
peers: (ApiChat | ApiUser)[];
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.chatlists.ExportChatlistInvite({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
title: title || '',
|
||||
peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
|
||||
}), undefined, true);
|
||||
|
||||
if (!result || result.filter instanceof GramJs.DialogFilterDefault) return undefined;
|
||||
|
||||
return {
|
||||
filter: buildApiChatFolder(result.filter),
|
||||
invite: buildApiChatlistExportedInvite(result.invite),
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteChatlistInvite({
|
||||
folderId, slug,
|
||||
}: {
|
||||
folderId: number;
|
||||
slug: string;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.chatlists.DeleteExportedInvite({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
slug,
|
||||
}));
|
||||
}
|
||||
|
||||
export async function editChatlistInvite({
|
||||
folderId, slug, title, peers,
|
||||
}: {
|
||||
folderId: number;
|
||||
slug: string;
|
||||
title?: string;
|
||||
peers: (ApiChat | ApiUser)[];
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.chatlists.EditExportedInvite({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
slug,
|
||||
title,
|
||||
peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
|
||||
}));
|
||||
if (!result) return undefined;
|
||||
|
||||
return buildApiChatlistExportedInvite(result);
|
||||
}
|
||||
|
||||
export async function fetchChatlistInvites({
|
||||
folderId,
|
||||
}: {
|
||||
folderId: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.chatlists.GetExportedInvites({
|
||||
chatlist: new GramJs.InputChatlistDialogFilter({
|
||||
filterId: folderId,
|
||||
}),
|
||||
}));
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
updateLocalDb(result);
|
||||
|
||||
return {
|
||||
invites: result.invites.map(buildApiChatlistExportedInvite).filter(Boolean),
|
||||
users: result.users.map(buildApiUser).filter(Boolean),
|
||||
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@ -20,7 +20,9 @@ export {
|
||||
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||
getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest, fetchTopics, deleteTopic, togglePinnedTopic,
|
||||
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden,
|
||||
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite,
|
||||
joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites,
|
||||
fetchLeaveChatlistSuggestions, leaveChatlist,
|
||||
} from './chats';
|
||||
|
||||
export {
|
||||
|
||||
@ -48,6 +48,7 @@ import {
|
||||
serializeBytes,
|
||||
log,
|
||||
swapLocalInvoiceMedia,
|
||||
isChatFolder,
|
||||
} from './helpers';
|
||||
import {
|
||||
buildApiNotifyException,
|
||||
@ -579,7 +580,7 @@ export function updater(update: Update) {
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateDialogFilter) {
|
||||
const { id, filter } = update;
|
||||
const folder = filter instanceof GramJs.DialogFilter ? buildApiChatFolder(filter) : undefined;
|
||||
const folder = isChatFolder(filter) ? buildApiChatFolder(filter) : undefined;
|
||||
|
||||
onUpdate({
|
||||
'@type': 'updateChatFolder',
|
||||
|
||||
@ -187,6 +187,8 @@ export interface ApiChatFolder {
|
||||
pinnedChatIds?: string[];
|
||||
includedChatIds: string[];
|
||||
excludedChatIds: string[];
|
||||
isChatList?: true;
|
||||
hasMyInvites?: true;
|
||||
}
|
||||
|
||||
export interface ApiChatSettings {
|
||||
@ -222,3 +224,25 @@ export interface ApiTopic {
|
||||
|
||||
isMuted?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiChatlistInviteNew {
|
||||
title: string;
|
||||
emoticon?: string;
|
||||
peerIds: string[];
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export interface ApiChatlistInviteAlready {
|
||||
folderId: number;
|
||||
missingPeerIds: string[];
|
||||
alreadyPeerIds: string[];
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export type ApiChatlistInvite = ApiChatlistInviteNew | ApiChatlistInviteAlready;
|
||||
|
||||
export interface ApiChatlistExportedInvite {
|
||||
title: string;
|
||||
url: string;
|
||||
peerIds: string[];
|
||||
}
|
||||
|
||||
@ -153,6 +153,7 @@ export type ApiChatInviteImporter = {
|
||||
date: number;
|
||||
isRequested?: boolean;
|
||||
about?: string;
|
||||
isFromChatList?: boolean;
|
||||
};
|
||||
|
||||
export interface ApiCountry {
|
||||
|
||||
BIN
src/assets/tgs/settings/FoldersShare.tgs
Normal file
BIN
src/assets/tgs/settings/FoldersShare.tgs
Normal file
Binary file not shown.
@ -18,6 +18,8 @@ export { default as GiftPremiumModal } from '../components/main/premium/GiftPrem
|
||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||
|
||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||
export { default as CalendarModal } from '../components/common/CalendarModal';
|
||||
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';
|
||||
|
||||
27
src/components/common/InviteLink.module.scss
Normal file
27
src/components/common/InviteLink.module.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.primaryLink {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.moreMenu {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
width: auto;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
99
src/components/common/InviteLink.tsx
Normal file
99
src/components/common/InviteLink.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import React, { memo, useCallback, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import Button from '../ui/Button';
|
||||
|
||||
import styles from './InviteLink.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
title?: string;
|
||||
inviteLink: string;
|
||||
onRevoke?: VoidFunction;
|
||||
};
|
||||
|
||||
const InviteLink: FC<OwnProps> = ({
|
||||
title,
|
||||
inviteLink,
|
||||
onRevoke,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const { showNotification, openChatWithDraft } = getActions();
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const copyLink = useCallback((link: string) => {
|
||||
copyTextToClipboard(link);
|
||||
showNotification({
|
||||
message: lang('LinkCopied'),
|
||||
});
|
||||
}, [lang]);
|
||||
|
||||
const handleCopyPrimaryClicked = useCallback(() => {
|
||||
copyLink(inviteLink);
|
||||
}, [copyLink, inviteLink]);
|
||||
|
||||
const handleShare = useCallback(() => {
|
||||
openChatWithDraft({ text: inviteLink });
|
||||
}, [inviteLink]);
|
||||
|
||||
const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }) => (
|
||||
<Button
|
||||
round
|
||||
ripple={!isMobile}
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
className={isOpen ? 'active' : ''}
|
||||
onClick={onTrigger}
|
||||
ariaLabel="Actions"
|
||||
>
|
||||
<i className="icon icon-more" />
|
||||
</Button>
|
||||
);
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<div className="settings-item">
|
||||
<p className="text-muted">
|
||||
{lang(title || 'InviteLink.InviteLink')}
|
||||
</p>
|
||||
<div className={styles.primaryLink}>
|
||||
<input
|
||||
className={buildClassName('form-control', styles.input)}
|
||||
value={inviteLink}
|
||||
readOnly
|
||||
onClick={handleCopyPrimaryClicked}
|
||||
/>
|
||||
<DropdownMenu
|
||||
className={styles.moreMenu}
|
||||
trigger={PrimaryLinkMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="copy" onClick={handleCopyPrimaryClicked}>{lang('Copy')}</MenuItem>
|
||||
{onRevoke && (
|
||||
<MenuItem icon="delete" onClick={onRevoke} destructive>{lang('RevokeButton')}</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button onClick={handleCopyPrimaryClicked} className={styles.button}>
|
||||
{lang('FolderLinkScreen.LinkActionCopy')}
|
||||
</Button>
|
||||
<Button onClick={handleShare} className={styles.button}>
|
||||
{lang('FolderLinkScreen.LinkActionShare')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(InviteLink);
|
||||
@ -1,10 +1,14 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useRef, useEffect, memo,
|
||||
useCallback, useRef, useEffect, memo, useMemo,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { isUserId } from '../../global/helpers';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
@ -13,8 +17,6 @@ import ListItem from '../ui/ListItem';
|
||||
import PrivateChatInfo from './PrivateChatInfo';
|
||||
import GroupChatInfo from './GroupChatInfo';
|
||||
import PickerSelectedItem from './PickerSelectedItem';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
|
||||
@ -29,8 +31,11 @@ type OwnProps = {
|
||||
searchInputId?: string;
|
||||
isLoading?: boolean;
|
||||
noScrollRestore?: boolean;
|
||||
onSelectedIdsChange: (ids: string[]) => void;
|
||||
onFilterChange: (value: string) => void;
|
||||
isSearchable?: boolean;
|
||||
lockedIds?: string[];
|
||||
onSelectedIdsChange?: (ids: string[]) => void;
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string) => void;
|
||||
onLoadMore?: () => void;
|
||||
};
|
||||
|
||||
@ -49,8 +54,11 @@ const Picker: FC<OwnProps> = ({
|
||||
searchInputId,
|
||||
isLoading,
|
||||
noScrollRestore,
|
||||
isSearchable,
|
||||
lockedIds,
|
||||
onSelectedIdsChange,
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -58,53 +66,93 @@ const Picker: FC<OwnProps> = ({
|
||||
const shouldMinimize = selectedIds.length > MAX_FULL_ITEMS;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSearchable) return;
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
}, []);
|
||||
}, [isSearchable]);
|
||||
|
||||
const [lockedSelectedIds, unlockedSelectedIds] = useMemo(() => {
|
||||
if (!lockedIds?.length) return [MEMO_EMPTY_ARRAY, selectedIds];
|
||||
const unlockedIds = selectedIds.filter((id) => !lockedIds.includes(id));
|
||||
return [lockedIds, unlockedIds];
|
||||
}, [selectedIds, lockedIds]);
|
||||
|
||||
const lockedIdsSet = useMemo(() => new Set(lockedIds), [lockedIds]);
|
||||
|
||||
const sortedItemIds = useMemo(() => {
|
||||
return itemIds.sort((a, b) => {
|
||||
const aIsLocked = lockedIdsSet.has(a);
|
||||
const bIsLocked = lockedIdsSet.has(b);
|
||||
if (aIsLocked && !bIsLocked) {
|
||||
return -1;
|
||||
}
|
||||
if (!aIsLocked && bIsLocked) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [itemIds, lockedIdsSet]);
|
||||
|
||||
const handleItemClick = useCallback((id: string) => {
|
||||
const newSelectedIds = [...selectedIds];
|
||||
if (lockedIdsSet.has(id)) {
|
||||
onDisabledClick?.(id);
|
||||
return;
|
||||
}
|
||||
|
||||
const newSelectedIds = selectedIds.slice();
|
||||
if (newSelectedIds.includes(id)) {
|
||||
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
|
||||
} else {
|
||||
newSelectedIds.push(id);
|
||||
}
|
||||
onSelectedIdsChange(newSelectedIds);
|
||||
onFilterChange('');
|
||||
}, [selectedIds, onSelectedIdsChange, onFilterChange]);
|
||||
onSelectedIdsChange?.(newSelectedIds);
|
||||
onFilterChange?.('');
|
||||
}, [lockedIdsSet, selectedIds, onSelectedIdsChange, onFilterChange, onDisabledClick]);
|
||||
|
||||
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.currentTarget;
|
||||
onFilterChange(value);
|
||||
onFilterChange?.(value);
|
||||
}, [onFilterChange]);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, itemIds, Boolean(filterValue));
|
||||
const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, sortedItemIds, Boolean(filterValue));
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
return (
|
||||
<div className="Picker">
|
||||
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{selectedIds.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
chatOrUserId={id}
|
||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||
canClose
|
||||
onClick={handleItemClick}
|
||||
clickArg={id}
|
||||
{isSearchable && (
|
||||
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lockedSelectedIds.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
chatOrUserId={id}
|
||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||
onClick={handleItemClick}
|
||||
clickArg={id}
|
||||
/>
|
||||
))}
|
||||
{unlockedSelectedIds.map((id, i) => (
|
||||
<PickerSelectedItem
|
||||
chatOrUserId={id}
|
||||
isMinimized={
|
||||
shouldMinimize && i + lockedSelectedIds.length < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
|
||||
}
|
||||
canClose
|
||||
onClick={handleItemClick}
|
||||
clickArg={id}
|
||||
/>
|
||||
))}
|
||||
<InputText
|
||||
id={searchInputId}
|
||||
ref={inputRef}
|
||||
value={filterValue}
|
||||
onChange={handleFilterChange}
|
||||
placeholder={filterPlaceholder || lang('SelectChat')}
|
||||
/>
|
||||
))}
|
||||
<InputText
|
||||
id={searchInputId}
|
||||
ref={inputRef}
|
||||
value={filterValue}
|
||||
onChange={handleFilterChange}
|
||||
placeholder={filterPlaceholder || lang('SelectChat')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewportIds?.length ? (
|
||||
<InfiniteScroll
|
||||
@ -117,11 +165,13 @@ const Picker: FC<OwnProps> = ({
|
||||
<ListItem
|
||||
key={id}
|
||||
className="chat-item-clickable picker-list-item"
|
||||
disabled={lockedIdsSet.has(id)}
|
||||
allowDisabledClick={Boolean(onDisabledClick)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleItemClick(id)}
|
||||
ripple
|
||||
>
|
||||
<Checkbox label="" checked={selectedIds.includes(id)} />
|
||||
<Checkbox label="" disabled={lockedIdsSet.has(id)} checked={selectedIds.includes(id)} />
|
||||
{isUserId(id) ? (
|
||||
<PrivateChatInfo userId={id} />
|
||||
) : (
|
||||
|
||||
@ -5,6 +5,7 @@ import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs
|
||||
|
||||
import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||
import FoldersShare from '../../../assets/tgs/settings/FoldersShare.tgs';
|
||||
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||
@ -31,6 +32,7 @@ export const LOCAL_TGS_URLS = {
|
||||
MonkeyPeek,
|
||||
FoldersAll,
|
||||
FoldersNew,
|
||||
FoldersShare,
|
||||
DiscussionGroups,
|
||||
Lock,
|
||||
CameraFlip,
|
||||
|
||||
@ -6,10 +6,12 @@ import { getActions, withGlobal } from '../../global';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import { LeftColumnContent, SettingsScreens } from '../../types';
|
||||
import type { ReducerAction } from '../../hooks/useReducer';
|
||||
import type { FoldersActions } from '../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
import { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/windowEnvironment';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
import { selectTabState, selectCurrentChat, selectIsForumPanelOpen } from '../../global/selectors';
|
||||
import { selectCurrentChat, selectIsForumPanelOpen, selectTabState } from '../../global/selectors';
|
||||
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||
import { useResize } from '../../hooks/useResize';
|
||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||
@ -32,6 +34,7 @@ type StateProps = {
|
||||
currentUserId?: string;
|
||||
hasPasscode?: boolean;
|
||||
nextSettingsScreen?: SettingsScreens;
|
||||
nextFoldersAction?: ReducerAction<FoldersActions>;
|
||||
isChatOpen: boolean;
|
||||
isUpdateAvailable?: boolean;
|
||||
isForumPanelOpen?: boolean;
|
||||
@ -63,6 +66,7 @@ const LeftColumn: FC<StateProps> = ({
|
||||
currentUserId,
|
||||
hasPasscode,
|
||||
nextSettingsScreen,
|
||||
nextFoldersAction,
|
||||
isChatOpen,
|
||||
isUpdateAvailable,
|
||||
isForumPanelOpen,
|
||||
@ -115,6 +119,7 @@ const LeftColumn: FC<StateProps> = ({
|
||||
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||
function fullReset() {
|
||||
setContent(LeftColumnContent.ChatList);
|
||||
setSettingsScreen(SettingsScreens.Main);
|
||||
setContactsFilter('');
|
||||
setGlobalSearchClosing({ isClosing: true });
|
||||
resetChatCreation();
|
||||
@ -285,12 +290,17 @@ const LeftColumn: FC<StateProps> = ({
|
||||
setSettingsScreen(SettingsScreens.Folders);
|
||||
return;
|
||||
|
||||
case SettingsScreens.FoldersShare:
|
||||
setSettingsScreen(SettingsScreens.FoldersEditFolder);
|
||||
return;
|
||||
|
||||
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
||||
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
||||
setSettingsScreen(SettingsScreens.FoldersEditFolderFromChatList);
|
||||
return;
|
||||
|
||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||
case SettingsScreens.FoldersEditFolderInvites:
|
||||
setContent(LeftColumnContent.ChatList);
|
||||
setSettingsScreen(SettingsScreens.Main);
|
||||
return;
|
||||
@ -394,7 +404,11 @@ const LeftColumn: FC<StateProps> = ({
|
||||
setSettingsScreen(nextSettingsScreen);
|
||||
requestNextSettingsScreen({ screen: undefined });
|
||||
}
|
||||
}, [nextSettingsScreen, requestNextSettingsScreen]);
|
||||
|
||||
if (nextFoldersAction) {
|
||||
foldersDispatch(nextFoldersAction);
|
||||
}
|
||||
}, [foldersDispatch, nextFoldersAction, nextSettingsScreen, requestNextSettingsScreen]);
|
||||
|
||||
const {
|
||||
initResize, resetResize, handleMouseUp,
|
||||
@ -513,6 +527,7 @@ export default memo(withGlobal(
|
||||
shouldSkipHistoryAnimations,
|
||||
activeChatFolder,
|
||||
nextSettingsScreen,
|
||||
nextFoldersAction,
|
||||
} = tabState;
|
||||
const {
|
||||
leftColumnWidth,
|
||||
@ -538,6 +553,7 @@ export default memo(withGlobal(
|
||||
currentUserId,
|
||||
hasPasscode,
|
||||
nextSettingsScreen,
|
||||
nextFoldersAction,
|
||||
isChatOpen,
|
||||
isUpdateAvailable,
|
||||
isForumPanelOpen,
|
||||
|
||||
@ -12,7 +12,7 @@ import {
|
||||
selectIsForumPanelOpen,
|
||||
} from '../../../global/selectors';
|
||||
|
||||
import Badge from './Badge';
|
||||
import ChatBadge from './ChatBadge';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -31,7 +31,7 @@ const AvatarBadge: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
return chat && (
|
||||
<div className="avatar-badge-wrapper">
|
||||
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
|
||||
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -110,21 +110,21 @@
|
||||
color: var(--color-white) !important;
|
||||
}
|
||||
|
||||
.Badge:not(.pinned):not(.muted) {
|
||||
.ChatBadge:not(.pinned):not(.muted) {
|
||||
color: var(--color-chat-active);
|
||||
background: var(--color-white);
|
||||
}
|
||||
|
||||
.Badge:not(.pinned).muted {
|
||||
.ChatBadge:not(.pinned).muted {
|
||||
color: var(--color-white);
|
||||
background: #FFFFFF33;
|
||||
}
|
||||
|
||||
.avatar-badge-wrapper .Badge:not(.pinned) {
|
||||
.avatar-badge-wrapper .ChatBadge:not(.pinned) {
|
||||
--outline-color: transparent;
|
||||
}
|
||||
|
||||
.avatar-badge-wrapper .Badge:not(.pinned).muted {
|
||||
.avatar-badge-wrapper .ChatBadge:not(.pinned).muted {
|
||||
background: var(--color-gray);
|
||||
}
|
||||
}
|
||||
@ -204,11 +204,11 @@
|
||||
|
||||
--outline-color: var(--color-background);
|
||||
|
||||
.Badge {
|
||||
.ChatBadge {
|
||||
box-shadow: 0 0 0 2px var(--outline-color);
|
||||
}
|
||||
|
||||
.Badge-transition {
|
||||
.ChatBadge-transition {
|
||||
transition: opacity var(--layer-transition), transform var(--layer-transition);
|
||||
|
||||
body.no-page-transitions & {
|
||||
|
||||
@ -55,7 +55,7 @@ import ReportModal from '../../common/ReportModal';
|
||||
import FullNameTitle from '../../common/FullNameTitle';
|
||||
import ChatFolderModal from '../ChatFolderModal.async';
|
||||
import ChatCallStatus from './ChatCallStatus';
|
||||
import Badge from './Badge';
|
||||
import ChatBadge from './ChatBadge';
|
||||
import AvatarBadge from './AvatarBadge';
|
||||
|
||||
import './Chat.scss';
|
||||
@ -264,7 +264,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
<div className="subtitle">
|
||||
{renderSubtitle()}
|
||||
<Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
||||
<ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
||||
</div>
|
||||
</div>
|
||||
{shouldRenderDeleteModal && (
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.Badge-transition {
|
||||
.ChatBadge-transition {
|
||||
opacity: 1;
|
||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
|
||||
@ -16,15 +16,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Badge-wrapper {
|
||||
.ChatBadge-wrapper {
|
||||
display: flex;
|
||||
|
||||
.Badge {
|
||||
.ChatBadge {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.Badge {
|
||||
.ChatBadge {
|
||||
min-width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background: var(--color-gray);
|
||||
@ -9,7 +9,7 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import ShowTransition from '../../ui/ShowTransition';
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
|
||||
import './Badge.scss';
|
||||
import './ChatBadge.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chat: ApiChat;
|
||||
@ -21,7 +21,7 @@ type OwnProps = {
|
||||
forceHidden?: boolean;
|
||||
};
|
||||
|
||||
const Badge: FC<OwnProps> = ({
|
||||
const ChatBadge: FC<OwnProps> = ({
|
||||
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden,
|
||||
}) => {
|
||||
const {
|
||||
@ -58,7 +58,7 @@ const Badge: FC<OwnProps> = ({
|
||||
|
||||
const isUnread = Boolean(unreadCount || hasUnreadMark);
|
||||
const className = buildClassName(
|
||||
'Badge',
|
||||
'ChatBadge',
|
||||
shouldBeMuted && 'muted',
|
||||
!isUnread && isPinned && 'pinned',
|
||||
isUnread && 'unread',
|
||||
@ -66,19 +66,19 @@ const Badge: FC<OwnProps> = ({
|
||||
|
||||
function renderContent() {
|
||||
const unreadReactionsElement = unreadReactionsCount && (
|
||||
<div className={buildClassName('Badge reaction', shouldBeMuted && 'muted')}>
|
||||
<div className={buildClassName('ChatBadge reaction', shouldBeMuted && 'muted')}>
|
||||
<i className="icon icon-heart" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const unreadMentionsElement = unreadMentionsCount && (
|
||||
<div className="Badge mention">
|
||||
<div className="ChatBadge mention">
|
||||
<i className="icon icon-mention" />
|
||||
</div>
|
||||
);
|
||||
|
||||
const unopenedTopicElement = isTopicUnopened && (
|
||||
<div className={buildClassName('Badge unopened', shouldBeMuted && 'muted')} />
|
||||
<div className={buildClassName('ChatBadge unopened', shouldBeMuted && 'muted')} />
|
||||
);
|
||||
|
||||
const unreadCountElement = (hasUnreadMark || unreadCount) ? (
|
||||
@ -109,17 +109,17 @@ const Badge: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Badge-wrapper">
|
||||
<div className="ChatBadge-wrapper">
|
||||
{elements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ShowTransition isCustom className="Badge-transition" isOpen={isShown}>
|
||||
<ShowTransition isCustom className="ChatBadge-transition" isOpen={isShown}>
|
||||
{renderContent()}
|
||||
</ShowTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Badge);
|
||||
export default memo(ChatBadge);
|
||||
@ -2,20 +2,22 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChatFolder } from '../../../api/types';
|
||||
import type { LeftColumnContent, SettingsScreens } from '../../../types';
|
||||
import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../api/types';
|
||||
import type { SettingsScreens, LeftColumnContent } from '../../../types';
|
||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { TabWithProperties } from '../../ui/TabList';
|
||||
|
||||
import { ALL_FOLDER_ID } from '../../../config';
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { selectCanShareFolder, selectTabState } from '../../../global/selectors';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
@ -35,12 +37,14 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
chatFoldersById: Record<number, ApiChatFolder>;
|
||||
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
|
||||
orderedFolderIds?: number[];
|
||||
activeChatFolder: number;
|
||||
currentUserId?: string;
|
||||
lastSyncTime?: number;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
maxFolders: number;
|
||||
maxFolderInvites: number;
|
||||
hasArchivedChats?: boolean;
|
||||
archiveSettings: GlobalState['archiveSettings'];
|
||||
};
|
||||
@ -61,6 +65,8 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
shouldSkipHistoryAnimations,
|
||||
maxFolders,
|
||||
shouldHideFolderTabs,
|
||||
folderInvitesById,
|
||||
maxFolderInvites,
|
||||
hasArchivedChats,
|
||||
archiveSettings,
|
||||
}) => {
|
||||
@ -68,6 +74,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
loadChatFolders,
|
||||
setActiveChatFolder,
|
||||
openChat,
|
||||
openShareChatFolderModal,
|
||||
openDeleteChatFolderModal,
|
||||
openEditChatFolder,
|
||||
openLimitReachedModal,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -81,11 +91,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [lastSyncTime, loadChatFolders]);
|
||||
|
||||
const allChatsFolder = useMemo(() => {
|
||||
const allChatsFolder: ApiChatFolder = useMemo(() => {
|
||||
return {
|
||||
id: ALL_FOLDER_ID,
|
||||
title: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats'),
|
||||
};
|
||||
includedChatIds: MEMO_EMPTY_ARRAY,
|
||||
excludedChatIds: MEMO_EMPTY_ARRAY,
|
||||
} satisfies ApiChatFolder;
|
||||
}, [orderedFolderIds, lang]);
|
||||
|
||||
const displayedFolders = useMemo(() => {
|
||||
@ -110,18 +122,63 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return displayedFolders.map(({ id, title }, i) => {
|
||||
const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1;
|
||||
const global = getGlobal();
|
||||
|
||||
return ({
|
||||
return displayedFolders.map((folder, i) => {
|
||||
const { id, title } = folder;
|
||||
const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1;
|
||||
const canShareFolder = selectCanShareFolder(global, id);
|
||||
|
||||
const contextActions = [];
|
||||
|
||||
if (canShareFolder) {
|
||||
contextActions.push({
|
||||
title: lang('ChatList.ContextMenuShare'),
|
||||
icon: 'link',
|
||||
handler: () => {
|
||||
// Greater amount can be after premium downgrade
|
||||
if (folderInvitesById[id]?.length >= maxFolderInvites) {
|
||||
openLimitReachedModal({
|
||||
limit: 'chatlistInvites',
|
||||
});
|
||||
} else {
|
||||
openShareChatFolderModal({
|
||||
folderId: id,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (id !== ALL_FOLDER_ID) {
|
||||
contextActions.push({
|
||||
title: lang('FilterEdit'),
|
||||
icon: 'edit',
|
||||
handler: () => {
|
||||
openEditChatFolder({ folderId: id });
|
||||
},
|
||||
});
|
||||
|
||||
contextActions.push({
|
||||
title: lang('FilterDeleteItem'),
|
||||
icon: 'delete',
|
||||
destructive: true,
|
||||
handler: () => {
|
||||
openDeleteChatFolderModal({ folderId: id });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
badgeCount: folderCountersById[id]?.chatsCount,
|
||||
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
|
||||
isBlocked,
|
||||
});
|
||||
contextActions: contextActions?.length ? contextActions : undefined,
|
||||
} satisfies TabWithProperties;
|
||||
});
|
||||
}, [displayedFolders, folderCountersById, maxFolders]);
|
||||
}, [displayedFolders, folderCountersById, lang, maxFolders, folderInvitesById, maxFolderInvites]);
|
||||
|
||||
const handleSwitchTab = useCallback((index: number) => {
|
||||
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
|
||||
@ -236,7 +293,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
>
|
||||
{shouldRenderFolders ? (
|
||||
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} areFolders />
|
||||
<TabList
|
||||
contextRootElementSelector="#LeftColumn"
|
||||
tabs={folderTabs}
|
||||
activeTab={activeChatFolder}
|
||||
onSwitchTab={handleSwitchTab}
|
||||
areFolders
|
||||
/>
|
||||
) : shouldRenderPlaceholder ? (
|
||||
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
||||
) : undefined}
|
||||
@ -258,6 +321,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
chatFolders: {
|
||||
byId: chatFoldersById,
|
||||
orderedIds: orderedFolderIds,
|
||||
invites: folderInvitesById,
|
||||
},
|
||||
chats: {
|
||||
listIds: {
|
||||
@ -272,6 +336,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
chatFoldersById,
|
||||
folderInvitesById,
|
||||
orderedFolderIds,
|
||||
activeChatFolder,
|
||||
currentUserId,
|
||||
@ -279,6 +344,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
shouldSkipHistoryAnimations,
|
||||
hasArchivedChats: Boolean(archived?.length),
|
||||
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
||||
maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'),
|
||||
archiveSettings,
|
||||
};
|
||||
},
|
||||
|
||||
@ -35,7 +35,7 @@ import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||
import Badge from './Badge';
|
||||
import ChatBadge from './ChatBadge';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import TopicIcon from '../../common/TopicIcon';
|
||||
|
||||
@ -179,7 +179,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
<div className="subtitle">
|
||||
{renderSubtitle()}
|
||||
<Badge
|
||||
<ChatBadge
|
||||
chat={chat}
|
||||
isPinned={isPinned}
|
||||
isMuted={isMuted}
|
||||
|
||||
@ -112,6 +112,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
||||
filterPlaceholder={lang('SendMessageTo')}
|
||||
searchInputId="new-group-picker-search"
|
||||
isLoading={isSearching}
|
||||
isSearchable
|
||||
onSelectedIdsChange={onSelectedMemberIdsChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
/>
|
||||
|
||||
@ -380,3 +380,11 @@
|
||||
margin-inline-end: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-item-chatlist {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.settings-item-chatlist .ListItem {
|
||||
margin: inherit;
|
||||
}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { SettingsScreens } from '../../../types';
|
||||
import type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
import { LAYERS_ANIMATION_NAME } from '../../../util/windowEnvironment';
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import useTwoFaReducer from '../../../hooks/reducers/useTwoFaReducer';
|
||||
|
||||
import Transition from '../../ui/Transition';
|
||||
@ -67,10 +69,12 @@ const FOLDERS_SCREENS = [
|
||||
SettingsScreens.FoldersCreateFolder,
|
||||
SettingsScreens.FoldersEditFolder,
|
||||
SettingsScreens.FoldersEditFolderFromChatList,
|
||||
SettingsScreens.FoldersEditFolderInvites,
|
||||
SettingsScreens.FoldersIncludedChats,
|
||||
SettingsScreens.FoldersIncludedChatsFromChatList,
|
||||
SettingsScreens.FoldersExcludedChats,
|
||||
SettingsScreens.FoldersExcludedChatsFromChatList,
|
||||
SettingsScreens.FoldersShare,
|
||||
];
|
||||
|
||||
const PRIVACY_SCREENS = [
|
||||
@ -137,11 +141,18 @@ const Settings: FC<OwnProps> = ({
|
||||
onReset,
|
||||
shouldSkipTransition,
|
||||
}) => {
|
||||
const { closeShareChatFolderModal } = getActions();
|
||||
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
|
||||
const [privacyPasscode, setPrivacyPasscode] = useState<string>('');
|
||||
|
||||
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||
if (forceReturnToChatList === true) {
|
||||
const isFromSettings = selectTabState(getGlobal()).shareFolderScreen?.isFromSettings;
|
||||
|
||||
if (currentScreen === SettingsScreens.FoldersShare) {
|
||||
closeShareChatFolderModal();
|
||||
}
|
||||
|
||||
if (forceReturnToChatList === true || (isFromSettings !== undefined && !isFromSettings)) {
|
||||
onReset(true);
|
||||
return;
|
||||
}
|
||||
@ -150,6 +161,7 @@ const Settings: FC<OwnProps> = ({
|
||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolderInvites
|
||||
) {
|
||||
setTimeout(() => {
|
||||
foldersDispatch({ type: 'reset' });
|
||||
@ -361,10 +373,12 @@ const Settings: FC<OwnProps> = ({
|
||||
case SettingsScreens.FoldersCreateFolder:
|
||||
case SettingsScreens.FoldersEditFolder:
|
||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||
case SettingsScreens.FoldersEditFolderInvites:
|
||||
case SettingsScreens.FoldersIncludedChats:
|
||||
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
||||
case SettingsScreens.FoldersExcludedChats:
|
||||
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
||||
case SettingsScreens.FoldersShare:
|
||||
return (
|
||||
<SettingsFolders
|
||||
currentScreen={currentScreen}
|
||||
|
||||
@ -201,8 +201,11 @@ const SettingsHeader: FC<OwnProps> = ({
|
||||
return <h3>{lang('Filters')}</h3>;
|
||||
case SettingsScreens.FoldersCreateFolder:
|
||||
return <h3>{lang('FilterNew')}</h3>;
|
||||
case SettingsScreens.FoldersShare:
|
||||
return <h3>{lang('FolderLinkScreen.Title')}</h3>;
|
||||
case SettingsScreens.FoldersEditFolder:
|
||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||
case SettingsScreens.FoldersEditFolderInvites:
|
||||
return (
|
||||
<div className="settings-main-header">
|
||||
<h3>{lang('FilterEdit')}</h3>
|
||||
|
||||
@ -107,6 +107,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')}
|
||||
searchInputId="new-group-picker-search"
|
||||
isSearchable
|
||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
/>
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import type { ApiChatFolder } from '../../../../api/types';
|
||||
import { SettingsScreens } from '../../../../types';
|
||||
|
||||
import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
|
||||
|
||||
import SettingsFoldersMain from './SettingsFoldersMain';
|
||||
import SettingsFoldersEdit from './SettingsFoldersEdit';
|
||||
import SettingsFoldersChatFilters from './SettingsFoldersChatFilters';
|
||||
import SettingsShareChatlist from './SettingsShareChatlist';
|
||||
|
||||
import './SettingsFolders.scss';
|
||||
|
||||
@ -33,11 +34,14 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
onScreenSelect,
|
||||
onReset,
|
||||
}) => {
|
||||
const { openShareChatFolderModal } = getActions();
|
||||
|
||||
const handleReset = useCallback(() => {
|
||||
if (
|
||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
||||
|| currentScreen === SettingsScreens.FoldersEditFolderInvites
|
||||
) {
|
||||
setTimeout(() => {
|
||||
dispatch({ type: 'reset' });
|
||||
@ -86,6 +90,16 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
: SettingsScreens.FoldersExcludedChats);
|
||||
}, [currentScreen, dispatch, onScreenSelect]);
|
||||
|
||||
const handleShareFolder = useCallback(() => {
|
||||
openShareChatFolderModal({ folderId: state.folderId!, noRequestNextScreen: true });
|
||||
onScreenSelect(SettingsScreens.FoldersShare);
|
||||
}, [onScreenSelect, state.folderId]);
|
||||
|
||||
const handleOpenInvite = useCallback((url: string) => {
|
||||
openShareChatFolderModal({ folderId: state.folderId!, url, noRequestNextScreen: true });
|
||||
onScreenSelect(SettingsScreens.FoldersShare);
|
||||
}, [onScreenSelect, state.folderId]);
|
||||
|
||||
switch (currentScreen) {
|
||||
case SettingsScreens.Folders:
|
||||
return (
|
||||
@ -104,17 +118,21 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
case SettingsScreens.FoldersCreateFolder:
|
||||
case SettingsScreens.FoldersEditFolder:
|
||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||
case SettingsScreens.FoldersEditFolderInvites:
|
||||
return (
|
||||
<SettingsFoldersEdit
|
||||
state={state}
|
||||
dispatch={dispatch}
|
||||
onAddIncludedChats={handleAddIncludedChats}
|
||||
onAddExcludedChats={handleAddExcludedChats}
|
||||
onShareFolder={handleShareFolder}
|
||||
onOpenInvite={handleOpenInvite}
|
||||
onReset={handleReset}
|
||||
isActive={isActive || [
|
||||
SettingsScreens.FoldersIncludedChats,
|
||||
SettingsScreens.FoldersExcludedChats,
|
||||
].includes(shownScreen)}
|
||||
isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites}
|
||||
onBack={onReset}
|
||||
/>
|
||||
);
|
||||
@ -141,6 +159,14 @@ const SettingsFolders: FC<OwnProps> = ({
|
||||
/>
|
||||
);
|
||||
|
||||
case SettingsScreens.FoldersShare:
|
||||
return (
|
||||
<SettingsShareChatlist
|
||||
isActive={isActive}
|
||||
onReset={handleReset}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -43,6 +43,8 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
||||
const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID);
|
||||
const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
||||
|
||||
const shouldHideChatTypes = state.folder.isChatList;
|
||||
|
||||
const displayedIds = useMemo(() => {
|
||||
// No need for expensive global updates on chats, so we avoid them
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
@ -116,6 +118,7 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
||||
selectedIds={selectedChatIds}
|
||||
selectedChatTypes={selectedChatTypes}
|
||||
filterValue={chatFilter}
|
||||
shouldHideChatTypes={shouldHideChatTypes}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
onSelectedChatTypesChange={handleSelectedChatTypesChange}
|
||||
onFilterChange={handleFilterChange}
|
||||
|
||||
@ -34,6 +34,7 @@ type OwnProps = {
|
||||
selectedIds: string[];
|
||||
selectedChatTypes: string[];
|
||||
filterValue?: string;
|
||||
shouldHideChatTypes?: boolean;
|
||||
onSelectedIdsChange: (ids: string[]) => void;
|
||||
onSelectedChatTypesChange: (types: string[]) => void;
|
||||
onFilterChange: (value: string) => void;
|
||||
@ -55,6 +56,7 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
|
||||
selectedIds,
|
||||
selectedChatTypes,
|
||||
filterValue,
|
||||
shouldHideChatTypes,
|
||||
onSelectedIdsChange,
|
||||
onSelectedChatTypesChange,
|
||||
onFilterChange,
|
||||
@ -200,11 +202,15 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{(!viewportIds || !viewportIds.length || viewportIds.includes(chatIds[0])) && (
|
||||
<div key="header">
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterChatTypes')}
|
||||
</h4>
|
||||
{chatTypes.map(renderChatType)}
|
||||
<div className="picker-list-divider" />
|
||||
{!shouldHideChatTypes && (
|
||||
<>
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterChatTypes')}
|
||||
</h4>
|
||||
{chatTypes.map(renderChatType)}
|
||||
<div className="picker-list-divider" />
|
||||
</>
|
||||
)}
|
||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterChats')}
|
||||
</h4>
|
||||
|
||||
@ -2,12 +2,13 @@ import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiChatFolder } from '../../../../api/types';
|
||||
import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../../api/types';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
||||
import { isUserId } from '../../../../global/helpers';
|
||||
import type {
|
||||
@ -19,6 +20,8 @@ import {
|
||||
INCLUDED_CHAT_TYPES,
|
||||
selectChatFilters,
|
||||
} from '../../../../hooks/reducers/useFoldersReducer';
|
||||
import { selectCanShareFolder } from '../../../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../../../global/selectors/limits';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
|
||||
@ -34,9 +37,12 @@ import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
type OwnProps = {
|
||||
state: FoldersState;
|
||||
dispatch: FolderEditDispatch;
|
||||
onAddIncludedChats: () => void;
|
||||
onAddExcludedChats: () => void;
|
||||
onAddIncludedChats: VoidFunction;
|
||||
onAddExcludedChats: VoidFunction;
|
||||
onShareFolder: VoidFunction;
|
||||
onOpenInvite: (url: string) => void;
|
||||
isActive?: boolean;
|
||||
isOnlyInvites?: boolean;
|
||||
onReset: () => void;
|
||||
onBack: () => void;
|
||||
};
|
||||
@ -44,7 +50,9 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
loadedActiveChatIds?: string[];
|
||||
loadedArchivedChatIds?: string[];
|
||||
invites?: ApiChatlistExportedInvite[];
|
||||
isRemoved?: boolean;
|
||||
maxInviteLinks: number;
|
||||
};
|
||||
|
||||
const SUBMIT_TIMEOUT = 500;
|
||||
@ -59,18 +67,29 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
dispatch,
|
||||
onAddIncludedChats,
|
||||
onAddExcludedChats,
|
||||
onShareFolder,
|
||||
onOpenInvite,
|
||||
isActive,
|
||||
onReset,
|
||||
isRemoved,
|
||||
onBack,
|
||||
loadedActiveChatIds,
|
||||
isOnlyInvites,
|
||||
loadedArchivedChatIds,
|
||||
invites,
|
||||
maxInviteLinks,
|
||||
}) => {
|
||||
const {
|
||||
editChatFolder,
|
||||
addChatFolder,
|
||||
loadChatlistInvites,
|
||||
openLimitReachedModal,
|
||||
showNotification,
|
||||
} = getActions();
|
||||
|
||||
const isCreating = state.mode === 'create';
|
||||
const isEditingChatList = state.folder.isChatList;
|
||||
|
||||
const [isIncludedChatsListExpanded, setIsIncludedChatsListExpanded] = useState(false);
|
||||
const [isExcludedChatsListExpanded, setIsExcludedChatsListExpanded] = useState(false);
|
||||
|
||||
@ -80,6 +99,12 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isRemoved, onReset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive && state.folderId && state.folder.isChatList) {
|
||||
loadChatlistInvites({ folderId: state.folderId });
|
||||
}
|
||||
}, [isActive, state.folder.isChatList, state.folderId]);
|
||||
|
||||
const {
|
||||
selectedChatIds: includedChatIds,
|
||||
selectedChatTypes: includedChatTypes,
|
||||
@ -129,7 +154,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });
|
||||
}, [dispatch]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const handleSaveFolder = useCallback((cb?: NoneToVoidFunction) => {
|
||||
const { title } = state.folder;
|
||||
|
||||
if (!title) {
|
||||
@ -142,17 +167,52 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({ type: 'setIsLoading', payload: true });
|
||||
if (state.mode === 'edit') {
|
||||
if (!isCreating) {
|
||||
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
|
||||
} else {
|
||||
addChatFolder({ folder: state.folder as ApiChatFolder });
|
||||
}
|
||||
cb?.();
|
||||
}, [dispatch, includedChatIds.length, includedChatTypes, isCreating, state.folder, state.folderId]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
handleSaveFolder();
|
||||
|
||||
dispatch({ type: 'setIsLoading', payload: true });
|
||||
setTimeout(() => {
|
||||
onReset();
|
||||
}, SUBMIT_TIMEOUT);
|
||||
}, [addChatFolder, dispatch, editChatFolder, includedChatIds.length, includedChatTypes, onReset, state]);
|
||||
}, [dispatch, handleSaveFolder, onReset]);
|
||||
|
||||
const handleCreateInviteClick = useCallback(() => {
|
||||
if (!invites) return;
|
||||
|
||||
// Ignoring global updates is a known drawback here
|
||||
if (!selectCanShareFolder(getGlobal(), state.folderId!)) {
|
||||
showNotification({ message: lang('ChatList.Filter.InviteLink.IncludeExcludeError') });
|
||||
return;
|
||||
}
|
||||
|
||||
if (invites.length < maxInviteLinks) {
|
||||
if (state.isTouched) {
|
||||
handleSaveFolder(onShareFolder);
|
||||
} else {
|
||||
onShareFolder();
|
||||
}
|
||||
} else {
|
||||
openLimitReachedModal({
|
||||
limit: 'chatlistInvites',
|
||||
});
|
||||
}
|
||||
}, [handleSaveFolder, invites, lang, maxInviteLinks, onShareFolder, state.folderId, state.isTouched]);
|
||||
|
||||
const handleEditInviteClick = useCallback((e: React.MouseEvent<HTMLElement>, url: string) => {
|
||||
if (state.isTouched) {
|
||||
handleSaveFolder(() => onOpenInvite(url));
|
||||
} else {
|
||||
onOpenInvite(url);
|
||||
}
|
||||
}, [handleSaveFolder, onOpenInvite, state.isTouched]);
|
||||
|
||||
function renderChatType(key: string, mode: 'included' | 'excluded') {
|
||||
const chatType = mode === 'included'
|
||||
@ -226,7 +286,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
className="settings-content-icon"
|
||||
/>
|
||||
|
||||
{state.mode === 'create' && (
|
||||
{isCreating && (
|
||||
<p className="settings-item-description mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FilterIncludeInfo')}
|
||||
</p>
|
||||
@ -241,39 +301,76 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="settings-item no-border pt-3">
|
||||
{state.error && state.error === ERROR_NO_CHATS && (
|
||||
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang(state.error)}
|
||||
</p>
|
||||
)}
|
||||
{!isOnlyInvites && (
|
||||
<div className="settings-item pt-3">
|
||||
{state.error && state.error === ERROR_NO_CHATS && (
|
||||
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang(state.error)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
icon="add"
|
||||
onClick={onAddIncludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
</ListItem>
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
icon="add"
|
||||
onClick={onAddIncludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
</ListItem>
|
||||
|
||||
{renderChats('included')}
|
||||
</div>
|
||||
{renderChats('included')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
||||
{!isOnlyInvites && !isEditingChatList && (
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
icon="add"
|
||||
onClick={onAddExcludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
</ListItem>
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
icon="add"
|
||||
onClick={onAddExcludedChats}
|
||||
>
|
||||
{lang('FilterAddChats')}
|
||||
</ListItem>
|
||||
|
||||
{renderChats('excluded')}
|
||||
</div>
|
||||
{renderChats('excluded')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCreating && (
|
||||
<div className="settings-item pt-3">
|
||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('FolderLinkScreen.Title')}
|
||||
</h4>
|
||||
|
||||
<ListItem
|
||||
className="settings-folders-list-item color-primary mb-0"
|
||||
icon="add"
|
||||
onClick={handleCreateInviteClick}
|
||||
>
|
||||
{lang('ChatListFilter.CreateLinkNew')}
|
||||
</ListItem>
|
||||
|
||||
{invites?.map((invite) => (
|
||||
<ListItem
|
||||
className="settings-folders-list-item mb-0"
|
||||
icon="link"
|
||||
multiline
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={handleEditInviteClick}
|
||||
clickArg={invite.url}
|
||||
>
|
||||
<span className="title" dir="auto">{invite.title || invite.url}</span>
|
||||
<span className="subtitle">
|
||||
{lang('ChatListFilter.LinkLabelChatCount', invite.peerIds.length, 'i')}
|
||||
</span>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<FloatingActionButton
|
||||
@ -295,12 +392,14 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { state }): StateProps => {
|
||||
const { listIds } = global.chats;
|
||||
const { byId } = global.chatFolders;
|
||||
const { byId, invites } = global.chatFolders;
|
||||
|
||||
return {
|
||||
loadedActiveChatIds: listIds.active,
|
||||
loadedArchivedChatIds: listIds.archived,
|
||||
invites: state.folderId ? (invites[state.folderId] || MEMO_EMPTY_ARRAY) : undefined,
|
||||
isRemoved: state.folderId !== undefined && !byId[state.folderId],
|
||||
maxInviteLinks: selectCurrentLimit(global, 'chatlistInvites'),
|
||||
};
|
||||
},
|
||||
)(SettingsFoldersEdit));
|
||||
|
||||
@ -139,6 +139,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
id: folder.id,
|
||||
title: folder.title,
|
||||
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
||||
isChatList: folder.isChatList,
|
||||
};
|
||||
});
|
||||
}, [folderIds, foldersById, lang, chatsCountByFolderId]);
|
||||
@ -296,7 +297,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
{renderText(folder.title, ['emoji'])}
|
||||
{isBlocked && <i className="icon icon-lock-badge settings-folders-blocked-icon" />}
|
||||
</span>
|
||||
<span className="subtitle">{folder.subtitle}</span>
|
||||
<span className="subtitle">
|
||||
{folder.isChatList && <i className="icon icon-link mr-1" />}
|
||||
{folder.subtitle}
|
||||
</span>
|
||||
</ListItem>
|
||||
</Draggable>
|
||||
);
|
||||
|
||||
201
src/components/left/settings/folders/SettingsShareChatlist.tsx
Normal file
201
src/components/left/settings/folders/SettingsShareChatlist.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
|
||||
import { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
import { partition } from '../../../../util/iteratees';
|
||||
import {
|
||||
selectCanInviteToChat, selectChat,
|
||||
selectChatFolder,
|
||||
selectTabState, selectUser,
|
||||
} from '../../../../global/selectors';
|
||||
import { isChatChannel, isUserBot } from '../../../../global/helpers';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
|
||||
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import InviteLink from '../../../common/InviteLink';
|
||||
import Picker from '../../../common/Picker';
|
||||
import Spinner from '../../../ui/Spinner';
|
||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
onReset: VoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
folderId?: number;
|
||||
title?: string;
|
||||
includedChatIds?: string[];
|
||||
pinnedChatIds?: string[];
|
||||
peerIds?: string[];
|
||||
url?: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
onReset,
|
||||
folderId,
|
||||
title,
|
||||
includedChatIds,
|
||||
pinnedChatIds,
|
||||
peerIds,
|
||||
url,
|
||||
isLoading,
|
||||
}) => {
|
||||
const {
|
||||
createChatlistInvite, deleteChatlistInvite, editChatlistInvite, showNotification,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const [isTouched, setIsTouched] = useState(false);
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onReset,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
setIsTouched(false);
|
||||
}
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!url && folderId) {
|
||||
createChatlistInvite({ folderId });
|
||||
}
|
||||
}, [folderId, url]);
|
||||
|
||||
const handleRevoke = useCallback(() => {
|
||||
if (!url || !folderId) return;
|
||||
|
||||
deleteChatlistInvite({ folderId, url });
|
||||
onReset();
|
||||
}, [folderId, onReset, url]);
|
||||
|
||||
const itemIds = useMemo(() => {
|
||||
return (includedChatIds || []).concat(pinnedChatIds || []);
|
||||
}, [includedChatIds, pinnedChatIds]);
|
||||
|
||||
const [unlockedIds, lockedIds] = useMemo(() => {
|
||||
const global = getGlobal();
|
||||
return partition(itemIds, (id) => selectCanInviteToChat(global, id));
|
||||
}, [itemIds]);
|
||||
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(peerIds || []);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsLoading]) => {
|
||||
if (isLoading && !prevIsLoading) {
|
||||
setSelectedIds(unlockedIds);
|
||||
} else if (peerIds) {
|
||||
setSelectedIds(peerIds);
|
||||
}
|
||||
}, [isLoading, unlockedIds, peerIds]);
|
||||
|
||||
const handleClickDisabled = useCallback((id: string) => {
|
||||
const global = getGlobal();
|
||||
const user = selectUser(global, id);
|
||||
const chat = selectChat(global, id);
|
||||
if (user && isUserBot(user)) {
|
||||
showNotification({
|
||||
message: lang('FolderLinkScreen.AlertTextUnavailableBot'),
|
||||
});
|
||||
} else if (user) {
|
||||
showNotification({
|
||||
message: lang('FolderLinkScreen.AlertTextUnavailableUser'),
|
||||
});
|
||||
} else if (chat && isChatChannel(chat)) {
|
||||
showNotification({
|
||||
message: lang('FolderLinkScreen.AlertTextUnavailablePublicChannel'),
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
message: lang('FolderLinkScreen.AlertTextUnavailablePublicGroup'),
|
||||
});
|
||||
}
|
||||
}, [lang]);
|
||||
|
||||
const handleSelectedIdsChange = useCallback((ids: string[]) => {
|
||||
setSelectedIds(ids);
|
||||
setIsTouched(true);
|
||||
}, []);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!folderId || !url) return;
|
||||
editChatlistInvite({ folderId, peerIds: selectedIds, url });
|
||||
}, [folderId, selectedIds, url]);
|
||||
|
||||
const chatsCount = selectedIds.length;
|
||||
|
||||
return (
|
||||
<div className="settings-content no-border custom-scroll">
|
||||
<div className="settings-content-header">
|
||||
<AnimatedIcon
|
||||
size={STICKER_SIZE_FOLDER_SETTINGS}
|
||||
tgsUrl={LOCAL_TGS_URLS.FoldersShare}
|
||||
className="settings-content-icon"
|
||||
/>
|
||||
|
||||
<p className="settings-item-description mb-3" dir="auto">
|
||||
{renderText(lang('FolderLinkScreen.TitleDescriptionSelected', [title, chatsCount]),
|
||||
['simple_markdown'])}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InviteLink
|
||||
inviteLink={isLoading ? lang('Loading') : url!}
|
||||
onRevoke={handleRevoke}
|
||||
/>
|
||||
|
||||
<div className="settings-item settings-item-chatlist">
|
||||
<Picker
|
||||
itemIds={itemIds}
|
||||
lockedIds={lockedIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
selectedIds={selectedIds}
|
||||
onDisabledClick={handleClickDisabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FloatingActionButton
|
||||
isShown={isLoading || isTouched}
|
||||
disabled={isLoading}
|
||||
onClick={handleSubmit}
|
||||
ariaLabel="Save changes"
|
||||
>
|
||||
{isLoading ? (
|
||||
<Spinner color="white" />
|
||||
) : (
|
||||
<i className="icon icon-check" />
|
||||
)}
|
||||
</FloatingActionButton>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { folderId, url, isLoading } = selectTabState(global).shareFolderScreen || {};
|
||||
const folder = folderId ? selectChatFolder(global, folderId) : undefined;
|
||||
const invite = folderId ? global.chatFolders.invites[folderId]?.find((i) => i.url === url) : undefined;
|
||||
|
||||
return {
|
||||
folderId,
|
||||
title: folder?.title,
|
||||
includedChatIds: folder?.includedChatIds,
|
||||
pinnedChatIds: folder?.pinnedChatIds,
|
||||
url,
|
||||
isLoading,
|
||||
peerIds: invite?.peerIds,
|
||||
};
|
||||
},
|
||||
)(SettingsShareChatlist));
|
||||
@ -7,8 +7,8 @@ import type { OwnProps } from './DeleteFolderDialog';
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const DeleteFolderDialogAsync: FC<OwnProps> = (props) => {
|
||||
const { deleteFolderDialogId } = props;
|
||||
const DeleteFolderDialog = useModuleLoader(Bundles.Extra, 'DeleteFolderDialog', !deleteFolderDialogId);
|
||||
const { folder } = props;
|
||||
const DeleteFolderDialog = useModuleLoader(Bundles.Extra, 'DeleteFolderDialog', !folder);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return DeleteFolderDialog ? <DeleteFolderDialog {...props} /> : undefined;
|
||||
|
||||
@ -1,31 +1,43 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { ApiChatFolder } from '../../api/types';
|
||||
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
|
||||
export type OwnProps = {
|
||||
deleteFolderDialogId?: number;
|
||||
folder?: ApiChatFolder;
|
||||
};
|
||||
|
||||
const DeleteFolderDialog: FC<OwnProps> = ({
|
||||
deleteFolderDialogId,
|
||||
folder,
|
||||
}) => {
|
||||
const { closeDeleteChatFolderModal, deleteChatFolder } = getActions();
|
||||
const { closeDeleteChatFolderModal, deleteChatFolder, openDeleteChatFolderModal } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(folder);
|
||||
|
||||
const renderingFolder = usePrevious(folder) || folder;
|
||||
const isMyChatlist = renderingFolder?.hasMyInvites;
|
||||
|
||||
const handleDeleteFolderMessage = useCallback(() => {
|
||||
closeDeleteChatFolderModal();
|
||||
deleteChatFolder({ id: deleteFolderDialogId! });
|
||||
}, [closeDeleteChatFolderModal, deleteChatFolder, deleteFolderDialogId]);
|
||||
if (isMyChatlist) {
|
||||
openDeleteChatFolderModal({ folderId: renderingFolder!.id, isConfirmedForChatlist: true });
|
||||
} else {
|
||||
deleteChatFolder({ id: renderingFolder!.id });
|
||||
}
|
||||
}, [isMyChatlist, renderingFolder]);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
isOpen={deleteFolderDialogId !== undefined}
|
||||
isOpen={isOpen}
|
||||
onClose={closeDeleteChatFolderModal}
|
||||
text={lang('FilterDeleteAlert')}
|
||||
text={isMyChatlist ? lang('FilterDeleteAlertLinks') : lang('FilterDeleteAlert')}
|
||||
confirmLabel={lang('Delete')}
|
||||
confirmHandler={handleDeleteFolderMessage}
|
||||
confirmIsDestructive
|
||||
|
||||
@ -9,7 +9,10 @@ import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
import type { LangCode } from '../../types';
|
||||
import type {
|
||||
ApiAttachBot,
|
||||
ApiChat, ApiMessage, ApiUser,
|
||||
ApiChat,
|
||||
ApiChatFolder,
|
||||
ApiMessage,
|
||||
ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||
|
||||
@ -31,6 +34,7 @@ import {
|
||||
selectIsReactionPickerOpen,
|
||||
selectPerformanceSettingsValue,
|
||||
selectCanAnimateInterface,
|
||||
selectChatFolder,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||
@ -86,6 +90,7 @@ import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
||||
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
||||
|
||||
import './Main.scss';
|
||||
|
||||
@ -132,11 +137,12 @@ type StateProps = {
|
||||
currentUser?: ApiUser;
|
||||
urlAuth?: TabState['urlAuth'];
|
||||
limitReached?: ApiLimitTypeWithModal;
|
||||
deleteFolderDialogId?: number;
|
||||
deleteFolderDialog?: ApiChatFolder;
|
||||
isPaymentModalOpen?: boolean;
|
||||
isReceiptModalOpen?: boolean;
|
||||
isReactionPickerOpen: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
chatlistModal?: TabState['chatlistModal'];
|
||||
noRightColumnAnimation?: boolean;
|
||||
withInterfaceAnimations?: boolean;
|
||||
};
|
||||
@ -191,8 +197,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isReceiptModalOpen,
|
||||
isReactionPickerOpen,
|
||||
isCurrentUserPremium,
|
||||
deleteFolderDialogId,
|
||||
deleteFolderDialog,
|
||||
isMasterTab,
|
||||
chatlistModal,
|
||||
noRightColumnAnimation,
|
||||
}) => {
|
||||
const {
|
||||
@ -509,6 +516,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
userId={newContactUserId}
|
||||
isByPhoneNumber={newContactByPhoneNumber}
|
||||
/>
|
||||
<ChatlistModal info={chatlistModal} />
|
||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||
<WebAppModal webApp={webApp} />
|
||||
<DownloadManager />
|
||||
@ -528,7 +536,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
<PremiumLimitReachedModal limit={limitReached} />
|
||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||
<DeleteFolderDialog deleteFolderDialogId={deleteFolderDialogId} />
|
||||
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
||||
</div>
|
||||
);
|
||||
@ -569,6 +577,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
payment,
|
||||
limitReachedModal,
|
||||
deleteFolderDialogModal,
|
||||
chatlistModal,
|
||||
} = selectTabState(global);
|
||||
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
@ -582,6 +591,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const noRightColumnAnimation = !selectPerformanceSettingsValue(global, 'rightColumnAnimations')
|
||||
|| !selectCanAnimateInterface(global);
|
||||
|
||||
const deleteFolderDialog = deleteFolderDialogModal ? selectChatFolder(global, deleteFolderDialogModal) : undefined;
|
||||
|
||||
return {
|
||||
lastSyncTime,
|
||||
isLeftColumnOpen: isLeftColumnShown,
|
||||
@ -623,9 +634,10 @@ export default memo(withGlobal<OwnProps>(
|
||||
limitReached: limitReachedModal?.limit,
|
||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||
isReceiptModalOpen: Boolean(payment.receipt),
|
||||
deleteFolderDialogId: deleteFolderDialogModal,
|
||||
deleteFolderDialog,
|
||||
isMasterTab,
|
||||
requestedDraft,
|
||||
chatlistModal,
|
||||
noRightColumnAnimation,
|
||||
};
|
||||
},
|
||||
|
||||
@ -76,7 +76,7 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
||||
'emoji_status',
|
||||
];
|
||||
|
||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
|
||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined'>;
|
||||
|
||||
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
|
||||
'channels',
|
||||
|
||||
@ -26,6 +26,8 @@ const LIMIT_DESCRIPTION: Record<ApiLimitTypeWithModal, string> = {
|
||||
dialogFolderPinned: 'LimitReachedPinDialogs',
|
||||
channelsPublic: 'LimitReachedPublicLinks',
|
||||
channels: 'LimitReachedCommunities',
|
||||
chatlistInvites: 'LimitReachedFolderLinks',
|
||||
chatlistJoined: 'LimitReachedSharedFolders',
|
||||
};
|
||||
|
||||
const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
|
||||
@ -35,6 +37,8 @@ const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
|
||||
dialogFolderPinned: 'LimitReachedPinDialogsLocked',
|
||||
channelsPublic: 'LimitReachedPublicLinksLocked',
|
||||
channels: 'LimitReachedCommunitiesLocked',
|
||||
chatlistInvites: 'LimitReachedFolderLinksLocked',
|
||||
chatlistJoined: 'LimitReachedSharedFoldersLocked',
|
||||
};
|
||||
|
||||
const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
|
||||
@ -44,6 +48,8 @@ const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
|
||||
dialogFolderPinned: 'LimitReachedPinDialogsPremium',
|
||||
channelsPublic: 'LimitReachedPublicLinksPremium',
|
||||
channels: 'LimitReachedCommunitiesPremium',
|
||||
chatlistInvites: 'LimitReachedFolderLinksPremium',
|
||||
chatlistJoined: 'LimitReachedSharedFoldersPremium',
|
||||
};
|
||||
|
||||
const LIMIT_ICON: Record<ApiLimitTypeWithModal, string> = {
|
||||
@ -53,6 +59,8 @@ const LIMIT_ICON: Record<ApiLimitTypeWithModal, string> = {
|
||||
dialogFolderPinned: 'icon-pin-badge',
|
||||
channelsPublic: 'icon-link-badge',
|
||||
channels: 'icon-chats-badge',
|
||||
chatlistInvites: 'icon-link-badge',
|
||||
chatlistJoined: 'icon-folder-badge',
|
||||
};
|
||||
|
||||
const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = {
|
||||
|
||||
111
src/components/modals/chatlist/ChatlistAlready.tsx
Normal file
111
src/components/modals/chatlist/ChatlistAlready.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiChatFolder, ApiChatlistInviteAlready } from '../../../api/types';
|
||||
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Picker from '../../common/Picker';
|
||||
import Badge from '../../ui/Badge';
|
||||
|
||||
import styles from './ChatlistModal.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
invite: ApiChatlistInviteAlready;
|
||||
folder: ApiChatFolder;
|
||||
};
|
||||
|
||||
const ChatlistAlready: FC<OwnProps> = ({ invite, folder }) => {
|
||||
const { closeChatlistModal, joinChatlistInvite } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [selectedPeerIds, setSelectedPeerIds] = useState<string[]>(invite.missingPeerIds);
|
||||
|
||||
const hasChatsToAdd = Boolean(invite.missingPeerIds.length);
|
||||
const newChatsCount = hasChatsToAdd ? invite.missingPeerIds.length : 0;
|
||||
const badgeText = selectedPeerIds.length ? selectedPeerIds.length.toString() : undefined;
|
||||
|
||||
const descriptionText = hasChatsToAdd
|
||||
? lang('FolderLinkSubtitleChats', [newChatsCount, folder.title], undefined, newChatsCount)
|
||||
: lang('FolderLinkSubtitleAlready', folder.title);
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
closeChatlistModal();
|
||||
|
||||
if (!selectedPeerIds.length) return;
|
||||
|
||||
joinChatlistInvite({
|
||||
invite,
|
||||
peerIds: selectedPeerIds,
|
||||
});
|
||||
}, [invite, selectedPeerIds]);
|
||||
|
||||
const handleSelectionToggle = useCallback(() => {
|
||||
const areAllSelected = selectedPeerIds.length === invite.missingPeerIds.length;
|
||||
setSelectedPeerIds(areAllSelected ? [] : invite.missingPeerIds);
|
||||
}, [invite.missingPeerIds, selectedPeerIds.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.description}>
|
||||
{renderText(descriptionText, ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<div className={buildClassName(styles.pickerWrapper, 'custom-scroll')}>
|
||||
{Boolean(invite.missingPeerIds.length) && (
|
||||
<>
|
||||
<div className={styles.pickerHeader}>
|
||||
<div className={styles.pickerHeaderInfo}>
|
||||
{lang('FolderLinkHeaderChatsJoin', selectedPeerIds.length, 'i')}
|
||||
</div>
|
||||
<div
|
||||
className={styles.selectionToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleSelectionToggle}
|
||||
>
|
||||
{selectedPeerIds.length === invite.missingPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={invite.missingPeerIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className={styles.pickerHeader}>
|
||||
<div className={styles.pickerHeaderInfo}>
|
||||
{lang('FolderLinkHeaderAlready')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={invite.alreadyPeerIds}
|
||||
lockedIds={invite.alreadyPeerIds}
|
||||
selectedIds={invite.alreadyPeerIds}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className={styles.buttonText}>
|
||||
{!selectedPeerIds.length && lang('OK')}
|
||||
{Boolean(selectedPeerIds.length) && (
|
||||
<>
|
||||
{lang('FolderLinkButtonJoinPlural')}
|
||||
<Badge className={styles.buttonBadge} text={badgeText} isAlternateColor />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatlistAlready);
|
||||
93
src/components/modals/chatlist/ChatlistDelete.tsx
Normal file
93
src/components/modals/chatlist/ChatlistDelete.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, { memo, useCallback, useState } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiChatFolder } from '../../../api/types';
|
||||
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Badge from '../../ui/Badge';
|
||||
import Picker from '../../common/Picker';
|
||||
|
||||
import styles from './ChatlistModal.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
folder: ApiChatFolder;
|
||||
suggestedPeerIds?: string[];
|
||||
};
|
||||
|
||||
const ChatlistDelete: FC<OwnProps> = ({
|
||||
folder,
|
||||
suggestedPeerIds = MEMO_EMPTY_ARRAY,
|
||||
}) => {
|
||||
const { closeChatlistModal, leaveChatlist } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [selectedPeerIds, setSelectedPeerIds] = useState<string[]>(suggestedPeerIds);
|
||||
|
||||
const badgeText = selectedPeerIds.length ? selectedPeerIds.length.toString() : undefined;
|
||||
|
||||
const handleSelectionToggle = useCallback(() => {
|
||||
const areAllSelected = selectedPeerIds.length === suggestedPeerIds.length;
|
||||
setSelectedPeerIds(areAllSelected ? [] : suggestedPeerIds);
|
||||
}, [suggestedPeerIds, selectedPeerIds.length]);
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
closeChatlistModal();
|
||||
leaveChatlist({ folderId: folder.id, peerIds: selectedPeerIds });
|
||||
}, [folder.id, selectedPeerIds]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
{Boolean(suggestedPeerIds?.length) && (
|
||||
<>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang('FolderLinkSubtitleRemove'), ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<div className={buildClassName(styles.pickerWrapper, 'custom-scroll')}>
|
||||
<div className={styles.pickerHeader}>
|
||||
<div className={styles.pickerHeaderInfo}>
|
||||
{lang('FolderLinkHeaderChatsQuit', selectedPeerIds.length, 'i')}
|
||||
</div>
|
||||
<div
|
||||
className={styles.selectionToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleSelectionToggle}
|
||||
>
|
||||
{selectedPeerIds.length === suggestedPeerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={suggestedPeerIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={handleButtonClick}
|
||||
>
|
||||
<div className={styles.buttonText}>
|
||||
{!selectedPeerIds.length && lang('FolderLinkButtonRemove')}
|
||||
{Boolean(selectedPeerIds.length) && (
|
||||
<>
|
||||
{lang('FolderLinkButtonRemoveChats')}
|
||||
<Badge className={styles.buttonBadge} text={badgeText} isAlternateColor />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatlistDelete);
|
||||
17
src/components/modals/chatlist/ChatlistModal.async.tsx
Normal file
17
src/components/modals/chatlist/ChatlistModal.async.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import type { OwnProps } from './ChatlistModal';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const ChatlistModalAsync: FC<OwnProps> = (props) => {
|
||||
const { info } = props;
|
||||
const ChatlistModal = useModuleLoader(Bundles.Extra, 'ChatlistModal', !info);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return ChatlistModal ? <ChatlistModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default memo(ChatlistModalAsync);
|
||||
71
src/components/modals/chatlist/ChatlistModal.module.scss
Normal file
71
src/components/modals/chatlist/ChatlistModal.module.scss
Normal file
@ -0,0 +1,71 @@
|
||||
.description {
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.picker-wrapper {
|
||||
overflow: auto;
|
||||
max-height: 18rem;
|
||||
}
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.picker-header-info {
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.selection-toggle {
|
||||
color: var(--color-links);
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
.foldersWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.folders {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
position: relative;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-image: linear-gradient(
|
||||
to right,
|
||||
var(--color-background) 0%,
|
||||
rgba(0, 0, 0, 0) 40%,
|
||||
rgba(0, 0, 0, 0) 60%,
|
||||
var(--color-background) 100%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.folder {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.button-text {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button-badge {
|
||||
position: absolute;
|
||||
right: -0.25rem;
|
||||
top: 50%;
|
||||
transform: translate(100%, -50%);
|
||||
}
|
||||
123
src/components/modals/chatlist/ChatlistModal.tsx
Normal file
123
src/components/modals/chatlist/ChatlistModal.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { ApiChatFolder } from '../../../api/types';
|
||||
|
||||
import { selectChatFolder } from '../../../global/selectors';
|
||||
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import ChatlistNew from './ChatlistNew';
|
||||
import ChatlistAlready from './ChatlistAlready';
|
||||
import ChatlistDelete from './ChatlistDelete';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Tab from '../../ui/Tab';
|
||||
|
||||
import styles from './ChatlistModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
info?: TabState['chatlistModal'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
folder?: ApiChatFolder;
|
||||
};
|
||||
|
||||
const ChatlistInviteModal: FC<OwnProps & StateProps> = ({
|
||||
info,
|
||||
folder,
|
||||
}) => {
|
||||
const { closeChatlistModal } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const isOpen = Boolean(info);
|
||||
const renderingInfo = usePrevious(info) || info;
|
||||
const renderingFolder = usePrevious(folder) || folder;
|
||||
|
||||
const title = useMemo(() => {
|
||||
if (!renderingInfo) return undefined;
|
||||
if (renderingInfo.invite) {
|
||||
const invite = renderingInfo.invite;
|
||||
if ('alreadyPeerIds' in invite) {
|
||||
return invite.missingPeerIds.length ? lang('FolderLinkTitleAddChats') : lang('FolderLinkTitleAlready');
|
||||
}
|
||||
return lang('FolderLinkTitleAdd');
|
||||
}
|
||||
if (renderingInfo.removal) {
|
||||
return lang('FolderLinkTitleRemove');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [lang, renderingInfo]);
|
||||
|
||||
const renderingFolderTitle = useMemo(() => {
|
||||
if (renderingFolder) return renderingFolder.title;
|
||||
if (renderingInfo?.invite && 'title' in renderingInfo.invite) return renderingInfo.invite.title;
|
||||
return undefined;
|
||||
}, [renderingFolder, renderingInfo]);
|
||||
|
||||
const folderTabNumber = useMemo(() => {
|
||||
if (!renderingInfo?.invite) return undefined;
|
||||
if ('missingPeerIds' in renderingInfo.invite) return renderingInfo.invite.missingPeerIds.length;
|
||||
return undefined;
|
||||
}, [renderingInfo]);
|
||||
|
||||
function renderFolders(folderTitle: string) {
|
||||
return (
|
||||
<div className={styles.foldersWrapper}>
|
||||
<div className={styles.folders}>
|
||||
<Tab className={styles.folder} title={lang('FolderLinkPreviewLeft')} />
|
||||
<Tab className={styles.folder} isActive badgeCount={folderTabNumber} isBadgeActive title={folderTitle} />
|
||||
<Tab className={styles.folder} title={lang('FolderLinkPreviewRight')} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
if (!renderingInfo) return undefined;
|
||||
if (renderingInfo.invite) {
|
||||
const invite = renderingInfo.invite;
|
||||
if ('alreadyPeerIds' in invite) {
|
||||
return <ChatlistAlready invite={invite} folder={renderingFolder!} />;
|
||||
}
|
||||
|
||||
return <ChatlistNew invite={invite} />;
|
||||
}
|
||||
|
||||
if (renderingInfo.removal) {
|
||||
return <ChatlistDelete folder={renderingFolder!} suggestedPeerIds={renderingInfo.removal.suggestedPeerIds} />;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [renderingFolder, renderingInfo]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
title={title}
|
||||
onClose={closeChatlistModal}
|
||||
isSlim
|
||||
hasCloseButton
|
||||
>
|
||||
{renderingFolderTitle && renderFolders(renderingFolderTitle)}
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { info }): StateProps => {
|
||||
const { invite, removal } = info || {};
|
||||
const folderId = removal?.folderId || (invite && 'folderId' in invite ? invite.folderId : undefined);
|
||||
const folder = folderId ? selectChatFolder(global, folderId) : undefined;
|
||||
|
||||
return {
|
||||
folder,
|
||||
};
|
||||
},
|
||||
)(ChatlistInviteModal));
|
||||
93
src/components/modals/chatlist/ChatlistNew.tsx
Normal file
93
src/components/modals/chatlist/ChatlistNew.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React, {
|
||||
memo, useCallback, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiChatlistInviteNew } from '../../../api/types';
|
||||
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Picker from '../../common/Picker';
|
||||
import Badge from '../../ui/Badge';
|
||||
|
||||
import styles from './ChatlistModal.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
invite: ApiChatlistInviteNew;
|
||||
};
|
||||
|
||||
const ChatlistNew: FC<OwnProps> = ({ invite }) => {
|
||||
const { closeChatlistModal, joinChatlistInvite } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [selectedPeerIds, setSelectedPeerIds] = useState<string[]>(invite.peerIds);
|
||||
|
||||
const joinedIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
return invite.peerIds.filter((id) => !chatsById[id].isNotJoined);
|
||||
}, [invite.peerIds]);
|
||||
|
||||
const selectedCount = selectedPeerIds.length - joinedIds.length;
|
||||
|
||||
const badgeText = selectedCount ? selectedCount.toString() : undefined;
|
||||
|
||||
const handleButtonClick = useCallback(() => {
|
||||
closeChatlistModal();
|
||||
|
||||
joinChatlistInvite({
|
||||
invite,
|
||||
peerIds: selectedPeerIds,
|
||||
});
|
||||
}, [invite, selectedPeerIds]);
|
||||
|
||||
const handleSelectionToggle = useCallback(() => {
|
||||
const areAllSelected = selectedPeerIds.length === invite.peerIds.length;
|
||||
setSelectedPeerIds(areAllSelected ? joinedIds : invite.peerIds);
|
||||
}, [invite.peerIds, joinedIds, selectedPeerIds.length]);
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang('FolderLinkSubtitle', invite.title), ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<div className={buildClassName(styles.pickerWrapper, 'custom-scroll')}>
|
||||
<div className={styles.pickerHeader}>
|
||||
<div className={styles.pickerHeaderInfo}>
|
||||
{lang('FolderLinkHeaderChatsJoin', selectedCount, 'i')}
|
||||
</div>
|
||||
<div
|
||||
className={styles.selectionToggle}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={handleSelectionToggle}
|
||||
>
|
||||
{selectedPeerIds.length === invite.peerIds.length ? lang('DeselectAll') : lang('SelectAll')}
|
||||
</div>
|
||||
</div>
|
||||
<Picker
|
||||
itemIds={invite.peerIds}
|
||||
lockedIds={joinedIds}
|
||||
onSelectedIdsChange={setSelectedPeerIds}
|
||||
selectedIds={selectedPeerIds}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleButtonClick}
|
||||
size="smaller"
|
||||
disabled={!selectedPeerIds.length}
|
||||
>
|
||||
<div className={styles.buttonText}>
|
||||
{lang('FolderLinkButtonAdd', invite.title)}
|
||||
<Badge className={styles.buttonBadge} text={badgeText} isAlternateColor />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChatlistNew);
|
||||
@ -128,6 +128,7 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
|
||||
isLoading={isSearching}
|
||||
onSelectedIdsChange={setSelectedMemberIds}
|
||||
onFilterChange={handleFilterChange}
|
||||
isSearchable
|
||||
noScrollRestore={noPickerScrollRestore}
|
||||
/>
|
||||
|
||||
|
||||
@ -31,6 +31,8 @@ type StateProps = {
|
||||
isChannel?: boolean;
|
||||
};
|
||||
|
||||
const BULLET = '\u2022';
|
||||
|
||||
const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
invite,
|
||||
@ -83,19 +85,23 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
||||
{!importers.length && (
|
||||
usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')
|
||||
)}
|
||||
{importers.map((importer) => (
|
||||
<ListItem
|
||||
className="chat-item-clickable scroll-item small-icon"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openChat({ id: importer.userId })}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
userId={importer.userId}
|
||||
status={formatMediaDateTime(lang, importer.date * 1000, true)}
|
||||
forceShowSelf
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
{importers.map((importer) => {
|
||||
const joinTime = formatMediaDateTime(lang, importer.date * 1000, true);
|
||||
const status = importer.isFromChatList ? `${joinTime} ${BULLET} ${lang('JoinedViaFolder')}` : joinTime;
|
||||
return (
|
||||
<ListItem
|
||||
className="chat-item-clickable scroll-item small-icon"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openChat({ id: importer.userId })}
|
||||
>
|
||||
<PrivateChatInfo
|
||||
userId={importer.userId}
|
||||
status={status}
|
||||
forceShowSelf
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -19,15 +19,13 @@ import useLang from '../../../hooks/useLang';
|
||||
import useInterval from '../../../hooks/useInterval';
|
||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
|
||||
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';
|
||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||
import InviteLink from '../../common/InviteLink';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
@ -79,7 +77,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
|
||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
@ -181,10 +178,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
}, [lang, showNotification]);
|
||||
|
||||
const handleCopyPrimaryClicked = useCallback(() => {
|
||||
copyLink(primaryInviteLink!);
|
||||
}, [copyLink, primaryInviteLink]);
|
||||
|
||||
const prepareUsageText = (invite: ApiExportedInvite) => {
|
||||
const {
|
||||
usage = 0, usageLimit, expireDate, isPermanent, requested, isRevoked,
|
||||
@ -277,22 +270,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
return actions;
|
||||
};
|
||||
|
||||
const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
return ({ onTrigger, isOpen }) => (
|
||||
<Button
|
||||
round
|
||||
ripple={!isMobile}
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
className={isOpen ? 'active' : ''}
|
||||
onClick={onTrigger}
|
||||
ariaLabel="Actions"
|
||||
>
|
||||
<i className="icon icon-more" />
|
||||
</Button>
|
||||
);
|
||||
}, [isMobile]);
|
||||
|
||||
return (
|
||||
<div className="Management ManageInvites">
|
||||
<div className="custom-scroll">
|
||||
@ -305,30 +282,11 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
<p className="text-muted">{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p>
|
||||
</div>
|
||||
{primaryInviteLink && (
|
||||
<div className="section">
|
||||
<p className="text-muted">
|
||||
{chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||
</p>
|
||||
<div className="primary-link">
|
||||
<input
|
||||
className="form-control primary-link-input"
|
||||
value={primaryInviteLink}
|
||||
readOnly
|
||||
onClick={handleCopyPrimaryClicked}
|
||||
/>
|
||||
<DropdownMenu
|
||||
className="primary-link-more-menu"
|
||||
trigger={PrimaryLinkMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="copy" onClick={handleCopyPrimaryClicked}>{lang('Copy')}</MenuItem>
|
||||
{!chat?.usernames && (
|
||||
<MenuItem icon="delete" onClick={handlePrimaryRevoke} destructive>{lang('RevokeButton')}</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<Button onClick={handleCopyPrimaryClicked}>{lang('CopyLink')}</Button>
|
||||
</div>
|
||||
<InviteLink
|
||||
inviteLink={primaryInviteLink}
|
||||
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
|
||||
title={chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||
/>
|
||||
)}
|
||||
<div className="section" teactFastList>
|
||||
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
|
||||
|
||||
@ -200,24 +200,6 @@
|
||||
}
|
||||
|
||||
.ManageInvites {
|
||||
.primary-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.primary-link-input {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 3rem;
|
||||
}
|
||||
|
||||
.primary-link-more-menu {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.create-link {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
21
src/components/ui/Badge.module.scss
Normal file
21
src/components/ui/Badge.module.scss
Normal file
@ -0,0 +1,21 @@
|
||||
.root {
|
||||
min-width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0 0.4375rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5625rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.default {
|
||||
background: var(--color-gray);
|
||||
color: var(--color-white);
|
||||
}
|
||||
|
||||
.alternate {
|
||||
color: var(--color-chat-active);
|
||||
background: var(--color-white);
|
||||
}
|
||||
33
src/components/ui/Badge.tsx
Normal file
33
src/components/ui/Badge.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
import ShowTransition from './ShowTransition';
|
||||
|
||||
import styles from './Badge.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
text?: string;
|
||||
className?: string;
|
||||
isAlternateColor?: boolean;
|
||||
};
|
||||
|
||||
const Badge: FC<OwnProps> = ({
|
||||
text,
|
||||
className,
|
||||
isAlternateColor,
|
||||
}) => {
|
||||
return (
|
||||
<ShowTransition
|
||||
className={buildClassName(styles.root, isAlternateColor ? styles.alternate : styles.default, className)}
|
||||
isOpen={Boolean(text)}
|
||||
>
|
||||
{text && <AnimatedCounter text={text} />}
|
||||
</ShowTransition>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Badge);
|
||||
@ -10,7 +10,6 @@
|
||||
padding: 0.625rem 0.25rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
border-top-left-radius: var(--border-radius-messages-small);
|
||||
border-top-right-radius: var(--border-radius-messages-small);
|
||||
|
||||
@ -20,6 +19,22 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&--interactive {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:not(&--active):hover {
|
||||
background: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
&:not(&--active):active {
|
||||
background: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--active {
|
||||
cursor: var(--custom-cursor, default);
|
||||
color: var(--color-primary);
|
||||
@ -29,18 +44,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:not(&--active):hover {
|
||||
background: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
&:not(&--active):active {
|
||||
background: var(--color-interactive-element-hover);
|
||||
}
|
||||
}
|
||||
|
||||
> span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -102,3 +105,7 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Tab-context-menu {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useRef,
|
||||
memo,
|
||||
@ -7,10 +6,19 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import forceReflow from '../../util/forceReflow';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import useMenuPosition from '../../hooks/useMenuPosition';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
|
||||
import Menu from './Menu';
|
||||
import MenuItem from './MenuItem';
|
||||
import MenuSeparator from './MenuSeparator';
|
||||
|
||||
import './Tab.scss';
|
||||
|
||||
@ -22,8 +30,10 @@ type OwnProps = {
|
||||
badgeCount?: number;
|
||||
isBadgeActive?: boolean;
|
||||
previousActiveTab?: number;
|
||||
onClick: (arg: number) => void;
|
||||
clickArg: number;
|
||||
onClick?: (arg: number) => void;
|
||||
clickArg?: number;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
contextRootElementSelector?: string;
|
||||
};
|
||||
|
||||
const classNames = {
|
||||
@ -41,6 +51,8 @@ const Tab: FC<OwnProps> = ({
|
||||
previousActiveTab,
|
||||
onClick,
|
||||
clickArg,
|
||||
contextActions,
|
||||
contextRootElementSelector,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tabRef = useRef<HTMLDivElement>(null);
|
||||
@ -95,19 +107,59 @@ const Tab: FC<OwnProps> = ({
|
||||
});
|
||||
}, [isActive, previousActiveTab]);
|
||||
|
||||
const {
|
||||
contextMenuPosition, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
|
||||
handleContextMenuHide, isContextMenuOpen,
|
||||
} = useContextMenuHandlers(tabRef, !contextActions);
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (contextActions && (e.button === MouseButton.Secondary || !onClick)) {
|
||||
handleBeforeContextMenu(e);
|
||||
}
|
||||
|
||||
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(clickArg);
|
||||
}, [clickArg, onClick]);
|
||||
onClick?.(clickArg!);
|
||||
}, [clickArg, contextActions, handleBeforeContextMenu, onClick]);
|
||||
|
||||
const getTriggerElement = useCallback(() => tabRef.current, []);
|
||||
|
||||
const getRootElement = useCallback(
|
||||
() => (contextRootElementSelector
|
||||
? tabRef.current!.closest(contextRootElementSelector)
|
||||
: document.body),
|
||||
[contextRootElementSelector],
|
||||
);
|
||||
|
||||
const getMenuElement = useCallback(
|
||||
() => document.querySelector('#portals')!
|
||||
.querySelector('.Tab-context-menu .bubble'),
|
||||
[],
|
||||
);
|
||||
|
||||
const getLayout = useCallback(
|
||||
() => ({ withPortal: true }),
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
getLayout,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Tab', className)}
|
||||
className={buildClassName('Tab', onClick && 'Tab--interactive', className)}
|
||||
onClick={IS_TOUCH_ENV ? handleClick : undefined}
|
||||
onMouseDown={!IS_TOUCH_ENV ? handleClick : undefined}
|
||||
onContextMenu={handleContextMenu}
|
||||
ref={tabRef}
|
||||
>
|
||||
<span className="Tab_inner">
|
||||
@ -118,6 +170,38 @@ const Tab: FC<OwnProps> = ({
|
||||
{isBlocked && <i className="icon icon-lock-badge blocked" />}
|
||||
<i className="platform" />
|
||||
</span>
|
||||
|
||||
{contextActions && contextMenuPosition !== undefined && (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
className="Tab-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || 'separator'} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{action.title}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useRef, useEffect } from '../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { MenuItemContextAction } from './ListItem';
|
||||
|
||||
import { ALL_FOLDER_ID } from '../../config';
|
||||
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
|
||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||
@ -19,6 +21,7 @@ export type TabWithProperties = {
|
||||
badgeCount?: number;
|
||||
isBlocked?: boolean;
|
||||
isBadgeActive?: boolean;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
@ -27,6 +30,7 @@ type OwnProps = {
|
||||
activeTab: number;
|
||||
big?: boolean;
|
||||
onSwitchTab: (index: number) => void;
|
||||
contextRootElementSelector?: string;
|
||||
};
|
||||
|
||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||
@ -35,6 +39,7 @@ const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
|
||||
|
||||
const TabList: FC<OwnProps> = ({
|
||||
tabs, areFolders, activeTab, big, onSwitchTab,
|
||||
contextRootElementSelector,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -86,6 +91,8 @@ const TabList: FC<OwnProps> = ({
|
||||
previousActiveTab={previousActiveTab}
|
||||
onClick={onSwitchTab}
|
||||
clickArg={i}
|
||||
contextActions={tab.contextActions}
|
||||
contextRootElementSelector={contextRootElementSelector}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -304,4 +304,6 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
|
||||
channels: [500, 1000],
|
||||
channelsPublic: [10, 20],
|
||||
aboutLength: [70, 140],
|
||||
chatlistInvites: [3, 100],
|
||||
chatlistJoined: [2, 20],
|
||||
};
|
||||
|
||||
@ -4,10 +4,15 @@ import {
|
||||
} from '../../index';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiUser, ApiError, ApiChatMember,
|
||||
ApiChat, ApiUser, ApiError, ApiChatMember, ApiChatFolder, ApiChatlistExportedInvite,
|
||||
} from '../../../api/types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { NewChatMembersProgress, ChatCreationProgress, ManagementProgress } from '../../../types';
|
||||
import {
|
||||
ChatCreationProgress,
|
||||
ManagementProgress,
|
||||
NewChatMembersProgress,
|
||||
SettingsScreens,
|
||||
} from '../../../types';
|
||||
import type {
|
||||
GlobalState, ActionReturnType, TabArgs,
|
||||
} from '../../types';
|
||||
@ -89,6 +94,11 @@ const SERVICE_NOTIFICATIONS_USER_MOCK: ApiUser = {
|
||||
isMin: true,
|
||||
phoneNumber: '',
|
||||
};
|
||||
const CHATLIST_LIMIT_ERROR_LIST = new Set([
|
||||
'FILTERS_TOO_MUCH',
|
||||
'CHATLISTS_TOO_MUCH',
|
||||
'INVITES_TOO_MUCH',
|
||||
]);
|
||||
|
||||
const runThrottledForLoadTopChats = throttle((cb) => cb(), 3000, true);
|
||||
const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true);
|
||||
@ -896,6 +906,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
||||
focusMessage,
|
||||
openInvoice,
|
||||
processAttachBotParameters,
|
||||
checkChatlistInvite,
|
||||
openChatByUsername: openChatByUsernameAction,
|
||||
} = actions;
|
||||
|
||||
@ -954,14 +965,23 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
||||
return;
|
||||
}
|
||||
|
||||
if (part1 === 'share') {
|
||||
const text = formatShareText(params.url, params.text);
|
||||
openChatWithDraft({ text, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
if (part1 === 'addlist') {
|
||||
const slug = part2;
|
||||
checkChatlistInvite({ slug, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const chatOrChannelPostId = part2 || undefined;
|
||||
const messageId = part3 ? Number(part3) : undefined;
|
||||
const commentId = params.comment ? Number(params.comment) : undefined;
|
||||
|
||||
if (part1 === 'share') {
|
||||
const text = formatShareText(params.url, params.text);
|
||||
openChatWithDraft({ text, tabId });
|
||||
} else if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) {
|
||||
if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) {
|
||||
joinVoiceChatByLink({
|
||||
username: part1,
|
||||
inviteHash: params.voicechat || params.livestream,
|
||||
@ -1814,6 +1834,269 @@ addActionHandler('toggleTopicPinned', (global, actions, payload): ActionReturnTy
|
||||
void callApi('togglePinnedTopic', { chat, topicId, isPinned });
|
||||
});
|
||||
|
||||
addActionHandler('checkChatlistInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { slug, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('checkChatlistInvite', { slug });
|
||||
if (!result) {
|
||||
actions.showNotification({
|
||||
message: langProvider.translate('lng_group_invite_bad_link'),
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
|
||||
global = updateTabState(global, {
|
||||
chatlistModal: {
|
||||
invite: result.invite,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('joinChatlistInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { invite, peerIds, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const peers = peerIds.map((peerId) => selectChat(global, peerId)).filter(Boolean);
|
||||
const notJoinedCount = peers.filter((peer) => peer.isNotJoined).length;
|
||||
|
||||
const folder = 'folderId' in invite ? selectChatFolder(global, invite.folderId) : undefined;
|
||||
const folderTitle = 'title' in invite ? invite.title : folder?.title;
|
||||
|
||||
try {
|
||||
const result = await callApi('joinChatlistInvite', { slug: invite.slug, peers });
|
||||
if (!result) return;
|
||||
|
||||
actions.showNotification({
|
||||
title: langProvider.translate(folder ? 'FolderLinkUpdatedTitle' : 'FolderLinkAddedTitle', folderTitle),
|
||||
message: langProvider.translate('FolderLinkAddedSubtitle', notJoinedCount, 'i'),
|
||||
tabId,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as ApiError).message === 'CHATLISTS_TOO_MUCH') {
|
||||
actions.openLimitReachedModal({ limit: 'chatlistJoined', tabId });
|
||||
} else {
|
||||
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('leaveChatlist', async (global, actions, payload): Promise<void> => {
|
||||
const { folderId, peerIds, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const folder = selectChatFolder(global, folderId);
|
||||
|
||||
const peers = peerIds?.map((peerId) => selectChat(global, peerId)).filter(Boolean) || [];
|
||||
|
||||
const result = await callApi('leaveChatlist', { folderId, peers });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
actions.showNotification({
|
||||
title: langProvider.translate('FolderLinkDeletedTitle', folder.title),
|
||||
message: langProvider.translate('FolderLinkDeletedSubtitle', peers.length, 'i'),
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('loadChatlistInvites', async (global, actions, payload): Promise<void> => {
|
||||
const { folderId } = payload;
|
||||
|
||||
const result = await callApi('fetchChatlistInvites', { folderId });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = {
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
invites: {
|
||||
...global.chatFolders.invites,
|
||||
[folderId]: result.invites,
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('createChatlistInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { folderId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const folder = selectChatFolder(global, folderId);
|
||||
if (!folder) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
shareFolderScreen: {
|
||||
...selectTabState(global, tabId).shareFolderScreen!,
|
||||
isLoading: true,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
let result: { filter: ApiChatFolder; invite: ApiChatlistExportedInvite | undefined } | undefined;
|
||||
|
||||
try {
|
||||
result = await callApi('createChalistInvite', {
|
||||
folderId,
|
||||
peers: folder.includedChatIds.concat(folder.pinnedChatIds || [])
|
||||
.map((chatId) => selectChat(global, chatId) || selectUser(global, chatId)).filter(Boolean),
|
||||
});
|
||||
} catch (error) {
|
||||
if (CHATLIST_LIMIT_ERROR_LIST.has((error as ApiError).message)) {
|
||||
actions.openLimitReachedModal({ limit: 'chatlistInvites', tabId });
|
||||
actions.requestNextSettingsScreen({ screen: SettingsScreens.Folders, tabId });
|
||||
} else {
|
||||
actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
|
||||
}
|
||||
}
|
||||
|
||||
if (!result || !result.invite) return;
|
||||
|
||||
const { shareFolderScreen } = selectTabState(global, tabId);
|
||||
|
||||
if (!shareFolderScreen) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
byId: {
|
||||
...global.chatFolders.byId,
|
||||
[folderId]: {
|
||||
...global.chatFolders.byId[folderId],
|
||||
...result.filter,
|
||||
},
|
||||
},
|
||||
invites: {
|
||||
...global.chatFolders.invites,
|
||||
[folderId]: [
|
||||
...(global.chatFolders.invites[folderId] || []),
|
||||
result.invite,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
global = updateTabState(global, {
|
||||
shareFolderScreen: {
|
||||
...shareFolderScreen,
|
||||
url: result.invite.url,
|
||||
isLoading: false,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('editChatlistInvite', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
folderId, peerIds, url, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const slug = url.split('/').pop();
|
||||
if (!slug) return;
|
||||
|
||||
const peers = peerIds
|
||||
.map((chatId) => selectChat(global, chatId) || selectUser(global, chatId)).filter(Boolean);
|
||||
|
||||
global = updateTabState(global, {
|
||||
shareFolderScreen: {
|
||||
...selectTabState(global, tabId).shareFolderScreen!,
|
||||
isLoading: true,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('editChatlistInvite', { folderId, slug, peers });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
invites: {
|
||||
...global.chatFolders.invites,
|
||||
[folderId]: global.chatFolders.invites[folderId]?.map((invite) => {
|
||||
if (invite.url === url) {
|
||||
return result;
|
||||
}
|
||||
return invite;
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
global = updateTabState(global, {
|
||||
shareFolderScreen: {
|
||||
...selectTabState(global, tabId).shareFolderScreen!,
|
||||
isLoading: false,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('deleteChatlistInvite', async (global, actions, payload): Promise<void> => {
|
||||
const { folderId, url } = payload;
|
||||
|
||||
const slug = url.split('/').pop();
|
||||
|
||||
if (!slug) return;
|
||||
|
||||
const result = await callApi('deleteChatlistInvite', { folderId, slug });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = {
|
||||
...global,
|
||||
chatFolders: {
|
||||
...global.chatFolders,
|
||||
invites: {
|
||||
...global.chatFolders.invites,
|
||||
[folderId]: global.chatFolders.invites[folderId]?.filter((invite) => invite.url !== url),
|
||||
},
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openDeleteChatFolderModal', async (global, actions, payload): Promise<void> => {
|
||||
const { folderId, isConfirmedForChatlist, tabId = getCurrentTabId() } = payload;
|
||||
const folder = selectChatFolder(global, folderId);
|
||||
if (!folder) return;
|
||||
|
||||
if (folder.isChatList && (!folder.hasMyInvites || isConfirmedForChatlist)) {
|
||||
const suggestions = await callApi('fetchLeaveChatlistSuggestions', { folderId });
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
chatlistModal: {
|
||||
removal: {
|
||||
folderId,
|
||||
suggestedPeerIds: suggestions,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
global = updateTabState(global, {
|
||||
deleteFolderDialogModal: folderId,
|
||||
}, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
async function loadChats<T extends GlobalState>(
|
||||
global: T,
|
||||
listType: 'active' | 'archived',
|
||||
|
||||
@ -5,7 +5,9 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import {
|
||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList,
|
||||
} from '../../reducers';
|
||||
import { selectChat, selectCurrentMessageList, selectTabState } from '../../selectors';
|
||||
import {
|
||||
selectChat, selectCurrentMessageList, selectTabState,
|
||||
} from '../../selectors';
|
||||
import { closeLocalTextSearch } from './localSearch';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
@ -153,16 +155,16 @@ addActionHandler('openNextChat', (global, actions, payload): ActionReturnType =>
|
||||
actions.openChat({ id: nextId, shouldReplaceHistory: true, tabId });
|
||||
});
|
||||
|
||||
addActionHandler('openDeleteChatFolderModal', (global, actions, payload): ActionReturnType => {
|
||||
const { folderId, tabId = getCurrentTabId() } = payload;
|
||||
return updateTabState(global, {
|
||||
deleteFolderDialogModal: folderId,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeDeleteChatFolderModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
deleteFolderDialogModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeChatlistModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
chatlistModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -1,15 +1,18 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { addActionHandler, getActions } from '../../index';
|
||||
|
||||
import { SettingsScreens } from '../../../types';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
|
||||
import { replaceSettings, replaceThemeSettings } from '../../reducers';
|
||||
import switchTheme from '../../../util/switchTheme';
|
||||
import { setLanguage, setTimeFormat } from '../../../util/langProvider';
|
||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { applyPerformanceSettings } from '../../../util/perfomanceSettings';
|
||||
import { selectCanAnimateInterface } from '../../selectors';
|
||||
import { selectCanAnimateInterface, selectChatFolder } from '../../selectors';
|
||||
|
||||
let prevGlobal: GlobalState | undefined;
|
||||
|
||||
@ -87,8 +90,58 @@ addActionHandler('setThemeSettings', (global, actions, payload): ActionReturnTyp
|
||||
});
|
||||
|
||||
addActionHandler('requestNextSettingsScreen', (global, actions, payload): ActionReturnType => {
|
||||
const { screen, tabId = getCurrentTabId() } = payload;
|
||||
const { screen, foldersAction, tabId = getCurrentTabId() } = payload;
|
||||
return updateTabState(global, {
|
||||
nextSettingsScreen: screen,
|
||||
nextFoldersAction: foldersAction,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openEditChatFolder', (global, actions, payload): ActionReturnType => {
|
||||
const { folderId, isOnlyInvites, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chatFolder = selectChatFolder(global, folderId);
|
||||
if (!chatFolder) return;
|
||||
|
||||
actions.requestNextSettingsScreen({
|
||||
screen: isOnlyInvites ? SettingsScreens.FoldersEditFolderInvites : SettingsScreens.FoldersEditFolderFromChatList,
|
||||
foldersAction: {
|
||||
type: 'editFolder',
|
||||
payload: chatFolder,
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('openShareChatFolderModal', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
folderId, url, noRequestNextScreen, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const chatFolder = selectChatFolder(global, folderId);
|
||||
const isChatList = chatFolder?.isChatList;
|
||||
if (isChatList && !noRequestNextScreen) {
|
||||
actions.openEditChatFolder({ folderId, isOnlyInvites: true, tabId });
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!noRequestNextScreen) actions.requestNextSettingsScreen({ screen: SettingsScreens.FoldersShare, tabId });
|
||||
|
||||
return updateTabState(global, {
|
||||
shareFolderScreen: {
|
||||
folderId,
|
||||
isFromSettings: Boolean(noRequestNextScreen),
|
||||
url,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeShareChatFolderModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
actions.requestNextSettingsScreen({ screen: undefined, tabId });
|
||||
|
||||
return updateTabState(global, {
|
||||
shareFolderScreen: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -323,12 +323,12 @@ export function getCanDeleteChat(chat: ApiChat) {
|
||||
|
||||
export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) {
|
||||
const {
|
||||
id, title, emoticon, description, pinnedChatIds,
|
||||
excludedChatIds, includedChatIds,
|
||||
excludeArchived, excludeMuted, excludeRead,
|
||||
...filters
|
||||
bots, groups, contacts, nonContacts, channels,
|
||||
} = folder;
|
||||
|
||||
const filters = [bots, groups, contacts, nonContacts, channels];
|
||||
|
||||
// If folder has multiple additive filters or uses include/exclude lists,
|
||||
// we display folder chats count
|
||||
if (
|
||||
@ -341,15 +341,15 @@ export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, ch
|
||||
}
|
||||
|
||||
// Otherwise, we return a short description of a single filter
|
||||
if (filters.bots) {
|
||||
if (bots) {
|
||||
return lang('FilterBots');
|
||||
} else if (filters.groups) {
|
||||
} else if (groups) {
|
||||
return lang('FilterGroups');
|
||||
} else if (filters.channels) {
|
||||
} else if (channels) {
|
||||
return lang('FilterChannels');
|
||||
} else if (filters.contacts) {
|
||||
} else if (contacts) {
|
||||
return lang('FilterContacts');
|
||||
} else if (filters.nonContacts) {
|
||||
} else if (nonContacts) {
|
||||
return lang('FilterNonContacts');
|
||||
} else {
|
||||
return undefined;
|
||||
|
||||
@ -123,6 +123,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
|
||||
chatFolders: {
|
||||
byId: {},
|
||||
invites: {},
|
||||
},
|
||||
|
||||
fileUploads: {
|
||||
|
||||
@ -3,7 +3,14 @@ import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
import {
|
||||
getPrivateChatUserId, isChatChannel, isUserId, isHistoryClearMessage, isUserBot, isUserOnline,
|
||||
getPrivateChatUserId,
|
||||
isChatChannel,
|
||||
isUserId,
|
||||
isHistoryClearMessage,
|
||||
isUserBot,
|
||||
isUserOnline,
|
||||
getHasAdminRight,
|
||||
isChatSuperGroup,
|
||||
} from '../helpers';
|
||||
import { selectBot, selectUser } from './users';
|
||||
import {
|
||||
@ -239,3 +246,31 @@ export function filterChatIdsByType<T extends GlobalState>(
|
||||
return filter.includes(type);
|
||||
});
|
||||
}
|
||||
|
||||
export function selectCanInviteToChat<T extends GlobalState>(global: T, chatId: string) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return false;
|
||||
|
||||
// https://github.com/TelegramMessenger/Telegram-iOS/blob/5126be83b3b9578fb014eb52ca553da9e7a8b83a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift#L6
|
||||
return Boolean(!isUserId(chatId) && ((isChatChannel(chat) || isChatSuperGroup(chat)) ? (
|
||||
chat.isCreator || getHasAdminRight(chat, 'inviteUsers')
|
||||
|| (chat.usernames?.length && !chat.isJoinRequest)
|
||||
) : (chat.isCreator || getHasAdminRight(chat, 'inviteUsers'))));
|
||||
}
|
||||
|
||||
export function selectCanShareFolder<T extends GlobalState>(global: T, folderId: number) {
|
||||
const folder = selectChatFolder(global, folderId);
|
||||
if (!folder) return false;
|
||||
|
||||
const {
|
||||
bots, groups, channels, contacts, nonContacts, includedChatIds, pinnedChatIds,
|
||||
excludeArchived, excludeMuted, excludeRead, excludedChatIds,
|
||||
} = folder;
|
||||
|
||||
return !bots && !groups && !channels && !contacts && !nonContacts
|
||||
&& !excludeArchived && !excludeMuted && !excludeRead && !excludedChatIds?.length
|
||||
&& (pinnedChatIds?.length || includedChatIds.length)
|
||||
&& folder.includedChatIds.concat(folder.pinnedChatIds || []).some((chatId) => {
|
||||
return selectCanInviteToChat(global, chatId);
|
||||
});
|
||||
}
|
||||
|
||||
@ -11,7 +11,8 @@ export function selectCurrentLimit<T extends GlobalState>(global: T, limit: ApiL
|
||||
const isPremium = selectIsCurrentUserPremium(global);
|
||||
const { limits } = appConfig;
|
||||
|
||||
const value = limits[limit][isPremium ? 1 : 0] ?? DEFAULT_LIMITS[limit][isPremium ? 1 : 0];
|
||||
// When there are new limits when updating a layer, until we get a new configuration, we must use the default values
|
||||
const value = limits[limit]?.[isPremium ? 1 : 0] ?? DEFAULT_LIMITS[limit][isPremium ? 1 : 0];
|
||||
if (limit === 'dialogFilters') return value + 1; // Server does not count "All" as folder, but we need to
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -60,6 +60,8 @@ import type {
|
||||
ApiWebSession,
|
||||
ApiUserFullInfo,
|
||||
ApiChatFullInfo,
|
||||
ApiChatlistInvite,
|
||||
ApiChatlistExportedInvite,
|
||||
} from '../api/types';
|
||||
import type {
|
||||
ApiInvoiceContainer,
|
||||
@ -93,6 +95,8 @@ import type {
|
||||
} from '../types';
|
||||
import type { P2pMessage } from '../lib/secret-sauce';
|
||||
import type { ApiCredentials } from '../components/payment/PaymentModal';
|
||||
import type { FoldersActions } from '../hooks/reducers/useFoldersReducer';
|
||||
import type { ReducerAction } from '../hooks/useReducer';
|
||||
|
||||
export type MessageListType =
|
||||
'thread'
|
||||
@ -157,7 +161,7 @@ export interface ServiceNotification {
|
||||
|
||||
export type ApiLimitType = (
|
||||
'uploadMaxFileparts' | 'stickersFaved' | 'savedGifs' | 'dialogFiltersChats' | 'dialogFilters' | 'dialogFolderPinned' |
|
||||
'captionLength' | 'channels' | 'channelsPublic' | 'aboutLength'
|
||||
'captionLength' | 'channels' | 'channelsPublic' | 'aboutLength' | 'chatlistInvites' | 'chatlistJoined'
|
||||
);
|
||||
|
||||
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
||||
@ -206,6 +210,13 @@ export type TabState = {
|
||||
};
|
||||
|
||||
nextSettingsScreen?: SettingsScreens;
|
||||
nextFoldersAction?: ReducerAction<FoldersActions>;
|
||||
shareFolderScreen?: {
|
||||
folderId: number;
|
||||
isFromSettings?: boolean;
|
||||
url?: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
isCallPanelVisible?: boolean;
|
||||
multitabNextAction?: CallbackAction;
|
||||
@ -553,6 +564,14 @@ export type TabState = {
|
||||
messageId: number;
|
||||
activeLanguage?: string;
|
||||
};
|
||||
|
||||
chatlistModal?: {
|
||||
invite?: ApiChatlistInvite;
|
||||
removal?: {
|
||||
folderId: number;
|
||||
suggestedPeerIds?: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalState = {
|
||||
@ -690,6 +709,7 @@ export type GlobalState = {
|
||||
chatFolders: {
|
||||
orderedIds?: number[];
|
||||
byId: Record<number, ApiChatFolder>;
|
||||
invites: Record<number, ApiChatlistExportedInvite[]>;
|
||||
recommended?: ApiChatFolder[];
|
||||
};
|
||||
|
||||
@ -1673,6 +1693,34 @@ export interface ActionPayloads {
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
checkChatlistInvite: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
joinChatlistInvite: {
|
||||
invite: ApiChatlistInvite;
|
||||
peerIds: string[];
|
||||
} & WithTabId;
|
||||
leaveChatlist: {
|
||||
folderId: number;
|
||||
peerIds?: string[];
|
||||
} & WithTabId;
|
||||
closeChatlistModal: WithTabId | undefined;
|
||||
loadChatlistInvites: {
|
||||
folderId: number;
|
||||
};
|
||||
createChatlistInvite: {
|
||||
folderId: number;
|
||||
} & WithTabId;
|
||||
editChatlistInvite: {
|
||||
folderId: number;
|
||||
url: string;
|
||||
peerIds: string[];
|
||||
} & WithTabId;
|
||||
deleteChatlistInvite: {
|
||||
folderId: number;
|
||||
url: string;
|
||||
} & WithTabId;
|
||||
|
||||
// Messages
|
||||
setEditingDraft: {
|
||||
text?: ApiFormattedText;
|
||||
@ -2300,10 +2348,24 @@ export interface ActionPayloads {
|
||||
} | undefined;
|
||||
requestNextSettingsScreen: {
|
||||
screen?: SettingsScreens;
|
||||
foldersAction?: ReducerAction<FoldersActions>;
|
||||
} & WithTabId;
|
||||
sortChatFolders: { folderIds: number[] };
|
||||
closeDeleteChatFolderModal: WithTabId | undefined;
|
||||
openDeleteChatFolderModal: { folderId: number } & WithTabId;
|
||||
openDeleteChatFolderModal: {
|
||||
folderId: number;
|
||||
isConfirmedForChatlist?: boolean;
|
||||
} & WithTabId;
|
||||
openShareChatFolderModal: {
|
||||
folderId: number;
|
||||
url?: string;
|
||||
noRequestNextScreen?: boolean;
|
||||
} & WithTabId;
|
||||
openEditChatFolder: {
|
||||
folderId: number;
|
||||
isOnlyInvites?: boolean;
|
||||
} & WithTabId;
|
||||
closeShareChatFolderModal: undefined | WithTabId;
|
||||
loadGlobalPrivacySettings: undefined;
|
||||
updateGlobalPrivacySettings: { shouldArchiveAndMuteNewNonContact: boolean };
|
||||
|
||||
|
||||
@ -1395,4 +1395,12 @@ stats.getBroadcastStats#ab42441a flags:# dark:flags.0?true channel:InputChannel
|
||||
stats.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
||||
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;
|
||||
stats.getMessagePublicForwards#5630281b channel:InputChannel msg_id:int offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;`;
|
||||
stats.getMessageStats#b6e0a3f5 flags:# dark:flags.0?true channel:InputChannel msg_id:int = stats.MessageStats;
|
||||
chatlists.exportChatlistInvite#8472478e chatlist:InputChatlist title:string peers:Vector<InputPeer> = chatlists.ExportedChatlistInvite;
|
||||
chatlists.deleteExportedInvite#719c5c5e chatlist:InputChatlist slug:string = Bool;
|
||||
chatlists.editExportedInvite#653db63d flags:# chatlist:InputChatlist slug:string title:flags.1?string peers:flags.2?Vector<InputPeer> = ExportedChatlistInvite;
|
||||
chatlists.getExportedInvites#ce03da83 chatlist:InputChatlist = chatlists.ExportedInvites;
|
||||
chatlists.checkChatlistInvite#41c10fff slug:string = chatlists.ChatlistInvite;
|
||||
chatlists.joinChatlistInvite#a6b1e39a slug:string peers:Vector<InputPeer> = Updates;
|
||||
chatlists.getLeaveChatlistSuggestions#fdbcd714 chatlist:InputChatlist = Vector<Peer>;
|
||||
chatlists.leaveChatlist#74fae13a chatlist:InputChatlist peers:Vector<InputPeer> = Updates;`;
|
||||
@ -281,5 +281,13 @@
|
||||
"channels.deleteTopicHistory",
|
||||
"channels.toggleParticipantsHidden",
|
||||
"photos.uploadContactProfilePhoto",
|
||||
"messages.getMessagesViews"
|
||||
"messages.getMessagesViews",
|
||||
"chatlists.exportChatlistInvite",
|
||||
"chatlists.deleteExportedInvite",
|
||||
"chatlists.editExportedInvite",
|
||||
"chatlists.getExportedInvites",
|
||||
"chatlists.checkChatlistInvite",
|
||||
"chatlists.joinChatlistInvite",
|
||||
"chatlists.getLeaveChatlistSuggestions",
|
||||
"chatlists.leaveChatlist"
|
||||
]
|
||||
|
||||
@ -193,6 +193,7 @@ export enum SettingsScreens {
|
||||
FoldersCreateFolder,
|
||||
FoldersEditFolder,
|
||||
FoldersEditFolderFromChatList,
|
||||
FoldersEditFolderInvites,
|
||||
FoldersIncludedChats,
|
||||
FoldersIncludedChatsFromChatList,
|
||||
FoldersExcludedChats,
|
||||
@ -228,6 +229,7 @@ export enum SettingsScreens {
|
||||
QuickReaction,
|
||||
CustomEmoji,
|
||||
DoNotTranslate,
|
||||
FoldersShare,
|
||||
}
|
||||
|
||||
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||
|
||||
@ -7,7 +7,7 @@ import { IS_SAFARI } from './windowEnvironment';
|
||||
|
||||
type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
||||
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
||||
'invoice';
|
||||
'invoice' | 'addlist';
|
||||
|
||||
export const processDeepLink = (url: string) => {
|
||||
const {
|
||||
@ -26,6 +26,7 @@ export const processDeepLink = (url: string) => {
|
||||
openInvoice,
|
||||
processAttachBotParameters,
|
||||
openChatWithDraft,
|
||||
checkChatlistInvite,
|
||||
} = getActions();
|
||||
|
||||
// Safari thinks the path in tg://path links is hostname for some reason
|
||||
@ -118,6 +119,11 @@ export const processDeepLink = (url: string) => {
|
||||
openChatWithDraft({ text: formatShareText(urlParam, text) });
|
||||
break;
|
||||
}
|
||||
case 'addlist': {
|
||||
checkChatlistInvite({ slug: params.slug });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'login': {
|
||||
// const { code, token } = params;
|
||||
break;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user