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 LimitType = 'default' | 'premium';
|
||||||
type Limit = 'upload_max_fileparts' | 'stickers_faved_limit' | 'saved_gifs_limit' | 'dialog_filters_chats_limit' |
|
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' |
|
'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 LimitKey = `${Limit}_${LimitType}`;
|
||||||
type LimitsConfig = Record<LimitKey, number>;
|
type LimitsConfig = Record<LimitKey, number>;
|
||||||
|
|
||||||
@ -99,6 +99,8 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
channels: getLimit(appConfig, 'channels_limit', 'channels'),
|
channels: getLimit(appConfig, 'channels_limit', 'channels'),
|
||||||
channelsPublic: getLimit(appConfig, 'channels_public_limit', 'channelsPublic'),
|
channelsPublic: getLimit(appConfig, 'channels_public_limit', 'channelsPublic'),
|
||||||
aboutLength: getLimit(appConfig, 'about_length_limit', 'aboutLength'),
|
aboutLength: getLimit(appConfig, 'about_length_limit', 'aboutLength'),
|
||||||
|
chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'),
|
||||||
|
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
|
||||||
},
|
},
|
||||||
hash,
|
hash,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,8 @@ import type {
|
|||||||
ApiTopic,
|
ApiTopic,
|
||||||
ApiSendAsPeerId,
|
ApiSendAsPeerId,
|
||||||
ApiChatReactions,
|
ApiChatReactions,
|
||||||
|
ApiChatlistInvite,
|
||||||
|
ApiChatlistExportedInvite,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { pick, pickTruthy } from '../../../util/iteratees';
|
import { pick, pickTruthy } from '../../../util/iteratees';
|
||||||
import {
|
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 {
|
return {
|
||||||
...pickTruthy(filter, [
|
...pickTruthy(filter, [
|
||||||
'id', 'title', 'emoticon', 'contacts', 'nonContacts', 'groups', 'bots',
|
'id', 'title', 'emoticon', 'contacts', 'nonContacts', 'groups', 'bots',
|
||||||
@ -382,7 +397,7 @@ export function buildApiChatFolderFromSuggested({
|
|||||||
filter: GramJs.TypeDialogFilter;
|
filter: GramJs.TypeDialogFilter;
|
||||||
description: string;
|
description: string;
|
||||||
}): ApiChatFolder | undefined {
|
}): ApiChatFolder | undefined {
|
||||||
if (!(filter instanceof GramJs.DialogFilter)) return undefined;
|
if (!(filter instanceof GramJs.DialogFilter || filter instanceof GramJs.DialogFilterChatlist)) return undefined;
|
||||||
return {
|
return {
|
||||||
...buildApiChatFolder(filter),
|
...buildApiChatFolder(filter),
|
||||||
description,
|
description,
|
||||||
@ -441,12 +456,14 @@ export function buildChatInviteImporter(importer: GramJs.ChatInviteImporter): Ap
|
|||||||
date,
|
date,
|
||||||
about,
|
about,
|
||||||
requested,
|
requested,
|
||||||
|
viaChatlist,
|
||||||
} = importer;
|
} = importer;
|
||||||
return {
|
return {
|
||||||
userId: buildApiPeerId(userId, 'user'),
|
userId: buildApiPeerId(userId, 'user'),
|
||||||
date,
|
date,
|
||||||
about,
|
about,
|
||||||
isRequested: requested,
|
isRequested: requested,
|
||||||
|
isFromChatList: viaChatlist,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -534,3 +551,45 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
|
|||||||
isMuted: silent || (muteUntil !== undefined ? muteUntil > 0 : undefined),
|
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 {
|
const {
|
||||||
emoticon,
|
emoticon,
|
||||||
contacts,
|
contacts,
|
||||||
@ -253,6 +253,17 @@ export function buildFilterFromApiFolder(folder: ApiChatFolder): GramJs.DialogFi
|
|||||||
? excludedChatIds.map(buildInputPeerFromLocalDb).filter(Boolean)
|
? 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({
|
return new GramJs.DialogFilter({
|
||||||
id: folder.id,
|
id: folder.id,
|
||||||
title: folder.title,
|
title: folder.title,
|
||||||
|
|||||||
@ -24,6 +24,12 @@ export function resolveMessageApiChatId(mtpMessage: GramJs.TypeMessage) {
|
|||||||
return getApiChatIdFromMtpPeer(mtpMessage.peerId);
|
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) {
|
export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageService) {
|
||||||
const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`;
|
const messageFullId = `${resolveMessageApiChatId(message)}-${message.id}`;
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,7 @@ import {
|
|||||||
buildApiChatSettings,
|
buildApiChatSettings,
|
||||||
buildApiChatReactions,
|
buildApiChatReactions,
|
||||||
buildApiTopic,
|
buildApiTopic,
|
||||||
|
buildApiChatlistInvite, buildApiChatlistExportedInvite,
|
||||||
} from '../apiBuilders/chats';
|
} from '../apiBuilders/chats';
|
||||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||||
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
import { buildApiUser, buildApiUsersAndStatuses } from '../apiBuilders/users';
|
||||||
@ -56,7 +57,9 @@ import {
|
|||||||
buildInputPhoto,
|
buildInputPhoto,
|
||||||
generateRandomBigInt,
|
generateRandomBigInt,
|
||||||
} from '../gramjsBuilders';
|
} from '../gramjsBuilders';
|
||||||
import { addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb } from '../helpers';
|
import {
|
||||||
|
addEntitiesWithPhotosToLocalDb, addMessageToLocalDb, addPhotoToLocalDb, isChatFolder,
|
||||||
|
} from '../helpers';
|
||||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||||
import { buildApiPhoto } from '../apiBuilders/common';
|
import { buildApiPhoto } from '../apiBuilders/common';
|
||||||
import { buildStickerSet } from '../apiBuilders/symbols';
|
import { buildStickerSet } from '../apiBuilders/symbols';
|
||||||
@ -816,7 +819,7 @@ export async function fetchChatFolders() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultFolderPosition = result.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault);
|
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);
|
const orderedIds = dialogFilters.map(({ id }) => id);
|
||||||
if (defaultFolderPosition !== -1) {
|
if (defaultFolderPosition !== -1) {
|
||||||
orderedIds.splice(defaultFolderPosition, 0, ALL_FOLDER_ID);
|
orderedIds.splice(defaultFolderPosition, 0, ALL_FOLDER_ID);
|
||||||
@ -1559,3 +1562,149 @@ export function editTopic({
|
|||||||
hidden: isHidden,
|
hidden: isHidden,
|
||||||
}), true);
|
}), 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,
|
updateChatTitle, updateChatAbout, toggleSignatures, updateChatAdmin, fetchGroupsForDiscussion, setDiscussionGroup,
|
||||||
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
migrateChat, openChatByInvite, fetchMembers, importChatInvite, addChatMembers, deleteChatMember, toggleIsProtected,
|
||||||
getChatByPhoneNumber, toggleJoinToSend, toggleJoinRequest, fetchTopics, deleteTopic, togglePinnedTopic,
|
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';
|
} from './chats';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@ -48,6 +48,7 @@ import {
|
|||||||
serializeBytes,
|
serializeBytes,
|
||||||
log,
|
log,
|
||||||
swapLocalInvoiceMedia,
|
swapLocalInvoiceMedia,
|
||||||
|
isChatFolder,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import {
|
import {
|
||||||
buildApiNotifyException,
|
buildApiNotifyException,
|
||||||
@ -579,7 +580,7 @@ export function updater(update: Update) {
|
|||||||
});
|
});
|
||||||
} else if (update instanceof GramJs.UpdateDialogFilter) {
|
} else if (update instanceof GramJs.UpdateDialogFilter) {
|
||||||
const { id, filter } = update;
|
const { id, filter } = update;
|
||||||
const folder = filter instanceof GramJs.DialogFilter ? buildApiChatFolder(filter) : undefined;
|
const folder = isChatFolder(filter) ? buildApiChatFolder(filter) : undefined;
|
||||||
|
|
||||||
onUpdate({
|
onUpdate({
|
||||||
'@type': 'updateChatFolder',
|
'@type': 'updateChatFolder',
|
||||||
|
|||||||
@ -187,6 +187,8 @@ export interface ApiChatFolder {
|
|||||||
pinnedChatIds?: string[];
|
pinnedChatIds?: string[];
|
||||||
includedChatIds: string[];
|
includedChatIds: string[];
|
||||||
excludedChatIds: string[];
|
excludedChatIds: string[];
|
||||||
|
isChatList?: true;
|
||||||
|
hasMyInvites?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiChatSettings {
|
export interface ApiChatSettings {
|
||||||
@ -222,3 +224,25 @@ export interface ApiTopic {
|
|||||||
|
|
||||||
isMuted?: boolean;
|
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;
|
date: number;
|
||||||
isRequested?: boolean;
|
isRequested?: boolean;
|
||||||
about?: string;
|
about?: string;
|
||||||
|
isFromChatList?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ApiCountry {
|
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 PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
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 AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||||
export { default as CalendarModal } from '../components/common/CalendarModal';
|
export { default as CalendarModal } from '../components/common/CalendarModal';
|
||||||
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';
|
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, {
|
import React, {
|
||||||
useCallback, useRef, useEffect, memo,
|
useCallback, useRef, useEffect, memo, useMemo,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
|
|
||||||
|
import type { FC } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import { isUserId } from '../../global/helpers';
|
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 InfiniteScroll from '../ui/InfiniteScroll';
|
||||||
import Checkbox from '../ui/Checkbox';
|
import Checkbox from '../ui/Checkbox';
|
||||||
@ -13,8 +17,6 @@ import ListItem from '../ui/ListItem';
|
|||||||
import PrivateChatInfo from './PrivateChatInfo';
|
import PrivateChatInfo from './PrivateChatInfo';
|
||||||
import GroupChatInfo from './GroupChatInfo';
|
import GroupChatInfo from './GroupChatInfo';
|
||||||
import PickerSelectedItem from './PickerSelectedItem';
|
import PickerSelectedItem from './PickerSelectedItem';
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
|
||||||
import useLang from '../../hooks/useLang';
|
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
import Loading from '../ui/Loading';
|
||||||
|
|
||||||
@ -29,8 +31,11 @@ type OwnProps = {
|
|||||||
searchInputId?: string;
|
searchInputId?: string;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
noScrollRestore?: boolean;
|
noScrollRestore?: boolean;
|
||||||
onSelectedIdsChange: (ids: string[]) => void;
|
isSearchable?: boolean;
|
||||||
onFilterChange: (value: string) => void;
|
lockedIds?: string[];
|
||||||
|
onSelectedIdsChange?: (ids: string[]) => void;
|
||||||
|
onFilterChange?: (value: string) => void;
|
||||||
|
onDisabledClick?: (id: string) => void;
|
||||||
onLoadMore?: () => void;
|
onLoadMore?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -49,8 +54,11 @@ const Picker: FC<OwnProps> = ({
|
|||||||
searchInputId,
|
searchInputId,
|
||||||
isLoading,
|
isLoading,
|
||||||
noScrollRestore,
|
noScrollRestore,
|
||||||
|
isSearchable,
|
||||||
|
lockedIds,
|
||||||
onSelectedIdsChange,
|
onSelectedIdsChange,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
|
onDisabledClick,
|
||||||
onLoadMore,
|
onLoadMore,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -58,53 +66,93 @@ const Picker: FC<OwnProps> = ({
|
|||||||
const shouldMinimize = selectedIds.length > MAX_FULL_ITEMS;
|
const shouldMinimize = selectedIds.length > MAX_FULL_ITEMS;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!isSearchable) return;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
requestMutation(() => {
|
requestMutation(() => {
|
||||||
inputRef.current!.focus();
|
inputRef.current!.focus();
|
||||||
});
|
});
|
||||||
}, FOCUS_DELAY_MS);
|
}, 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 handleItemClick = useCallback((id: string) => {
|
||||||
const newSelectedIds = [...selectedIds];
|
if (lockedIdsSet.has(id)) {
|
||||||
|
onDisabledClick?.(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSelectedIds = selectedIds.slice();
|
||||||
if (newSelectedIds.includes(id)) {
|
if (newSelectedIds.includes(id)) {
|
||||||
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
|
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
|
||||||
} else {
|
} else {
|
||||||
newSelectedIds.push(id);
|
newSelectedIds.push(id);
|
||||||
}
|
}
|
||||||
onSelectedIdsChange(newSelectedIds);
|
onSelectedIdsChange?.(newSelectedIds);
|
||||||
onFilterChange('');
|
onFilterChange?.('');
|
||||||
}, [selectedIds, onSelectedIdsChange, onFilterChange]);
|
}, [lockedIdsSet, selectedIds, onSelectedIdsChange, onFilterChange, onDisabledClick]);
|
||||||
|
|
||||||
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFilterChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const { value } = e.currentTarget;
|
const { value } = e.currentTarget;
|
||||||
onFilterChange(value);
|
onFilterChange?.(value);
|
||||||
}, [onFilterChange]);
|
}, [onFilterChange]);
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, itemIds, Boolean(filterValue));
|
const [viewportIds, getMore] = useInfiniteScroll(onLoadMore, sortedItemIds, Boolean(filterValue));
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Picker">
|
<div className="Picker">
|
||||||
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
{isSearchable && (
|
||||||
{selectedIds.map((id, i) => (
|
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
<PickerSelectedItem
|
{lockedSelectedIds.map((id, i) => (
|
||||||
chatOrUserId={id}
|
<PickerSelectedItem
|
||||||
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
chatOrUserId={id}
|
||||||
canClose
|
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
|
||||||
onClick={handleItemClick}
|
onClick={handleItemClick}
|
||||||
clickArg={id}
|
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')}
|
||||||
/>
|
/>
|
||||||
))}
|
</div>
|
||||||
<InputText
|
)}
|
||||||
id={searchInputId}
|
|
||||||
ref={inputRef}
|
|
||||||
value={filterValue}
|
|
||||||
onChange={handleFilterChange}
|
|
||||||
placeholder={filterPlaceholder || lang('SelectChat')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{viewportIds?.length ? (
|
{viewportIds?.length ? (
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
@ -117,11 +165,13 @@ const Picker: FC<OwnProps> = ({
|
|||||||
<ListItem
|
<ListItem
|
||||||
key={id}
|
key={id}
|
||||||
className="chat-item-clickable picker-list-item"
|
className="chat-item-clickable picker-list-item"
|
||||||
|
disabled={lockedIdsSet.has(id)}
|
||||||
|
allowDisabledClick={Boolean(onDisabledClick)}
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
onClick={() => handleItemClick(id)}
|
onClick={() => handleItemClick(id)}
|
||||||
ripple
|
ripple
|
||||||
>
|
>
|
||||||
<Checkbox label="" checked={selectedIds.includes(id)} />
|
<Checkbox label="" disabled={lockedIdsSet.has(id)} checked={selectedIds.includes(id)} />
|
||||||
{isUserId(id) ? (
|
{isUserId(id) ? (
|
||||||
<PrivateChatInfo userId={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 FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.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 DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs';
|
||||||
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||||
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
|
||||||
@ -31,6 +32,7 @@ export const LOCAL_TGS_URLS = {
|
|||||||
MonkeyPeek,
|
MonkeyPeek,
|
||||||
FoldersAll,
|
FoldersAll,
|
||||||
FoldersNew,
|
FoldersNew,
|
||||||
|
FoldersShare,
|
||||||
DiscussionGroups,
|
DiscussionGroups,
|
||||||
Lock,
|
Lock,
|
||||||
CameraFlip,
|
CameraFlip,
|
||||||
|
|||||||
@ -6,10 +6,12 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import type { GlobalState } from '../../global/types';
|
import type { GlobalState } from '../../global/types';
|
||||||
import { LeftColumnContent, SettingsScreens } from '../../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 { IS_MAC_OS, IS_PWA, LAYERS_ANIMATION_NAME } from '../../util/windowEnvironment';
|
||||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
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 useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
|
||||||
import { useResize } from '../../hooks/useResize';
|
import { useResize } from '../../hooks/useResize';
|
||||||
import { useHotkeys } from '../../hooks/useHotkeys';
|
import { useHotkeys } from '../../hooks/useHotkeys';
|
||||||
@ -32,6 +34,7 @@ type StateProps = {
|
|||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
hasPasscode?: boolean;
|
hasPasscode?: boolean;
|
||||||
nextSettingsScreen?: SettingsScreens;
|
nextSettingsScreen?: SettingsScreens;
|
||||||
|
nextFoldersAction?: ReducerAction<FoldersActions>;
|
||||||
isChatOpen: boolean;
|
isChatOpen: boolean;
|
||||||
isUpdateAvailable?: boolean;
|
isUpdateAvailable?: boolean;
|
||||||
isForumPanelOpen?: boolean;
|
isForumPanelOpen?: boolean;
|
||||||
@ -63,6 +66,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
currentUserId,
|
currentUserId,
|
||||||
hasPasscode,
|
hasPasscode,
|
||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
|
nextFoldersAction,
|
||||||
isChatOpen,
|
isChatOpen,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
@ -115,6 +119,7 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
||||||
function fullReset() {
|
function fullReset() {
|
||||||
setContent(LeftColumnContent.ChatList);
|
setContent(LeftColumnContent.ChatList);
|
||||||
|
setSettingsScreen(SettingsScreens.Main);
|
||||||
setContactsFilter('');
|
setContactsFilter('');
|
||||||
setGlobalSearchClosing({ isClosing: true });
|
setGlobalSearchClosing({ isClosing: true });
|
||||||
resetChatCreation();
|
resetChatCreation();
|
||||||
@ -285,12 +290,17 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
setSettingsScreen(SettingsScreens.Folders);
|
setSettingsScreen(SettingsScreens.Folders);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
case SettingsScreens.FoldersShare:
|
||||||
|
setSettingsScreen(SettingsScreens.FoldersEditFolder);
|
||||||
|
return;
|
||||||
|
|
||||||
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
||||||
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
||||||
setSettingsScreen(SettingsScreens.FoldersEditFolderFromChatList);
|
setSettingsScreen(SettingsScreens.FoldersEditFolderFromChatList);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||||
|
case SettingsScreens.FoldersEditFolderInvites:
|
||||||
setContent(LeftColumnContent.ChatList);
|
setContent(LeftColumnContent.ChatList);
|
||||||
setSettingsScreen(SettingsScreens.Main);
|
setSettingsScreen(SettingsScreens.Main);
|
||||||
return;
|
return;
|
||||||
@ -394,7 +404,11 @@ const LeftColumn: FC<StateProps> = ({
|
|||||||
setSettingsScreen(nextSettingsScreen);
|
setSettingsScreen(nextSettingsScreen);
|
||||||
requestNextSettingsScreen({ screen: undefined });
|
requestNextSettingsScreen({ screen: undefined });
|
||||||
}
|
}
|
||||||
}, [nextSettingsScreen, requestNextSettingsScreen]);
|
|
||||||
|
if (nextFoldersAction) {
|
||||||
|
foldersDispatch(nextFoldersAction);
|
||||||
|
}
|
||||||
|
}, [foldersDispatch, nextFoldersAction, nextSettingsScreen, requestNextSettingsScreen]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
initResize, resetResize, handleMouseUp,
|
initResize, resetResize, handleMouseUp,
|
||||||
@ -513,6 +527,7 @@ export default memo(withGlobal(
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
activeChatFolder,
|
activeChatFolder,
|
||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
|
nextFoldersAction,
|
||||||
} = tabState;
|
} = tabState;
|
||||||
const {
|
const {
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
@ -538,6 +553,7 @@ export default memo(withGlobal(
|
|||||||
currentUserId,
|
currentUserId,
|
||||||
hasPasscode,
|
hasPasscode,
|
||||||
nextSettingsScreen,
|
nextSettingsScreen,
|
||||||
|
nextFoldersAction,
|
||||||
isChatOpen,
|
isChatOpen,
|
||||||
isUpdateAvailable,
|
isUpdateAvailable,
|
||||||
isForumPanelOpen,
|
isForumPanelOpen,
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
selectIsForumPanelOpen,
|
selectIsForumPanelOpen,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
|
|
||||||
import Badge from './Badge';
|
import ChatBadge from './ChatBadge';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -31,7 +31,7 @@ const AvatarBadge: FC<OwnProps & StateProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return chat && (
|
return chat && (
|
||||||
<div className="avatar-badge-wrapper">
|
<div className="avatar-badge-wrapper">
|
||||||
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
|
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={!isForumPanelActive} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -110,21 +110,21 @@
|
|||||||
color: var(--color-white) !important;
|
color: var(--color-white) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge:not(.pinned):not(.muted) {
|
.ChatBadge:not(.pinned):not(.muted) {
|
||||||
color: var(--color-chat-active);
|
color: var(--color-chat-active);
|
||||||
background: var(--color-white);
|
background: var(--color-white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge:not(.pinned).muted {
|
.ChatBadge:not(.pinned).muted {
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
background: #FFFFFF33;
|
background: #FFFFFF33;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-badge-wrapper .Badge:not(.pinned) {
|
.avatar-badge-wrapper .ChatBadge:not(.pinned) {
|
||||||
--outline-color: transparent;
|
--outline-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-badge-wrapper .Badge:not(.pinned).muted {
|
.avatar-badge-wrapper .ChatBadge:not(.pinned).muted {
|
||||||
background: var(--color-gray);
|
background: var(--color-gray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,11 +204,11 @@
|
|||||||
|
|
||||||
--outline-color: var(--color-background);
|
--outline-color: var(--color-background);
|
||||||
|
|
||||||
.Badge {
|
.ChatBadge {
|
||||||
box-shadow: 0 0 0 2px var(--outline-color);
|
box-shadow: 0 0 0 2px var(--outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge-transition {
|
.ChatBadge-transition {
|
||||||
transition: opacity var(--layer-transition), transform var(--layer-transition);
|
transition: opacity var(--layer-transition), transform var(--layer-transition);
|
||||||
|
|
||||||
body.no-page-transitions & {
|
body.no-page-transitions & {
|
||||||
|
|||||||
@ -55,7 +55,7 @@ import ReportModal from '../../common/ReportModal';
|
|||||||
import FullNameTitle from '../../common/FullNameTitle';
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
import ChatFolderModal from '../ChatFolderModal.async';
|
import ChatFolderModal from '../ChatFolderModal.async';
|
||||||
import ChatCallStatus from './ChatCallStatus';
|
import ChatCallStatus from './ChatCallStatus';
|
||||||
import Badge from './Badge';
|
import ChatBadge from './ChatBadge';
|
||||||
import AvatarBadge from './AvatarBadge';
|
import AvatarBadge from './AvatarBadge';
|
||||||
|
|
||||||
import './Chat.scss';
|
import './Chat.scss';
|
||||||
@ -264,7 +264,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{renderSubtitle()}
|
{renderSubtitle()}
|
||||||
<Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
<ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{shouldRenderDeleteModal && (
|
{shouldRenderDeleteModal && (
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
.Badge-transition {
|
.ChatBadge-transition {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
|
||||||
@ -16,15 +16,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge-wrapper {
|
.ChatBadge-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.Badge {
|
.ChatBadge {
|
||||||
margin-inline-start: 0.5rem;
|
margin-inline-start: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.Badge {
|
.ChatBadge {
|
||||||
min-width: 1.5rem;
|
min-width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
background: var(--color-gray);
|
background: var(--color-gray);
|
||||||
@ -9,7 +9,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import ShowTransition from '../../ui/ShowTransition';
|
import ShowTransition from '../../ui/ShowTransition';
|
||||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||||
|
|
||||||
import './Badge.scss';
|
import './ChatBadge.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
@ -21,7 +21,7 @@ type OwnProps = {
|
|||||||
forceHidden?: boolean;
|
forceHidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Badge: FC<OwnProps> = ({
|
const ChatBadge: FC<OwnProps> = ({
|
||||||
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden,
|
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -58,7 +58,7 @@ const Badge: FC<OwnProps> = ({
|
|||||||
|
|
||||||
const isUnread = Boolean(unreadCount || hasUnreadMark);
|
const isUnread = Boolean(unreadCount || hasUnreadMark);
|
||||||
const className = buildClassName(
|
const className = buildClassName(
|
||||||
'Badge',
|
'ChatBadge',
|
||||||
shouldBeMuted && 'muted',
|
shouldBeMuted && 'muted',
|
||||||
!isUnread && isPinned && 'pinned',
|
!isUnread && isPinned && 'pinned',
|
||||||
isUnread && 'unread',
|
isUnread && 'unread',
|
||||||
@ -66,19 +66,19 @@ const Badge: FC<OwnProps> = ({
|
|||||||
|
|
||||||
function renderContent() {
|
function renderContent() {
|
||||||
const unreadReactionsElement = unreadReactionsCount && (
|
const unreadReactionsElement = unreadReactionsCount && (
|
||||||
<div className={buildClassName('Badge reaction', shouldBeMuted && 'muted')}>
|
<div className={buildClassName('ChatBadge reaction', shouldBeMuted && 'muted')}>
|
||||||
<i className="icon icon-heart" />
|
<i className="icon icon-heart" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const unreadMentionsElement = unreadMentionsCount && (
|
const unreadMentionsElement = unreadMentionsCount && (
|
||||||
<div className="Badge mention">
|
<div className="ChatBadge mention">
|
||||||
<i className="icon icon-mention" />
|
<i className="icon icon-mention" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const unopenedTopicElement = isTopicUnopened && (
|
const unopenedTopicElement = isTopicUnopened && (
|
||||||
<div className={buildClassName('Badge unopened', shouldBeMuted && 'muted')} />
|
<div className={buildClassName('ChatBadge unopened', shouldBeMuted && 'muted')} />
|
||||||
);
|
);
|
||||||
|
|
||||||
const unreadCountElement = (hasUnreadMark || unreadCount) ? (
|
const unreadCountElement = (hasUnreadMark || unreadCount) ? (
|
||||||
@ -109,17 +109,17 @@ const Badge: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="Badge-wrapper">
|
<div className="ChatBadge-wrapper">
|
||||||
{elements}
|
{elements}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowTransition isCustom className="Badge-transition" isOpen={isShown}>
|
<ShowTransition isCustom className="ChatBadge-transition" isOpen={isShown}>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</ShowTransition>
|
</ShowTransition>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(Badge);
|
export default memo(ChatBadge);
|
||||||
@ -2,20 +2,22 @@ import type { FC } from '../../../lib/teact/teact';
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useRef,
|
memo, useCallback, useEffect, useMemo, useRef,
|
||||||
} from '../../../lib/teact/teact';
|
} 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 type { LeftColumnContent, SettingsScreens } from '../../../types';
|
import type { SettingsScreens, LeftColumnContent } from '../../../types';
|
||||||
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
import type { GlobalState } from '../../../global/types';
|
import type { GlobalState } from '../../../global/types';
|
||||||
|
import type { TabWithProperties } from '../../ui/TabList';
|
||||||
|
|
||||||
import { ALL_FOLDER_ID } from '../../../config';
|
import { ALL_FOLDER_ID } from '../../../config';
|
||||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
import { selectCurrentLimit } from '../../../global/selectors/limits';
|
||||||
import { selectTabState } from '../../../global/selectors';
|
import { selectCanShareFolder, selectTabState } from '../../../global/selectors';
|
||||||
import useShowTransition from '../../../hooks/useShowTransition';
|
import useShowTransition from '../../../hooks/useShowTransition';
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
@ -35,12 +37,14 @@ type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
chatFoldersById: Record<number, ApiChatFolder>;
|
chatFoldersById: Record<number, ApiChatFolder>;
|
||||||
|
folderInvitesById: Record<number, ApiChatlistExportedInvite[]>;
|
||||||
orderedFolderIds?: number[];
|
orderedFolderIds?: number[];
|
||||||
activeChatFolder: number;
|
activeChatFolder: number;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
lastSyncTime?: number;
|
lastSyncTime?: number;
|
||||||
shouldSkipHistoryAnimations?: boolean;
|
shouldSkipHistoryAnimations?: boolean;
|
||||||
maxFolders: number;
|
maxFolders: number;
|
||||||
|
maxFolderInvites: number;
|
||||||
hasArchivedChats?: boolean;
|
hasArchivedChats?: boolean;
|
||||||
archiveSettings: GlobalState['archiveSettings'];
|
archiveSettings: GlobalState['archiveSettings'];
|
||||||
};
|
};
|
||||||
@ -61,6 +65,8 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
maxFolders,
|
maxFolders,
|
||||||
shouldHideFolderTabs,
|
shouldHideFolderTabs,
|
||||||
|
folderInvitesById,
|
||||||
|
maxFolderInvites,
|
||||||
hasArchivedChats,
|
hasArchivedChats,
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
}) => {
|
}) => {
|
||||||
@ -68,6 +74,10 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
loadChatFolders,
|
loadChatFolders,
|
||||||
setActiveChatFolder,
|
setActiveChatFolder,
|
||||||
openChat,
|
openChat,
|
||||||
|
openShareChatFolderModal,
|
||||||
|
openDeleteChatFolderModal,
|
||||||
|
openEditChatFolder,
|
||||||
|
openLimitReachedModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -81,11 +91,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [lastSyncTime, loadChatFolders]);
|
}, [lastSyncTime, loadChatFolders]);
|
||||||
|
|
||||||
const allChatsFolder = useMemo(() => {
|
const allChatsFolder: ApiChatFolder = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
id: ALL_FOLDER_ID,
|
id: ALL_FOLDER_ID,
|
||||||
title: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats'),
|
title: orderedFolderIds?.[0] === ALL_FOLDER_ID ? lang('FilterAllChatsShort') : lang('FilterAllChats'),
|
||||||
};
|
includedChatIds: MEMO_EMPTY_ARRAY,
|
||||||
|
excludedChatIds: MEMO_EMPTY_ARRAY,
|
||||||
|
} satisfies ApiChatFolder;
|
||||||
}, [orderedFolderIds, lang]);
|
}, [orderedFolderIds, lang]);
|
||||||
|
|
||||||
const displayedFolders = useMemo(() => {
|
const displayedFolders = useMemo(() => {
|
||||||
@ -110,18 +122,63 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return displayedFolders.map(({ id, title }, i) => {
|
const global = getGlobal();
|
||||||
const isBlocked = id !== ALL_FOLDER_ID && i > maxFolders - 1;
|
|
||||||
|
|
||||||
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,
|
id,
|
||||||
title,
|
title,
|
||||||
badgeCount: folderCountersById[id]?.chatsCount,
|
badgeCount: folderCountersById[id]?.chatsCount,
|
||||||
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
|
isBadgeActive: Boolean(folderCountersById[id]?.notificationsCount),
|
||||||
isBlocked,
|
isBlocked,
|
||||||
});
|
contextActions: contextActions?.length ? contextActions : undefined,
|
||||||
|
} satisfies TabWithProperties;
|
||||||
});
|
});
|
||||||
}, [displayedFolders, folderCountersById, maxFolders]);
|
}, [displayedFolders, folderCountersById, lang, maxFolders, folderInvitesById, maxFolderInvites]);
|
||||||
|
|
||||||
const handleSwitchTab = useCallback((index: number) => {
|
const handleSwitchTab = useCallback((index: number) => {
|
||||||
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
|
setActiveChatFolder({ activeChatFolder: index }, { forceOnHeavyAnimation: true });
|
||||||
@ -236,7 +293,13 @@ const ChatFolders: FC<OwnProps & StateProps> = ({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{shouldRenderFolders ? (
|
{shouldRenderFolders ? (
|
||||||
<TabList tabs={folderTabs} activeTab={activeChatFolder} onSwitchTab={handleSwitchTab} areFolders />
|
<TabList
|
||||||
|
contextRootElementSelector="#LeftColumn"
|
||||||
|
tabs={folderTabs}
|
||||||
|
activeTab={activeChatFolder}
|
||||||
|
onSwitchTab={handleSwitchTab}
|
||||||
|
areFolders
|
||||||
|
/>
|
||||||
) : shouldRenderPlaceholder ? (
|
) : shouldRenderPlaceholder ? (
|
||||||
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
<div className={buildClassName('tabs-placeholder', transitionClassNames)} />
|
||||||
) : undefined}
|
) : undefined}
|
||||||
@ -258,6 +321,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
chatFolders: {
|
chatFolders: {
|
||||||
byId: chatFoldersById,
|
byId: chatFoldersById,
|
||||||
orderedIds: orderedFolderIds,
|
orderedIds: orderedFolderIds,
|
||||||
|
invites: folderInvitesById,
|
||||||
},
|
},
|
||||||
chats: {
|
chats: {
|
||||||
listIds: {
|
listIds: {
|
||||||
@ -272,6 +336,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
chatFoldersById,
|
chatFoldersById,
|
||||||
|
folderInvitesById,
|
||||||
orderedFolderIds,
|
orderedFolderIds,
|
||||||
activeChatFolder,
|
activeChatFolder,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
@ -279,6 +344,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
hasArchivedChats: Boolean(archived?.length),
|
hasArchivedChats: Boolean(archived?.length),
|
||||||
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
maxFolders: selectCurrentLimit(global, 'dialogFilters'),
|
||||||
|
maxFolderInvites: selectCurrentLimit(global, 'chatlistInvites'),
|
||||||
archiveSettings,
|
archiveSettings,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -35,7 +35,7 @@ import useLang from '../../../hooks/useLang';
|
|||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import LastMessageMeta from '../../common/LastMessageMeta';
|
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||||
import Badge from './Badge';
|
import ChatBadge from './ChatBadge';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import TopicIcon from '../../common/TopicIcon';
|
import TopicIcon from '../../common/TopicIcon';
|
||||||
|
|
||||||
@ -179,7 +179,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="subtitle">
|
<div className="subtitle">
|
||||||
{renderSubtitle()}
|
{renderSubtitle()}
|
||||||
<Badge
|
<ChatBadge
|
||||||
chat={chat}
|
chat={chat}
|
||||||
isPinned={isPinned}
|
isPinned={isPinned}
|
||||||
isMuted={isMuted}
|
isMuted={isMuted}
|
||||||
|
|||||||
@ -112,6 +112,7 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
|
|||||||
filterPlaceholder={lang('SendMessageTo')}
|
filterPlaceholder={lang('SendMessageTo')}
|
||||||
searchInputId="new-group-picker-search"
|
searchInputId="new-group-picker-search"
|
||||||
isLoading={isSearching}
|
isLoading={isSearching}
|
||||||
|
isSearchable
|
||||||
onSelectedIdsChange={onSelectedMemberIdsChange}
|
onSelectedIdsChange={onSelectedMemberIdsChange}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -380,3 +380,11 @@
|
|||||||
margin-inline-end: 2rem;
|
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 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 { SettingsScreens } from '../../../types';
|
||||||
import type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch, FoldersState } from '../../../hooks/reducers/useFoldersReducer';
|
||||||
|
|
||||||
import { LAYERS_ANIMATION_NAME } from '../../../util/windowEnvironment';
|
import { LAYERS_ANIMATION_NAME } from '../../../util/windowEnvironment';
|
||||||
|
import { selectTabState } from '../../../global/selectors';
|
||||||
import useTwoFaReducer from '../../../hooks/reducers/useTwoFaReducer';
|
import useTwoFaReducer from '../../../hooks/reducers/useTwoFaReducer';
|
||||||
|
|
||||||
import Transition from '../../ui/Transition';
|
import Transition from '../../ui/Transition';
|
||||||
@ -67,10 +69,12 @@ const FOLDERS_SCREENS = [
|
|||||||
SettingsScreens.FoldersCreateFolder,
|
SettingsScreens.FoldersCreateFolder,
|
||||||
SettingsScreens.FoldersEditFolder,
|
SettingsScreens.FoldersEditFolder,
|
||||||
SettingsScreens.FoldersEditFolderFromChatList,
|
SettingsScreens.FoldersEditFolderFromChatList,
|
||||||
|
SettingsScreens.FoldersEditFolderInvites,
|
||||||
SettingsScreens.FoldersIncludedChats,
|
SettingsScreens.FoldersIncludedChats,
|
||||||
SettingsScreens.FoldersIncludedChatsFromChatList,
|
SettingsScreens.FoldersIncludedChatsFromChatList,
|
||||||
SettingsScreens.FoldersExcludedChats,
|
SettingsScreens.FoldersExcludedChats,
|
||||||
SettingsScreens.FoldersExcludedChatsFromChatList,
|
SettingsScreens.FoldersExcludedChatsFromChatList,
|
||||||
|
SettingsScreens.FoldersShare,
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRIVACY_SCREENS = [
|
const PRIVACY_SCREENS = [
|
||||||
@ -137,11 +141,18 @@ const Settings: FC<OwnProps> = ({
|
|||||||
onReset,
|
onReset,
|
||||||
shouldSkipTransition,
|
shouldSkipTransition,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { closeShareChatFolderModal } = getActions();
|
||||||
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
|
const [twoFaState, twoFaDispatch] = useTwoFaReducer();
|
||||||
const [privacyPasscode, setPrivacyPasscode] = useState<string>('');
|
const [privacyPasscode, setPrivacyPasscode] = useState<string>('');
|
||||||
|
|
||||||
const handleReset = useCallback((forceReturnToChatList?: true | Event) => {
|
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);
|
onReset(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -150,6 +161,7 @@ const Settings: FC<OwnProps> = ({
|
|||||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||||
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
||||||
|
|| currentScreen === SettingsScreens.FoldersEditFolderInvites
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
foldersDispatch({ type: 'reset' });
|
foldersDispatch({ type: 'reset' });
|
||||||
@ -361,10 +373,12 @@ const Settings: FC<OwnProps> = ({
|
|||||||
case SettingsScreens.FoldersCreateFolder:
|
case SettingsScreens.FoldersCreateFolder:
|
||||||
case SettingsScreens.FoldersEditFolder:
|
case SettingsScreens.FoldersEditFolder:
|
||||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||||
|
case SettingsScreens.FoldersEditFolderInvites:
|
||||||
case SettingsScreens.FoldersIncludedChats:
|
case SettingsScreens.FoldersIncludedChats:
|
||||||
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
case SettingsScreens.FoldersIncludedChatsFromChatList:
|
||||||
case SettingsScreens.FoldersExcludedChats:
|
case SettingsScreens.FoldersExcludedChats:
|
||||||
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
case SettingsScreens.FoldersExcludedChatsFromChatList:
|
||||||
|
case SettingsScreens.FoldersShare:
|
||||||
return (
|
return (
|
||||||
<SettingsFolders
|
<SettingsFolders
|
||||||
currentScreen={currentScreen}
|
currentScreen={currentScreen}
|
||||||
|
|||||||
@ -201,8 +201,11 @@ const SettingsHeader: FC<OwnProps> = ({
|
|||||||
return <h3>{lang('Filters')}</h3>;
|
return <h3>{lang('Filters')}</h3>;
|
||||||
case SettingsScreens.FoldersCreateFolder:
|
case SettingsScreens.FoldersCreateFolder:
|
||||||
return <h3>{lang('FilterNew')}</h3>;
|
return <h3>{lang('FilterNew')}</h3>;
|
||||||
|
case SettingsScreens.FoldersShare:
|
||||||
|
return <h3>{lang('FolderLinkScreen.Title')}</h3>;
|
||||||
case SettingsScreens.FoldersEditFolder:
|
case SettingsScreens.FoldersEditFolder:
|
||||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||||
|
case SettingsScreens.FoldersEditFolderInvites:
|
||||||
return (
|
return (
|
||||||
<div className="settings-main-header">
|
<div className="settings-main-header">
|
||||||
<h3>{lang('FilterEdit')}</h3>
|
<h3>{lang('FilterEdit')}</h3>
|
||||||
|
|||||||
@ -107,6 +107,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
|
|||||||
filterValue={searchQuery}
|
filterValue={searchQuery}
|
||||||
filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')}
|
filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')}
|
||||||
searchInputId="new-group-picker-search"
|
searchInputId="new-group-picker-search"
|
||||||
|
isSearchable
|
||||||
onSelectedIdsChange={handleSelectedContactIdsChange}
|
onSelectedIdsChange={handleSelectedContactIdsChange}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import type { FC } from '../../../../lib/teact/teact';
|
|
||||||
import React, { memo, useCallback } 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 type { ApiChatFolder } from '../../../../api/types';
|
||||||
import { SettingsScreens } from '../../../../types';
|
import { SettingsScreens } from '../../../../types';
|
||||||
|
|
||||||
import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
|
import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer';
|
||||||
|
|
||||||
import SettingsFoldersMain from './SettingsFoldersMain';
|
import SettingsFoldersMain from './SettingsFoldersMain';
|
||||||
import SettingsFoldersEdit from './SettingsFoldersEdit';
|
import SettingsFoldersEdit from './SettingsFoldersEdit';
|
||||||
import SettingsFoldersChatFilters from './SettingsFoldersChatFilters';
|
import SettingsFoldersChatFilters from './SettingsFoldersChatFilters';
|
||||||
|
import SettingsShareChatlist from './SettingsShareChatlist';
|
||||||
|
|
||||||
import './SettingsFolders.scss';
|
import './SettingsFolders.scss';
|
||||||
|
|
||||||
@ -33,11 +34,14 @@ const SettingsFolders: FC<OwnProps> = ({
|
|||||||
onScreenSelect,
|
onScreenSelect,
|
||||||
onReset,
|
onReset,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { openShareChatFolderModal } = getActions();
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
if (
|
if (
|
||||||
currentScreen === SettingsScreens.FoldersCreateFolder
|
currentScreen === SettingsScreens.FoldersCreateFolder
|
||||||
|| currentScreen === SettingsScreens.FoldersEditFolder
|
|| currentScreen === SettingsScreens.FoldersEditFolder
|
||||||
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
|| currentScreen === SettingsScreens.FoldersEditFolderFromChatList
|
||||||
|
|| currentScreen === SettingsScreens.FoldersEditFolderInvites
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
dispatch({ type: 'reset' });
|
dispatch({ type: 'reset' });
|
||||||
@ -86,6 +90,16 @@ const SettingsFolders: FC<OwnProps> = ({
|
|||||||
: SettingsScreens.FoldersExcludedChats);
|
: SettingsScreens.FoldersExcludedChats);
|
||||||
}, [currentScreen, dispatch, onScreenSelect]);
|
}, [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) {
|
switch (currentScreen) {
|
||||||
case SettingsScreens.Folders:
|
case SettingsScreens.Folders:
|
||||||
return (
|
return (
|
||||||
@ -104,17 +118,21 @@ const SettingsFolders: FC<OwnProps> = ({
|
|||||||
case SettingsScreens.FoldersCreateFolder:
|
case SettingsScreens.FoldersCreateFolder:
|
||||||
case SettingsScreens.FoldersEditFolder:
|
case SettingsScreens.FoldersEditFolder:
|
||||||
case SettingsScreens.FoldersEditFolderFromChatList:
|
case SettingsScreens.FoldersEditFolderFromChatList:
|
||||||
|
case SettingsScreens.FoldersEditFolderInvites:
|
||||||
return (
|
return (
|
||||||
<SettingsFoldersEdit
|
<SettingsFoldersEdit
|
||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
onAddIncludedChats={handleAddIncludedChats}
|
onAddIncludedChats={handleAddIncludedChats}
|
||||||
onAddExcludedChats={handleAddExcludedChats}
|
onAddExcludedChats={handleAddExcludedChats}
|
||||||
|
onShareFolder={handleShareFolder}
|
||||||
|
onOpenInvite={handleOpenInvite}
|
||||||
onReset={handleReset}
|
onReset={handleReset}
|
||||||
isActive={isActive || [
|
isActive={isActive || [
|
||||||
SettingsScreens.FoldersIncludedChats,
|
SettingsScreens.FoldersIncludedChats,
|
||||||
SettingsScreens.FoldersExcludedChats,
|
SettingsScreens.FoldersExcludedChats,
|
||||||
].includes(shownScreen)}
|
].includes(shownScreen)}
|
||||||
|
isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites}
|
||||||
onBack={onReset}
|
onBack={onReset}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -141,6 +159,14 @@ const SettingsFolders: FC<OwnProps> = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
case SettingsScreens.FoldersShare:
|
||||||
|
return (
|
||||||
|
<SettingsShareChatlist
|
||||||
|
isActive={isActive}
|
||||||
|
onReset={handleReset}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,8 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
|||||||
const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID);
|
const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID);
|
||||||
const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
|
||||||
|
|
||||||
|
const shouldHideChatTypes = state.folder.isChatList;
|
||||||
|
|
||||||
const displayedIds = useMemo(() => {
|
const displayedIds = useMemo(() => {
|
||||||
// No need for expensive global updates on chats, so we avoid them
|
// No need for expensive global updates on chats, so we avoid them
|
||||||
const chatsById = getGlobal().chats.byId;
|
const chatsById = getGlobal().chats.byId;
|
||||||
@ -116,6 +118,7 @@ const SettingsFoldersChatFilters: FC<OwnProps> = ({
|
|||||||
selectedIds={selectedChatIds}
|
selectedIds={selectedChatIds}
|
||||||
selectedChatTypes={selectedChatTypes}
|
selectedChatTypes={selectedChatTypes}
|
||||||
filterValue={chatFilter}
|
filterValue={chatFilter}
|
||||||
|
shouldHideChatTypes={shouldHideChatTypes}
|
||||||
onSelectedIdsChange={handleSelectedIdsChange}
|
onSelectedIdsChange={handleSelectedIdsChange}
|
||||||
onSelectedChatTypesChange={handleSelectedChatTypesChange}
|
onSelectedChatTypesChange={handleSelectedChatTypesChange}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
|
|||||||
@ -34,6 +34,7 @@ type OwnProps = {
|
|||||||
selectedIds: string[];
|
selectedIds: string[];
|
||||||
selectedChatTypes: string[];
|
selectedChatTypes: string[];
|
||||||
filterValue?: string;
|
filterValue?: string;
|
||||||
|
shouldHideChatTypes?: boolean;
|
||||||
onSelectedIdsChange: (ids: string[]) => void;
|
onSelectedIdsChange: (ids: string[]) => void;
|
||||||
onSelectedChatTypesChange: (types: string[]) => void;
|
onSelectedChatTypesChange: (types: string[]) => void;
|
||||||
onFilterChange: (value: string) => void;
|
onFilterChange: (value: string) => void;
|
||||||
@ -55,6 +56,7 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
|
|||||||
selectedIds,
|
selectedIds,
|
||||||
selectedChatTypes,
|
selectedChatTypes,
|
||||||
filterValue,
|
filterValue,
|
||||||
|
shouldHideChatTypes,
|
||||||
onSelectedIdsChange,
|
onSelectedIdsChange,
|
||||||
onSelectedChatTypesChange,
|
onSelectedChatTypesChange,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
@ -200,11 +202,15 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
{(!viewportIds || !viewportIds.length || viewportIds.includes(chatIds[0])) && (
|
{(!viewportIds || !viewportIds.length || viewportIds.includes(chatIds[0])) && (
|
||||||
<div key="header">
|
<div key="header">
|
||||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
{!shouldHideChatTypes && (
|
||||||
{lang('FilterChatTypes')}
|
<>
|
||||||
</h4>
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{chatTypes.map(renderChatType)}
|
{lang('FilterChatTypes')}
|
||||||
<div className="picker-list-divider" />
|
</h4>
|
||||||
|
{chatTypes.map(renderChatType)}
|
||||||
|
<div className="picker-list-divider" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{lang('FilterChats')}
|
{lang('FilterChats')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@ -2,12 +2,13 @@ import type { FC } from '../../../../lib/teact/teact';
|
|||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useState,
|
memo, useCallback, useEffect, useMemo, useState,
|
||||||
} from '../../../../lib/teact/teact';
|
} 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 { STICKER_SIZE_FOLDER_SETTINGS } from '../../../../config';
|
||||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../../../util/memo';
|
||||||
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
import { findIntersectionWithSet } from '../../../../util/iteratees';
|
||||||
import { isUserId } from '../../../../global/helpers';
|
import { isUserId } from '../../../../global/helpers';
|
||||||
import type {
|
import type {
|
||||||
@ -19,6 +20,8 @@ import {
|
|||||||
INCLUDED_CHAT_TYPES,
|
INCLUDED_CHAT_TYPES,
|
||||||
selectChatFilters,
|
selectChatFilters,
|
||||||
} from '../../../../hooks/reducers/useFoldersReducer';
|
} from '../../../../hooks/reducers/useFoldersReducer';
|
||||||
|
import { selectCanShareFolder } from '../../../../global/selectors';
|
||||||
|
import { selectCurrentLimit } from '../../../../global/selectors/limits';
|
||||||
import useLang from '../../../../hooks/useLang';
|
import useLang from '../../../../hooks/useLang';
|
||||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||||
|
|
||||||
@ -34,9 +37,12 @@ import AnimatedIcon from '../../../common/AnimatedIcon';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
state: FoldersState;
|
state: FoldersState;
|
||||||
dispatch: FolderEditDispatch;
|
dispatch: FolderEditDispatch;
|
||||||
onAddIncludedChats: () => void;
|
onAddIncludedChats: VoidFunction;
|
||||||
onAddExcludedChats: () => void;
|
onAddExcludedChats: VoidFunction;
|
||||||
|
onShareFolder: VoidFunction;
|
||||||
|
onOpenInvite: (url: string) => void;
|
||||||
isActive?: boolean;
|
isActive?: boolean;
|
||||||
|
isOnlyInvites?: boolean;
|
||||||
onReset: () => void;
|
onReset: () => void;
|
||||||
onBack: () => void;
|
onBack: () => void;
|
||||||
};
|
};
|
||||||
@ -44,7 +50,9 @@ type OwnProps = {
|
|||||||
type StateProps = {
|
type StateProps = {
|
||||||
loadedActiveChatIds?: string[];
|
loadedActiveChatIds?: string[];
|
||||||
loadedArchivedChatIds?: string[];
|
loadedArchivedChatIds?: string[];
|
||||||
|
invites?: ApiChatlistExportedInvite[];
|
||||||
isRemoved?: boolean;
|
isRemoved?: boolean;
|
||||||
|
maxInviteLinks: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SUBMIT_TIMEOUT = 500;
|
const SUBMIT_TIMEOUT = 500;
|
||||||
@ -59,18 +67,29 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
dispatch,
|
dispatch,
|
||||||
onAddIncludedChats,
|
onAddIncludedChats,
|
||||||
onAddExcludedChats,
|
onAddExcludedChats,
|
||||||
|
onShareFolder,
|
||||||
|
onOpenInvite,
|
||||||
isActive,
|
isActive,
|
||||||
onReset,
|
onReset,
|
||||||
isRemoved,
|
isRemoved,
|
||||||
onBack,
|
onBack,
|
||||||
loadedActiveChatIds,
|
loadedActiveChatIds,
|
||||||
|
isOnlyInvites,
|
||||||
loadedArchivedChatIds,
|
loadedArchivedChatIds,
|
||||||
|
invites,
|
||||||
|
maxInviteLinks,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
editChatFolder,
|
editChatFolder,
|
||||||
addChatFolder,
|
addChatFolder,
|
||||||
|
loadChatlistInvites,
|
||||||
|
openLimitReachedModal,
|
||||||
|
showNotification,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
|
const isCreating = state.mode === 'create';
|
||||||
|
const isEditingChatList = state.folder.isChatList;
|
||||||
|
|
||||||
const [isIncludedChatsListExpanded, setIsIncludedChatsListExpanded] = useState(false);
|
const [isIncludedChatsListExpanded, setIsIncludedChatsListExpanded] = useState(false);
|
||||||
const [isExcludedChatsListExpanded, setIsExcludedChatsListExpanded] = useState(false);
|
const [isExcludedChatsListExpanded, setIsExcludedChatsListExpanded] = useState(false);
|
||||||
|
|
||||||
@ -80,6 +99,12 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isRemoved, onReset]);
|
}, [isRemoved, onReset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isActive && state.folderId && state.folder.isChatList) {
|
||||||
|
loadChatlistInvites({ folderId: state.folderId });
|
||||||
|
}
|
||||||
|
}, [isActive, state.folder.isChatList, state.folderId]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedChatIds: includedChatIds,
|
selectedChatIds: includedChatIds,
|
||||||
selectedChatTypes: includedChatTypes,
|
selectedChatTypes: includedChatTypes,
|
||||||
@ -129,7 +154,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });
|
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
const handleSaveFolder = useCallback((cb?: NoneToVoidFunction) => {
|
||||||
const { title } = state.folder;
|
const { title } = state.folder;
|
||||||
|
|
||||||
if (!title) {
|
if (!title) {
|
||||||
@ -142,17 +167,52 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'setIsLoading', payload: true });
|
if (!isCreating) {
|
||||||
if (state.mode === 'edit') {
|
|
||||||
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
|
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
|
||||||
} else {
|
} else {
|
||||||
addChatFolder({ folder: state.folder as ApiChatFolder });
|
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(() => {
|
setTimeout(() => {
|
||||||
onReset();
|
onReset();
|
||||||
}, SUBMIT_TIMEOUT);
|
}, 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') {
|
function renderChatType(key: string, mode: 'included' | 'excluded') {
|
||||||
const chatType = mode === 'included'
|
const chatType = mode === 'included'
|
||||||
@ -226,7 +286,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
className="settings-content-icon"
|
className="settings-content-icon"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{state.mode === 'create' && (
|
{isCreating && (
|
||||||
<p className="settings-item-description mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
|
<p className="settings-item-description mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
{lang('FilterIncludeInfo')}
|
{lang('FilterIncludeInfo')}
|
||||||
</p>
|
</p>
|
||||||
@ -241,39 +301,76 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="settings-item no-border pt-3">
|
{!isOnlyInvites && (
|
||||||
{state.error && state.error === ERROR_NO_CHATS && (
|
<div className="settings-item pt-3">
|
||||||
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
|
{state.error && state.error === ERROR_NO_CHATS && (
|
||||||
{lang(state.error)}
|
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
</p>
|
{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
|
<ListItem
|
||||||
className="settings-folders-list-item color-primary mb-0"
|
className="settings-folders-list-item color-primary mb-0"
|
||||||
icon="add"
|
icon="add"
|
||||||
onClick={onAddIncludedChats}
|
onClick={onAddIncludedChats}
|
||||||
>
|
>
|
||||||
{lang('FilterAddChats')}
|
{lang('FilterAddChats')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{renderChats('included')}
|
{renderChats('included')}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="settings-item pt-3">
|
{!isOnlyInvites && !isEditingChatList && (
|
||||||
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
<div className="settings-item pt-3">
|
||||||
|
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
className="settings-folders-list-item color-primary mb-0"
|
className="settings-folders-list-item color-primary mb-0"
|
||||||
icon="add"
|
icon="add"
|
||||||
onClick={onAddExcludedChats}
|
onClick={onAddExcludedChats}
|
||||||
>
|
>
|
||||||
{lang('FilterAddChats')}
|
{lang('FilterAddChats')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
{renderChats('excluded')}
|
{renderChats('excluded')}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<FloatingActionButton
|
<FloatingActionButton
|
||||||
@ -295,12 +392,14 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { state }): StateProps => {
|
(global, { state }): StateProps => {
|
||||||
const { listIds } = global.chats;
|
const { listIds } = global.chats;
|
||||||
const { byId } = global.chatFolders;
|
const { byId, invites } = global.chatFolders;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loadedActiveChatIds: listIds.active,
|
loadedActiveChatIds: listIds.active,
|
||||||
loadedArchivedChatIds: listIds.archived,
|
loadedArchivedChatIds: listIds.archived,
|
||||||
|
invites: state.folderId ? (invites[state.folderId] || MEMO_EMPTY_ARRAY) : undefined,
|
||||||
isRemoved: state.folderId !== undefined && !byId[state.folderId],
|
isRemoved: state.folderId !== undefined && !byId[state.folderId],
|
||||||
|
maxInviteLinks: selectCurrentLimit(global, 'chatlistInvites'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsFoldersEdit));
|
)(SettingsFoldersEdit));
|
||||||
|
|||||||
@ -139,6 +139,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
id: folder.id,
|
id: folder.id,
|
||||||
title: folder.title,
|
title: folder.title,
|
||||||
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
subtitle: getFolderDescriptionText(lang, folder, chatsCountByFolderId[folder.id]),
|
||||||
|
isChatList: folder.isChatList,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [folderIds, foldersById, lang, chatsCountByFolderId]);
|
}, [folderIds, foldersById, lang, chatsCountByFolderId]);
|
||||||
@ -296,7 +297,10 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
|||||||
{renderText(folder.title, ['emoji'])}
|
{renderText(folder.title, ['emoji'])}
|
||||||
{isBlocked && <i className="icon icon-lock-badge settings-folders-blocked-icon" />}
|
{isBlocked && <i className="icon icon-lock-badge settings-folders-blocked-icon" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="subtitle">{folder.subtitle}</span>
|
<span className="subtitle">
|
||||||
|
{folder.isChatList && <i className="icon icon-link mr-1" />}
|
||||||
|
{folder.subtitle}
|
||||||
|
</span>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Draggable>
|
</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';
|
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||||
|
|
||||||
const DeleteFolderDialogAsync: FC<OwnProps> = (props) => {
|
const DeleteFolderDialogAsync: FC<OwnProps> = (props) => {
|
||||||
const { deleteFolderDialogId } = props;
|
const { folder } = props;
|
||||||
const DeleteFolderDialog = useModuleLoader(Bundles.Extra, 'DeleteFolderDialog', !deleteFolderDialogId);
|
const DeleteFolderDialog = useModuleLoader(Bundles.Extra, 'DeleteFolderDialog', !folder);
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
return DeleteFolderDialog ? <DeleteFolderDialog {...props} /> : undefined;
|
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 React, { memo, useCallback } from '../../lib/teact/teact';
|
||||||
import { getActions } from '../../global';
|
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 useLang from '../../hooks/useLang';
|
||||||
|
|
||||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||||
|
|
||||||
export type OwnProps = {
|
export type OwnProps = {
|
||||||
deleteFolderDialogId?: number;
|
folder?: ApiChatFolder;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DeleteFolderDialog: FC<OwnProps> = ({
|
const DeleteFolderDialog: FC<OwnProps> = ({
|
||||||
deleteFolderDialogId,
|
folder,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeDeleteChatFolderModal, deleteChatFolder } = getActions();
|
const { closeDeleteChatFolderModal, deleteChatFolder, openDeleteChatFolderModal } = getActions();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
|
const isOpen = Boolean(folder);
|
||||||
|
|
||||||
|
const renderingFolder = usePrevious(folder) || folder;
|
||||||
|
const isMyChatlist = renderingFolder?.hasMyInvites;
|
||||||
|
|
||||||
const handleDeleteFolderMessage = useCallback(() => {
|
const handleDeleteFolderMessage = useCallback(() => {
|
||||||
closeDeleteChatFolderModal();
|
closeDeleteChatFolderModal();
|
||||||
deleteChatFolder({ id: deleteFolderDialogId! });
|
if (isMyChatlist) {
|
||||||
}, [closeDeleteChatFolderModal, deleteChatFolder, deleteFolderDialogId]);
|
openDeleteChatFolderModal({ folderId: renderingFolder!.id, isConfirmedForChatlist: true });
|
||||||
|
} else {
|
||||||
|
deleteChatFolder({ id: renderingFolder!.id });
|
||||||
|
}
|
||||||
|
}, [isMyChatlist, renderingFolder]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={deleteFolderDialogId !== undefined}
|
isOpen={isOpen}
|
||||||
onClose={closeDeleteChatFolderModal}
|
onClose={closeDeleteChatFolderModal}
|
||||||
text={lang('FilterDeleteAlert')}
|
text={isMyChatlist ? lang('FilterDeleteAlertLinks') : lang('FilterDeleteAlert')}
|
||||||
confirmLabel={lang('Delete')}
|
confirmLabel={lang('Delete')}
|
||||||
confirmHandler={handleDeleteFolderMessage}
|
confirmHandler={handleDeleteFolderMessage}
|
||||||
confirmIsDestructive
|
confirmIsDestructive
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import { getActions, getGlobal, withGlobal } from '../../global';
|
|||||||
import type { LangCode } from '../../types';
|
import type { LangCode } from '../../types';
|
||||||
import type {
|
import type {
|
||||||
ApiAttachBot,
|
ApiAttachBot,
|
||||||
ApiChat, ApiMessage, ApiUser,
|
ApiChat,
|
||||||
|
ApiChatFolder,
|
||||||
|
ApiMessage,
|
||||||
|
ApiUser,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||||
|
|
||||||
@ -31,6 +34,7 @@ import {
|
|||||||
selectIsReactionPickerOpen,
|
selectIsReactionPickerOpen,
|
||||||
selectPerformanceSettingsValue,
|
selectPerformanceSettingsValue,
|
||||||
selectCanAnimateInterface,
|
selectCanAnimateInterface,
|
||||||
|
selectChatFolder,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||||
@ -86,6 +90,7 @@ import CustomEmojiSetsModal from '../common/CustomEmojiSetsModal.async';
|
|||||||
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
import DraftRecipientPicker from './DraftRecipientPicker.async';
|
||||||
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
import AttachBotRecipientPicker from './AttachBotRecipientPicker.async';
|
||||||
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
import ReactionPicker from '../middle/message/ReactionPicker.async';
|
||||||
|
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
||||||
|
|
||||||
import './Main.scss';
|
import './Main.scss';
|
||||||
|
|
||||||
@ -132,11 +137,12 @@ type StateProps = {
|
|||||||
currentUser?: ApiUser;
|
currentUser?: ApiUser;
|
||||||
urlAuth?: TabState['urlAuth'];
|
urlAuth?: TabState['urlAuth'];
|
||||||
limitReached?: ApiLimitTypeWithModal;
|
limitReached?: ApiLimitTypeWithModal;
|
||||||
deleteFolderDialogId?: number;
|
deleteFolderDialog?: ApiChatFolder;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
isReceiptModalOpen?: boolean;
|
isReceiptModalOpen?: boolean;
|
||||||
isReactionPickerOpen: boolean;
|
isReactionPickerOpen: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
|
chatlistModal?: TabState['chatlistModal'];
|
||||||
noRightColumnAnimation?: boolean;
|
noRightColumnAnimation?: boolean;
|
||||||
withInterfaceAnimations?: boolean;
|
withInterfaceAnimations?: boolean;
|
||||||
};
|
};
|
||||||
@ -191,8 +197,9 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
isReactionPickerOpen,
|
isReactionPickerOpen,
|
||||||
isCurrentUserPremium,
|
isCurrentUserPremium,
|
||||||
deleteFolderDialogId,
|
deleteFolderDialog,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
|
chatlistModal,
|
||||||
noRightColumnAnimation,
|
noRightColumnAnimation,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -509,6 +516,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
userId={newContactUserId}
|
userId={newContactUserId}
|
||||||
isByPhoneNumber={newContactByPhoneNumber}
|
isByPhoneNumber={newContactByPhoneNumber}
|
||||||
/>
|
/>
|
||||||
|
<ChatlistModal info={chatlistModal} />
|
||||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||||
<WebAppModal webApp={webApp} />
|
<WebAppModal webApp={webApp} />
|
||||||
<DownloadManager />
|
<DownloadManager />
|
||||||
@ -528,7 +536,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<PremiumLimitReachedModal limit={limitReached} />
|
<PremiumLimitReachedModal limit={limitReached} />
|
||||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||||
<DeleteFolderDialog deleteFolderDialogId={deleteFolderDialogId} />
|
<DeleteFolderDialog folder={deleteFolderDialog} />
|
||||||
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
<ReactionPicker isOpen={isReactionPickerOpen} shouldLoad={shouldLoadReactionPicker} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -569,6 +577,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
payment,
|
payment,
|
||||||
limitReachedModal,
|
limitReachedModal,
|
||||||
deleteFolderDialogModal,
|
deleteFolderDialogModal,
|
||||||
|
chatlistModal,
|
||||||
} = selectTabState(global);
|
} = selectTabState(global);
|
||||||
|
|
||||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||||
@ -582,6 +591,8 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const noRightColumnAnimation = !selectPerformanceSettingsValue(global, 'rightColumnAnimations')
|
const noRightColumnAnimation = !selectPerformanceSettingsValue(global, 'rightColumnAnimations')
|
||||||
|| !selectCanAnimateInterface(global);
|
|| !selectCanAnimateInterface(global);
|
||||||
|
|
||||||
|
const deleteFolderDialog = deleteFolderDialogModal ? selectChatFolder(global, deleteFolderDialogModal) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
lastSyncTime,
|
lastSyncTime,
|
||||||
isLeftColumnOpen: isLeftColumnShown,
|
isLeftColumnOpen: isLeftColumnShown,
|
||||||
@ -623,9 +634,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
limitReached: limitReachedModal?.limit,
|
limitReached: limitReachedModal?.limit,
|
||||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||||
isReceiptModalOpen: Boolean(payment.receipt),
|
isReceiptModalOpen: Boolean(payment.receipt),
|
||||||
deleteFolderDialogId: deleteFolderDialogModal,
|
deleteFolderDialog,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
requestedDraft,
|
requestedDraft,
|
||||||
|
chatlistModal,
|
||||||
noRightColumnAnimation,
|
noRightColumnAnimation,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
|
|||||||
'emoji_status',
|
'emoji_status',
|
||||||
];
|
];
|
||||||
|
|
||||||
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts'>;
|
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined'>;
|
||||||
|
|
||||||
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
|
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
|
||||||
'channels',
|
'channels',
|
||||||
|
|||||||
@ -26,6 +26,8 @@ const LIMIT_DESCRIPTION: Record<ApiLimitTypeWithModal, string> = {
|
|||||||
dialogFolderPinned: 'LimitReachedPinDialogs',
|
dialogFolderPinned: 'LimitReachedPinDialogs',
|
||||||
channelsPublic: 'LimitReachedPublicLinks',
|
channelsPublic: 'LimitReachedPublicLinks',
|
||||||
channels: 'LimitReachedCommunities',
|
channels: 'LimitReachedCommunities',
|
||||||
|
chatlistInvites: 'LimitReachedFolderLinks',
|
||||||
|
chatlistJoined: 'LimitReachedSharedFolders',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
|
const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
|
||||||
@ -35,6 +37,8 @@ const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
|
|||||||
dialogFolderPinned: 'LimitReachedPinDialogsLocked',
|
dialogFolderPinned: 'LimitReachedPinDialogsLocked',
|
||||||
channelsPublic: 'LimitReachedPublicLinksLocked',
|
channelsPublic: 'LimitReachedPublicLinksLocked',
|
||||||
channels: 'LimitReachedCommunitiesLocked',
|
channels: 'LimitReachedCommunitiesLocked',
|
||||||
|
chatlistInvites: 'LimitReachedFolderLinksLocked',
|
||||||
|
chatlistJoined: 'LimitReachedSharedFoldersLocked',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
|
const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
|
||||||
@ -44,6 +48,8 @@ const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
|
|||||||
dialogFolderPinned: 'LimitReachedPinDialogsPremium',
|
dialogFolderPinned: 'LimitReachedPinDialogsPremium',
|
||||||
channelsPublic: 'LimitReachedPublicLinksPremium',
|
channelsPublic: 'LimitReachedPublicLinksPremium',
|
||||||
channels: 'LimitReachedCommunitiesPremium',
|
channels: 'LimitReachedCommunitiesPremium',
|
||||||
|
chatlistInvites: 'LimitReachedFolderLinksPremium',
|
||||||
|
chatlistJoined: 'LimitReachedSharedFoldersPremium',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIMIT_ICON: Record<ApiLimitTypeWithModal, string> = {
|
const LIMIT_ICON: Record<ApiLimitTypeWithModal, string> = {
|
||||||
@ -53,6 +59,8 @@ const LIMIT_ICON: Record<ApiLimitTypeWithModal, string> = {
|
|||||||
dialogFolderPinned: 'icon-pin-badge',
|
dialogFolderPinned: 'icon-pin-badge',
|
||||||
channelsPublic: 'icon-link-badge',
|
channelsPublic: 'icon-link-badge',
|
||||||
channels: 'icon-chats-badge',
|
channels: 'icon-chats-badge',
|
||||||
|
chatlistInvites: 'icon-link-badge',
|
||||||
|
chatlistJoined: 'icon-folder-badge',
|
||||||
};
|
};
|
||||||
|
|
||||||
const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = {
|
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}
|
isLoading={isSearching}
|
||||||
onSelectedIdsChange={setSelectedMemberIds}
|
onSelectedIdsChange={setSelectedMemberIds}
|
||||||
onFilterChange={handleFilterChange}
|
onFilterChange={handleFilterChange}
|
||||||
|
isSearchable
|
||||||
noScrollRestore={noPickerScrollRestore}
|
noScrollRestore={noPickerScrollRestore}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@ -31,6 +31,8 @@ type StateProps = {
|
|||||||
isChannel?: boolean;
|
isChannel?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BULLET = '\u2022';
|
||||||
|
|
||||||
const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
||||||
chatId,
|
chatId,
|
||||||
invite,
|
invite,
|
||||||
@ -83,19 +85,23 @@ const ManageInviteInfo: FC<OwnProps & StateProps> = ({
|
|||||||
{!importers.length && (
|
{!importers.length && (
|
||||||
usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')
|
usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')
|
||||||
)}
|
)}
|
||||||
{importers.map((importer) => (
|
{importers.map((importer) => {
|
||||||
<ListItem
|
const joinTime = formatMediaDateTime(lang, importer.date * 1000, true);
|
||||||
className="chat-item-clickable scroll-item small-icon"
|
const status = importer.isFromChatList ? `${joinTime} ${BULLET} ${lang('JoinedViaFolder')}` : joinTime;
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
return (
|
||||||
onClick={() => openChat({ id: importer.userId })}
|
<ListItem
|
||||||
>
|
className="chat-item-clickable scroll-item small-icon"
|
||||||
<PrivateChatInfo
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
userId={importer.userId}
|
onClick={() => openChat({ id: importer.userId })}
|
||||||
status={formatMediaDateTime(lang, importer.date * 1000, true)}
|
>
|
||||||
forceShowSelf
|
<PrivateChatInfo
|
||||||
/>
|
userId={importer.userId}
|
||||||
</ListItem>
|
status={status}
|
||||||
))}
|
forceShowSelf
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -19,15 +19,13 @@ import useLang from '../../../hooks/useLang';
|
|||||||
import useInterval from '../../../hooks/useInterval';
|
import useInterval from '../../../hooks/useInterval';
|
||||||
import useForceUpdate from '../../../hooks/useForceUpdate';
|
import useForceUpdate from '../../../hooks/useForceUpdate';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
|
||||||
|
|
||||||
import ListItem from '../../ui/ListItem';
|
import ListItem from '../../ui/ListItem';
|
||||||
import NothingFound from '../../common/NothingFound';
|
import NothingFound from '../../common/NothingFound';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import DropdownMenu from '../../ui/DropdownMenu';
|
|
||||||
import MenuItem from '../../ui/MenuItem';
|
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||||
|
import InviteLink from '../../common/InviteLink';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -79,7 +77,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
|||||||
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
|
const [revokingInvite, setRevokingInvite] = useState<ApiExportedInvite | undefined>();
|
||||||
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
|
||||||
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
|
const [deletingInvite, setDeletingInvite] = useState<ApiExportedInvite | undefined>();
|
||||||
const { isMobile } = useAppLayout();
|
|
||||||
|
|
||||||
useHistoryBack({
|
useHistoryBack({
|
||||||
isActive,
|
isActive,
|
||||||
@ -181,10 +178,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
}, [lang, showNotification]);
|
}, [lang, showNotification]);
|
||||||
|
|
||||||
const handleCopyPrimaryClicked = useCallback(() => {
|
|
||||||
copyLink(primaryInviteLink!);
|
|
||||||
}, [copyLink, primaryInviteLink]);
|
|
||||||
|
|
||||||
const prepareUsageText = (invite: ApiExportedInvite) => {
|
const prepareUsageText = (invite: ApiExportedInvite) => {
|
||||||
const {
|
const {
|
||||||
usage = 0, usageLimit, expireDate, isPermanent, requested, isRevoked,
|
usage = 0, usageLimit, expireDate, isPermanent, requested, isRevoked,
|
||||||
@ -277,22 +270,6 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
|||||||
return actions;
|
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 (
|
return (
|
||||||
<div className="Management ManageInvites">
|
<div className="Management ManageInvites">
|
||||||
<div className="custom-scroll">
|
<div className="custom-scroll">
|
||||||
@ -305,30 +282,11 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
|||||||
<p className="text-muted">{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p>
|
<p className="text-muted">{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p>
|
||||||
</div>
|
</div>
|
||||||
{primaryInviteLink && (
|
{primaryInviteLink && (
|
||||||
<div className="section">
|
<InviteLink
|
||||||
<p className="text-muted">
|
inviteLink={primaryInviteLink}
|
||||||
{chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
|
||||||
</p>
|
title={chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||||
<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>
|
|
||||||
)}
|
)}
|
||||||
<div className="section" teactFastList>
|
<div className="section" teactFastList>
|
||||||
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
|
<Button isText key="create" className="create-link" onClick={handleCreateNewClick}>
|
||||||
|
|||||||
@ -200,24 +200,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ManageInvites {
|
.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 {
|
.create-link {
|
||||||
margin-bottom: 0.5rem;
|
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;
|
padding: 0.625rem 0.25rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--color-text-secondary);
|
color: var(--color-text-secondary);
|
||||||
cursor: var(--custom-cursor, pointer);
|
|
||||||
border-top-left-radius: var(--border-radius-messages-small);
|
border-top-left-radius: var(--border-radius-messages-small);
|
||||||
border-top-right-radius: var(--border-radius-messages-small);
|
border-top-right-radius: var(--border-radius-messages-small);
|
||||||
|
|
||||||
@ -20,6 +19,22 @@
|
|||||||
outline: none;
|
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 {
|
&--active {
|
||||||
cursor: var(--custom-cursor, default);
|
cursor: var(--custom-cursor, default);
|
||||||
color: var(--color-primary);
|
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 {
|
> span {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -102,3 +105,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Tab-context-menu {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, {
|
import React, {
|
||||||
useRef,
|
useRef,
|
||||||
memo,
|
memo,
|
||||||
@ -7,10 +6,19 @@ import React, {
|
|||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom';
|
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 { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||||
import forceReflow from '../../util/forceReflow';
|
import forceReflow from '../../util/forceReflow';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import renderText from '../common/helpers/renderText';
|
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';
|
import './Tab.scss';
|
||||||
|
|
||||||
@ -22,8 +30,10 @@ type OwnProps = {
|
|||||||
badgeCount?: number;
|
badgeCount?: number;
|
||||||
isBadgeActive?: boolean;
|
isBadgeActive?: boolean;
|
||||||
previousActiveTab?: number;
|
previousActiveTab?: number;
|
||||||
onClick: (arg: number) => void;
|
onClick?: (arg: number) => void;
|
||||||
clickArg: number;
|
clickArg?: number;
|
||||||
|
contextActions?: MenuItemContextAction[];
|
||||||
|
contextRootElementSelector?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const classNames = {
|
const classNames = {
|
||||||
@ -41,6 +51,8 @@ const Tab: FC<OwnProps> = ({
|
|||||||
previousActiveTab,
|
previousActiveTab,
|
||||||
onClick,
|
onClick,
|
||||||
clickArg,
|
clickArg,
|
||||||
|
contextActions,
|
||||||
|
contextRootElementSelector,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const tabRef = useRef<HTMLDivElement>(null);
|
const tabRef = useRef<HTMLDivElement>(null);
|
||||||
@ -95,19 +107,59 @@ const Tab: FC<OwnProps> = ({
|
|||||||
});
|
});
|
||||||
}, [isActive, previousActiveTab]);
|
}, [isActive, previousActiveTab]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
contextMenuPosition, handleContextMenu, handleBeforeContextMenu, handleContextMenuClose,
|
||||||
|
handleContextMenuHide, isContextMenuOpen,
|
||||||
|
} = useContextMenuHandlers(tabRef, !contextActions);
|
||||||
|
|
||||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
if (contextActions && (e.button === MouseButton.Secondary || !onClick)) {
|
||||||
|
handleBeforeContextMenu(e);
|
||||||
|
}
|
||||||
|
|
||||||
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(clickArg);
|
onClick?.(clickArg!);
|
||||||
}, [clickArg, onClick]);
|
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName('Tab', className)}
|
className={buildClassName('Tab', onClick && 'Tab--interactive', className)}
|
||||||
onClick={IS_TOUCH_ENV ? handleClick : undefined}
|
onClick={IS_TOUCH_ENV ? handleClick : undefined}
|
||||||
onMouseDown={!IS_TOUCH_ENV ? handleClick : undefined}
|
onMouseDown={!IS_TOUCH_ENV ? handleClick : undefined}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
ref={tabRef}
|
ref={tabRef}
|
||||||
>
|
>
|
||||||
<span className="Tab_inner">
|
<span className="Tab_inner">
|
||||||
@ -118,6 +170,38 @@ const Tab: FC<OwnProps> = ({
|
|||||||
{isBlocked && <i className="icon icon-lock-badge blocked" />}
|
{isBlocked && <i className="icon icon-lock-badge blocked" />}
|
||||||
<i className="platform" />
|
<i className="platform" />
|
||||||
</span>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, { memo, useRef, useEffect } 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 { ALL_FOLDER_ID } from '../../config';
|
||||||
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
|
import { IS_ANDROID, IS_IOS } from '../../util/windowEnvironment';
|
||||||
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
import animateHorizontalScroll from '../../util/animateHorizontalScroll';
|
||||||
@ -19,6 +21,7 @@ export type TabWithProperties = {
|
|||||||
badgeCount?: number;
|
badgeCount?: number;
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
isBadgeActive?: boolean;
|
isBadgeActive?: boolean;
|
||||||
|
contextActions?: MenuItemContextAction[];
|
||||||
};
|
};
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
@ -27,6 +30,7 @@ type OwnProps = {
|
|||||||
activeTab: number;
|
activeTab: number;
|
||||||
big?: boolean;
|
big?: boolean;
|
||||||
onSwitchTab: (index: number) => void;
|
onSwitchTab: (index: number) => void;
|
||||||
|
contextRootElementSelector?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TAB_SCROLL_THRESHOLD_PX = 16;
|
const TAB_SCROLL_THRESHOLD_PX = 16;
|
||||||
@ -35,6 +39,7 @@ const SCROLL_DURATION = IS_IOS ? 450 : IS_ANDROID ? 400 : 300;
|
|||||||
|
|
||||||
const TabList: FC<OwnProps> = ({
|
const TabList: FC<OwnProps> = ({
|
||||||
tabs, areFolders, activeTab, big, onSwitchTab,
|
tabs, areFolders, activeTab, big, onSwitchTab,
|
||||||
|
contextRootElementSelector,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
@ -86,6 +91,8 @@ const TabList: FC<OwnProps> = ({
|
|||||||
previousActiveTab={previousActiveTab}
|
previousActiveTab={previousActiveTab}
|
||||||
onClick={onSwitchTab}
|
onClick={onSwitchTab}
|
||||||
clickArg={i}
|
clickArg={i}
|
||||||
|
contextActions={tab.contextActions}
|
||||||
|
contextRootElementSelector={contextRootElementSelector}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -304,4 +304,6 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
|
|||||||
channels: [500, 1000],
|
channels: [500, 1000],
|
||||||
channelsPublic: [10, 20],
|
channelsPublic: [10, 20],
|
||||||
aboutLength: [70, 140],
|
aboutLength: [70, 140],
|
||||||
|
chatlistInvites: [3, 100],
|
||||||
|
chatlistJoined: [2, 20],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -4,10 +4,15 @@ import {
|
|||||||
} from '../../index';
|
} from '../../index';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiUser, ApiError, ApiChatMember,
|
ApiChat, ApiUser, ApiError, ApiChatMember, ApiChatFolder, ApiChatlistExportedInvite,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import { MAIN_THREAD_ID } 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 {
|
import type {
|
||||||
GlobalState, ActionReturnType, TabArgs,
|
GlobalState, ActionReturnType, TabArgs,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
@ -89,6 +94,11 @@ const SERVICE_NOTIFICATIONS_USER_MOCK: ApiUser = {
|
|||||||
isMin: true,
|
isMin: true,
|
||||||
phoneNumber: '',
|
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 runThrottledForLoadTopChats = throttle((cb) => cb(), 3000, true);
|
||||||
const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true);
|
const runDebouncedForLoadFullChat = debounce((cb) => cb(), 500, false, true);
|
||||||
@ -896,6 +906,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
|||||||
focusMessage,
|
focusMessage,
|
||||||
openInvoice,
|
openInvoice,
|
||||||
processAttachBotParameters,
|
processAttachBotParameters,
|
||||||
|
checkChatlistInvite,
|
||||||
openChatByUsername: openChatByUsernameAction,
|
openChatByUsername: openChatByUsernameAction,
|
||||||
} = actions;
|
} = actions;
|
||||||
|
|
||||||
@ -954,14 +965,23 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
|||||||
return;
|
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 chatOrChannelPostId = part2 || undefined;
|
||||||
const messageId = part3 ? Number(part3) : undefined;
|
const messageId = part3 ? Number(part3) : undefined;
|
||||||
const commentId = params.comment ? Number(params.comment) : undefined;
|
const commentId = params.comment ? Number(params.comment) : undefined;
|
||||||
|
|
||||||
if (part1 === 'share') {
|
if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) {
|
||||||
const text = formatShareText(params.url, params.text);
|
|
||||||
openChatWithDraft({ text, tabId });
|
|
||||||
} else if (params.hasOwnProperty('voicechat') || params.hasOwnProperty('livestream')) {
|
|
||||||
joinVoiceChatByLink({
|
joinVoiceChatByLink({
|
||||||
username: part1,
|
username: part1,
|
||||||
inviteHash: params.voicechat || params.livestream,
|
inviteHash: params.voicechat || params.livestream,
|
||||||
@ -1814,6 +1834,269 @@ addActionHandler('toggleTopicPinned', (global, actions, payload): ActionReturnTy
|
|||||||
void callApi('togglePinnedTopic', { chat, topicId, isPinned });
|
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>(
|
async function loadChats<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
listType: 'active' | 'archived',
|
listType: 'active' | 'archived',
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
|||||||
import {
|
import {
|
||||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList,
|
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import { selectChat, selectCurrentMessageList, selectTabState } from '../../selectors';
|
import {
|
||||||
|
selectChat, selectCurrentMessageList, selectTabState,
|
||||||
|
} from '../../selectors';
|
||||||
import { closeLocalTextSearch } from './localSearch';
|
import { closeLocalTextSearch } from './localSearch';
|
||||||
import type { ActionReturnType } from '../../types';
|
import type { ActionReturnType } from '../../types';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
@ -153,16 +155,16 @@ addActionHandler('openNextChat', (global, actions, payload): ActionReturnType =>
|
|||||||
actions.openChat({ id: nextId, shouldReplaceHistory: true, tabId });
|
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 => {
|
addActionHandler('closeDeleteChatFolderModal', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
deleteFolderDialogModal: undefined,
|
deleteFolderDialogModal: undefined,
|
||||||
}, tabId);
|
}, 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 { addActionHandler, getActions } from '../../index';
|
||||||
|
|
||||||
|
import { SettingsScreens } from '../../../types';
|
||||||
|
import type { ActionReturnType, GlobalState } from '../../types';
|
||||||
|
|
||||||
import { replaceSettings, replaceThemeSettings } from '../../reducers';
|
import { replaceSettings, replaceThemeSettings } from '../../reducers';
|
||||||
import switchTheme from '../../../util/switchTheme';
|
import switchTheme from '../../../util/switchTheme';
|
||||||
import { setLanguage, setTimeFormat } from '../../../util/langProvider';
|
import { setLanguage, setTimeFormat } from '../../../util/langProvider';
|
||||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||||
import type { ActionReturnType, GlobalState } from '../../types';
|
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
import { addCallback } from '../../../lib/teact/teactn';
|
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
|
||||||
import { applyPerformanceSettings } from '../../../util/perfomanceSettings';
|
import { applyPerformanceSettings } from '../../../util/perfomanceSettings';
|
||||||
import { selectCanAnimateInterface } from '../../selectors';
|
import { selectCanAnimateInterface, selectChatFolder } from '../../selectors';
|
||||||
|
|
||||||
let prevGlobal: GlobalState | undefined;
|
let prevGlobal: GlobalState | undefined;
|
||||||
|
|
||||||
@ -87,8 +90,58 @@ addActionHandler('setThemeSettings', (global, actions, payload): ActionReturnTyp
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('requestNextSettingsScreen', (global, actions, payload): ActionReturnType => {
|
addActionHandler('requestNextSettingsScreen', (global, actions, payload): ActionReturnType => {
|
||||||
const { screen, tabId = getCurrentTabId() } = payload;
|
const { screen, foldersAction, tabId = getCurrentTabId() } = payload;
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
nextSettingsScreen: screen,
|
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);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -323,12 +323,12 @@ export function getCanDeleteChat(chat: ApiChat) {
|
|||||||
|
|
||||||
export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) {
|
export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, chatsCount?: number) {
|
||||||
const {
|
const {
|
||||||
id, title, emoticon, description, pinnedChatIds,
|
|
||||||
excludedChatIds, includedChatIds,
|
excludedChatIds, includedChatIds,
|
||||||
excludeArchived, excludeMuted, excludeRead,
|
bots, groups, contacts, nonContacts, channels,
|
||||||
...filters
|
|
||||||
} = folder;
|
} = folder;
|
||||||
|
|
||||||
|
const filters = [bots, groups, contacts, nonContacts, channels];
|
||||||
|
|
||||||
// If folder has multiple additive filters or uses include/exclude lists,
|
// If folder has multiple additive filters or uses include/exclude lists,
|
||||||
// we display folder chats count
|
// we display folder chats count
|
||||||
if (
|
if (
|
||||||
@ -341,15 +341,15 @@ export function getFolderDescriptionText(lang: LangFn, folder: ApiChatFolder, ch
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, we return a short description of a single filter
|
// Otherwise, we return a short description of a single filter
|
||||||
if (filters.bots) {
|
if (bots) {
|
||||||
return lang('FilterBots');
|
return lang('FilterBots');
|
||||||
} else if (filters.groups) {
|
} else if (groups) {
|
||||||
return lang('FilterGroups');
|
return lang('FilterGroups');
|
||||||
} else if (filters.channels) {
|
} else if (channels) {
|
||||||
return lang('FilterChannels');
|
return lang('FilterChannels');
|
||||||
} else if (filters.contacts) {
|
} else if (contacts) {
|
||||||
return lang('FilterContacts');
|
return lang('FilterContacts');
|
||||||
} else if (filters.nonContacts) {
|
} else if (nonContacts) {
|
||||||
return lang('FilterNonContacts');
|
return lang('FilterNonContacts');
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -123,6 +123,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
|
|
||||||
chatFolders: {
|
chatFolders: {
|
||||||
byId: {},
|
byId: {},
|
||||||
|
invites: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
fileUploads: {
|
fileUploads: {
|
||||||
|
|||||||
@ -3,7 +3,14 @@ import { MAIN_THREAD_ID } from '../../api/types';
|
|||||||
import type { GlobalState, TabArgs } from '../types';
|
import type { GlobalState, TabArgs } from '../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getPrivateChatUserId, isChatChannel, isUserId, isHistoryClearMessage, isUserBot, isUserOnline,
|
getPrivateChatUserId,
|
||||||
|
isChatChannel,
|
||||||
|
isUserId,
|
||||||
|
isHistoryClearMessage,
|
||||||
|
isUserBot,
|
||||||
|
isUserOnline,
|
||||||
|
getHasAdminRight,
|
||||||
|
isChatSuperGroup,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { selectBot, selectUser } from './users';
|
import { selectBot, selectUser } from './users';
|
||||||
import {
|
import {
|
||||||
@ -239,3 +246,31 @@ export function filterChatIdsByType<T extends GlobalState>(
|
|||||||
return filter.includes(type);
|
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 isPremium = selectIsCurrentUserPremium(global);
|
||||||
const { limits } = appConfig;
|
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
|
if (limit === 'dialogFilters') return value + 1; // Server does not count "All" as folder, but we need to
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,8 @@ import type {
|
|||||||
ApiWebSession,
|
ApiWebSession,
|
||||||
ApiUserFullInfo,
|
ApiUserFullInfo,
|
||||||
ApiChatFullInfo,
|
ApiChatFullInfo,
|
||||||
|
ApiChatlistInvite,
|
||||||
|
ApiChatlistExportedInvite,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
import type {
|
import type {
|
||||||
ApiInvoiceContainer,
|
ApiInvoiceContainer,
|
||||||
@ -93,6 +95,8 @@ import type {
|
|||||||
} from '../types';
|
} from '../types';
|
||||||
import type { P2pMessage } from '../lib/secret-sauce';
|
import type { P2pMessage } from '../lib/secret-sauce';
|
||||||
import type { ApiCredentials } from '../components/payment/PaymentModal';
|
import type { ApiCredentials } from '../components/payment/PaymentModal';
|
||||||
|
import type { FoldersActions } from '../hooks/reducers/useFoldersReducer';
|
||||||
|
import type { ReducerAction } from '../hooks/useReducer';
|
||||||
|
|
||||||
export type MessageListType =
|
export type MessageListType =
|
||||||
'thread'
|
'thread'
|
||||||
@ -157,7 +161,7 @@ export interface ServiceNotification {
|
|||||||
|
|
||||||
export type ApiLimitType = (
|
export type ApiLimitType = (
|
||||||
'uploadMaxFileparts' | 'stickersFaved' | 'savedGifs' | 'dialogFiltersChats' | 'dialogFilters' | 'dialogFolderPinned' |
|
'uploadMaxFileparts' | 'stickersFaved' | 'savedGifs' | 'dialogFiltersChats' | 'dialogFilters' | 'dialogFolderPinned' |
|
||||||
'captionLength' | 'channels' | 'channelsPublic' | 'aboutLength'
|
'captionLength' | 'channels' | 'channelsPublic' | 'aboutLength' | 'chatlistInvites' | 'chatlistJoined'
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
export type ApiLimitTypeWithModal = Exclude<ApiLimitType, (
|
||||||
@ -206,6 +210,13 @@ export type TabState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
nextSettingsScreen?: SettingsScreens;
|
nextSettingsScreen?: SettingsScreens;
|
||||||
|
nextFoldersAction?: ReducerAction<FoldersActions>;
|
||||||
|
shareFolderScreen?: {
|
||||||
|
folderId: number;
|
||||||
|
isFromSettings?: boolean;
|
||||||
|
url?: string;
|
||||||
|
isLoading?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
isCallPanelVisible?: boolean;
|
isCallPanelVisible?: boolean;
|
||||||
multitabNextAction?: CallbackAction;
|
multitabNextAction?: CallbackAction;
|
||||||
@ -553,6 +564,14 @@ export type TabState = {
|
|||||||
messageId: number;
|
messageId: number;
|
||||||
activeLanguage?: string;
|
activeLanguage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
chatlistModal?: {
|
||||||
|
invite?: ApiChatlistInvite;
|
||||||
|
removal?: {
|
||||||
|
folderId: number;
|
||||||
|
suggestedPeerIds?: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalState = {
|
export type GlobalState = {
|
||||||
@ -690,6 +709,7 @@ export type GlobalState = {
|
|||||||
chatFolders: {
|
chatFolders: {
|
||||||
orderedIds?: number[];
|
orderedIds?: number[];
|
||||||
byId: Record<number, ApiChatFolder>;
|
byId: Record<number, ApiChatFolder>;
|
||||||
|
invites: Record<number, ApiChatlistExportedInvite[]>;
|
||||||
recommended?: ApiChatFolder[];
|
recommended?: ApiChatFolder[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1673,6 +1693,34 @@ export interface ActionPayloads {
|
|||||||
isEnabled: boolean;
|
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
|
// Messages
|
||||||
setEditingDraft: {
|
setEditingDraft: {
|
||||||
text?: ApiFormattedText;
|
text?: ApiFormattedText;
|
||||||
@ -2300,10 +2348,24 @@ export interface ActionPayloads {
|
|||||||
} | undefined;
|
} | undefined;
|
||||||
requestNextSettingsScreen: {
|
requestNextSettingsScreen: {
|
||||||
screen?: SettingsScreens;
|
screen?: SettingsScreens;
|
||||||
|
foldersAction?: ReducerAction<FoldersActions>;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
sortChatFolders: { folderIds: number[] };
|
sortChatFolders: { folderIds: number[] };
|
||||||
closeDeleteChatFolderModal: WithTabId | undefined;
|
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;
|
loadGlobalPrivacySettings: undefined;
|
||||||
updateGlobalPrivacySettings: { shouldArchiveAndMuteNewNonContact: boolean };
|
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.loadAsyncGraph#621d5fa0 flags:# token:string x:flags.0?long = StatsGraph;
|
||||||
stats.getMegagroupStats#dcdf8607 flags:# dark:flags.0?true channel:InputChannel = stats.MegagroupStats;
|
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.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.deleteTopicHistory",
|
||||||
"channels.toggleParticipantsHidden",
|
"channels.toggleParticipantsHidden",
|
||||||
"photos.uploadContactProfilePhoto",
|
"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,
|
FoldersCreateFolder,
|
||||||
FoldersEditFolder,
|
FoldersEditFolder,
|
||||||
FoldersEditFolderFromChatList,
|
FoldersEditFolderFromChatList,
|
||||||
|
FoldersEditFolderInvites,
|
||||||
FoldersIncludedChats,
|
FoldersIncludedChats,
|
||||||
FoldersIncludedChatsFromChatList,
|
FoldersIncludedChatsFromChatList,
|
||||||
FoldersExcludedChats,
|
FoldersExcludedChats,
|
||||||
@ -228,6 +229,7 @@ export enum SettingsScreens {
|
|||||||
QuickReaction,
|
QuickReaction,
|
||||||
CustomEmoji,
|
CustomEmoji,
|
||||||
DoNotTranslate,
|
DoNotTranslate,
|
||||||
|
FoldersShare,
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import { IS_SAFARI } from './windowEnvironment';
|
|||||||
|
|
||||||
type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
||||||
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
||||||
'invoice';
|
'invoice' | 'addlist';
|
||||||
|
|
||||||
export const processDeepLink = (url: string) => {
|
export const processDeepLink = (url: string) => {
|
||||||
const {
|
const {
|
||||||
@ -26,6 +26,7 @@ export const processDeepLink = (url: string) => {
|
|||||||
openInvoice,
|
openInvoice,
|
||||||
processAttachBotParameters,
|
processAttachBotParameters,
|
||||||
openChatWithDraft,
|
openChatWithDraft,
|
||||||
|
checkChatlistInvite,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
// Safari thinks the path in tg://path links is hostname for some reason
|
// 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) });
|
openChatWithDraft({ text: formatShareText(urlParam, text) });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'addlist': {
|
||||||
|
checkChatlistInvite({ slug: params.slug });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'login': {
|
case 'login': {
|
||||||
// const { code, token } = params;
|
// const { code, token } = params;
|
||||||
break;
|
break;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user