Folders: Fix sharing (#3142)

This commit is contained in:
Alexander Zinchuk 2023-05-05 15:53:15 +04:00
parent d04177aea8
commit 4b96cc5bfe
15 changed files with 256 additions and 132 deletions

View File

@ -1681,7 +1681,7 @@ export async function editChatlistInvite({
slug, slug,
title, title,
peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
})); }), undefined, true);
if (!result) return undefined; if (!result) return undefined;
return buildApiChatlistExportedInvite(result); return buildApiChatlistExportedInvite(result);

View File

@ -6,6 +6,7 @@
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
margin-bottom: 1rem; margin-bottom: 1rem;
padding-right: 3rem; padding-right: 3rem;
text-overflow: ellipsis;
} }
.moreMenu { .moreMenu {

View File

@ -18,12 +18,14 @@ type OwnProps = {
title?: string; title?: string;
inviteLink: string; inviteLink: string;
onRevoke?: VoidFunction; onRevoke?: VoidFunction;
isDisabled?: boolean;
}; };
const InviteLink: FC<OwnProps> = ({ const InviteLink: FC<OwnProps> = ({
title, title,
inviteLink, inviteLink,
onRevoke, onRevoke,
isDisabled,
}) => { }) => {
const lang = useLang(); const lang = useLang();
const { showNotification, openChatWithDraft } = getActions(); const { showNotification, openChatWithDraft } = getActions();
@ -85,10 +87,20 @@ const InviteLink: FC<OwnProps> = ({
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className={styles.buttons}> <div className={styles.buttons}>
<Button onClick={handleCopyPrimaryClicked} className={styles.button}> <Button
onClick={handleCopyPrimaryClicked}
className={styles.button}
size="smaller"
disabled={isDisabled}
>
{lang('FolderLinkScreen.LinkActionCopy')} {lang('FolderLinkScreen.LinkActionCopy')}
</Button> </Button>
<Button onClick={handleShare} className={styles.button}> <Button
onClick={handleShare}
className={styles.button}
size="smaller"
disabled={isDisabled}
>
{lang('FolderLinkScreen.LinkActionShare')} {lang('FolderLinkScreen.LinkActionShare')}
</Button> </Button>
</div> </div>

View File

@ -6,6 +6,7 @@ import { requestMutation } from '../../lib/fasterdom/fasterdom';
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import { isUserId } from '../../global/helpers'; import { isUserId } from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import useInfiniteScroll from '../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../hooks/useInfiniteScroll';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -32,6 +33,7 @@ type OwnProps = {
isLoading?: boolean; isLoading?: boolean;
noScrollRestore?: boolean; noScrollRestore?: boolean;
isSearchable?: boolean; isSearchable?: boolean;
isRoundCheckbox?: boolean;
lockedIds?: string[]; lockedIds?: string[];
onSelectedIdsChange?: (ids: string[]) => void; onSelectedIdsChange?: (ids: string[]) => void;
onFilterChange?: (value: string) => void; onFilterChange?: (value: string) => void;
@ -55,6 +57,7 @@ const Picker: FC<OwnProps> = ({
isLoading, isLoading,
noScrollRestore, noScrollRestore,
isSearchable, isSearchable,
isRoundCheckbox,
lockedIds, lockedIds,
onSelectedIdsChange, onSelectedIdsChange,
onFilterChange, onFilterChange,
@ -161,24 +164,37 @@ const Picker: FC<OwnProps> = ({
onLoadMore={getMore} onLoadMore={getMore}
noScrollRestore={noScrollRestore} noScrollRestore={noScrollRestore}
> >
{viewportIds.map((id) => ( {viewportIds.map((id) => {
<ListItem const renderCheckbox = () => {
key={id} return (
className="chat-item-clickable picker-list-item" <Checkbox
disabled={lockedIdsSet.has(id)} label=""
allowDisabledClick={Boolean(onDisabledClick)} disabled={lockedIdsSet.has(id)}
// eslint-disable-next-line react/jsx-no-bind checked={selectedIds.includes(id)}
onClick={() => handleItemClick(id)} round={isRoundCheckbox}
ripple />
> );
<Checkbox label="" disabled={lockedIdsSet.has(id)} checked={selectedIds.includes(id)} /> };
{isUserId(id) ? ( return (
<PrivateChatInfo userId={id} /> <ListItem
) : ( key={id}
<GroupChatInfo chatId={id} /> className={buildClassName('chat-item-clickable picker-list-item', isRoundCheckbox && 'chat-item')}
)} disabled={lockedIdsSet.has(id)}
</ListItem> allowDisabledClick={Boolean(onDisabledClick)}
))} // eslint-disable-next-line react/jsx-no-bind
onClick={() => handleItemClick(id)}
ripple
>
{!isRoundCheckbox ? renderCheckbox() : undefined}
{isUserId(id) ? (
<PrivateChatInfo userId={id} />
) : (
<GroupChatInfo chatId={id} />
)}
{isRoundCheckbox ? renderCheckbox() : undefined}
</ListItem>
);
})}
</InfiniteScroll> </InfiniteScroll>
) : !isLoading && viewportIds && !viewportIds.length ? ( ) : !isLoading && viewportIds && !viewportIds.length ? (
<p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p> <p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p>

View File

@ -6,8 +6,10 @@ 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 { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer';
import SettingsFoldersMain from './SettingsFoldersMain'; import SettingsFoldersMain from './SettingsFoldersMain';
import SettingsFoldersEdit from './SettingsFoldersEdit'; import SettingsFoldersEdit, { ERROR_NO_CHATS, ERROR_NO_TITLE } from './SettingsFoldersEdit';
import SettingsFoldersChatFilters from './SettingsFoldersChatFilters'; import SettingsFoldersChatFilters from './SettingsFoldersChatFilters';
import SettingsShareChatlist from './SettingsShareChatlist'; import SettingsShareChatlist from './SettingsShareChatlist';
@ -34,7 +36,11 @@ const SettingsFolders: FC<OwnProps> = ({
onScreenSelect, onScreenSelect,
onReset, onReset,
}) => { }) => {
const { openShareChatFolderModal } = getActions(); const {
openShareChatFolderModal,
editChatFolder,
addChatFolder,
} = getActions();
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
if ( if (
@ -66,10 +72,50 @@ const SettingsFolders: FC<OwnProps> = ({
currentScreen, onReset, onScreenSelect, currentScreen, onReset, onScreenSelect,
]); ]);
const isCreating = state.mode === 'create';
const saveState = useCallback((newState: FoldersState) => {
const { title } = newState.folder;
if (!title) {
dispatch({ type: 'setError', payload: ERROR_NO_TITLE });
return false;
}
const {
selectedChatIds: includedChatIds,
selectedChatTypes: includedChatTypes,
} = selectChatFilters(newState, 'included');
if (!includedChatIds.length && !Object.keys(includedChatTypes).length) {
dispatch({ type: 'setError', payload: ERROR_NO_CHATS });
return false;
}
if (!isCreating) {
editChatFolder({ id: newState.folderId!, folderUpdate: newState.folder });
} else {
addChatFolder({ folder: newState.folder as ApiChatFolder });
}
dispatch({ type: 'setError', payload: undefined });
dispatch({ type: 'setIsTouched', payload: false });
return true;
}, [dispatch, isCreating]);
const handleSaveFolder = useCallback((cb?: NoneToVoidFunction) => {
if (!saveState(state)) {
return;
}
cb?.();
}, [saveState, state]);
const handleSaveFilter = useCallback(() => { const handleSaveFilter = useCallback(() => {
dispatch({ type: 'saveFilters' }); const newState = dispatch({ type: 'saveFilters' });
handleReset(); handleReset();
}, [dispatch, handleReset]); saveState(newState);
}, [dispatch, handleReset, saveState]);
const handleCreateFolder = useCallback(() => { const handleCreateFolder = useCallback(() => {
dispatch({ type: 'reset' }); dispatch({ type: 'reset' });
@ -97,8 +143,9 @@ const SettingsFolders: FC<OwnProps> = ({
const handleShareFolder = useCallback(() => { const handleShareFolder = useCallback(() => {
openShareChatFolderModal({ folderId: state.folderId!, noRequestNextScreen: true }); openShareChatFolderModal({ folderId: state.folderId!, noRequestNextScreen: true });
dispatch({ type: 'setIsChatlist', payload: true });
onScreenSelect(SettingsScreens.FoldersShare); onScreenSelect(SettingsScreens.FoldersShare);
}, [onScreenSelect, state.folderId]); }, [dispatch, onScreenSelect, state.folderId]);
const handleOpenInvite = useCallback((url: string) => { const handleOpenInvite = useCallback((url: string) => {
openShareChatFolderModal({ folderId: state.folderId!, url, noRequestNextScreen: true }); openShareChatFolderModal({ folderId: state.folderId!, url, noRequestNextScreen: true });
@ -139,6 +186,7 @@ const SettingsFolders: FC<OwnProps> = ({
].includes(shownScreen)} ].includes(shownScreen)}
isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites} isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites}
onBack={onReset} onBack={onReset}
onSaveFolder={handleSaveFolder}
/> />
); );
case SettingsScreens.FoldersIncludedChats: case SettingsScreens.FoldersIncludedChats:

View File

@ -4,7 +4,7 @@ import React, {
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
import type { ApiChatFolder, ApiChatlistExportedInvite } from '../../../../api/types'; import type { 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';
@ -45,6 +45,7 @@ type OwnProps = {
isOnlyInvites?: boolean; isOnlyInvites?: boolean;
onReset: () => void; onReset: () => void;
onBack: () => void; onBack: () => void;
onSaveFolder: (cb?: VoidFunction) => void;
}; };
type StateProps = { type StateProps = {
@ -59,8 +60,8 @@ const SUBMIT_TIMEOUT = 500;
const INITIAL_CHATS_LIMIT = 5; const INITIAL_CHATS_LIMIT = 5;
const ERROR_NO_TITLE = 'Please provide a title for this folder.'; export const ERROR_NO_TITLE = 'Please provide a title for this folder.';
const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty'; export const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty';
const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
state, state,
@ -78,10 +79,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
loadedArchivedChatIds, loadedArchivedChatIds,
invites, invites,
maxInviteLinks, maxInviteLinks,
onSaveFolder,
}) => { }) => {
const { const {
editChatFolder,
addChatFolder,
loadChatlistInvites, loadChatlistInvites,
openLimitReachedModal, openLimitReachedModal,
showNotification, showNotification,
@ -154,38 +154,23 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
dispatch({ type: 'setTitle', payload: currentTarget.value.trim() }); dispatch({ type: 'setTitle', payload: currentTarget.value.trim() });
}, [dispatch]); }, [dispatch]);
const handleSaveFolder = useCallback((cb?: NoneToVoidFunction) => {
const { title } = state.folder;
if (!title) {
dispatch({ type: 'setError', payload: ERROR_NO_TITLE });
return;
}
if (!includedChatIds.length && !Object.keys(includedChatTypes).length) {
dispatch({ type: 'setError', payload: ERROR_NO_CHATS });
return;
}
if (!isCreating) {
editChatFolder({ id: state.folderId!, folderUpdate: state.folder });
} else {
addChatFolder({ folder: state.folder as ApiChatFolder });
}
cb?.();
}, [dispatch, includedChatIds.length, includedChatTypes, isCreating, state.folder, state.folderId]);
const handleSubmit = useCallback(() => { const handleSubmit = useCallback(() => {
handleSaveFolder();
dispatch({ type: 'setIsLoading', payload: true }); dispatch({ type: 'setIsLoading', payload: true });
setTimeout(() => {
onReset(); onSaveFolder(() => {
}, SUBMIT_TIMEOUT); setTimeout(() => {
}, [dispatch, handleSaveFolder, onReset]); onReset();
}, SUBMIT_TIMEOUT);
});
}, [dispatch, onSaveFolder, onReset]);
const handleCreateInviteClick = useCallback(() => { const handleCreateInviteClick = useCallback(() => {
if (!invites) return; if (!invites) {
if (isCreating) {
onSaveFolder(onShareFolder);
}
return;
}
// Ignoring global updates is a known drawback here // Ignoring global updates is a known drawback here
if (!selectCanShareFolder(getGlobal(), state.folderId!)) { if (!selectCanShareFolder(getGlobal(), state.folderId!)) {
@ -195,7 +180,7 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
if (invites.length < maxInviteLinks) { if (invites.length < maxInviteLinks) {
if (state.isTouched) { if (state.isTouched) {
handleSaveFolder(onShareFolder); onSaveFolder(onShareFolder);
} else { } else {
onShareFolder(); onShareFolder();
} }
@ -204,15 +189,17 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
limit: 'chatlistInvites', limit: 'chatlistInvites',
}); });
} }
}, [handleSaveFolder, invites, lang, maxInviteLinks, onShareFolder, state.folderId, state.isTouched]); }, [
invites, state.folderId, state.isTouched, maxInviteLinks, isCreating, onSaveFolder, onShareFolder, lang,
]);
const handleEditInviteClick = useCallback((e: React.MouseEvent<HTMLElement>, url: string) => { const handleEditInviteClick = useCallback((e: React.MouseEvent<HTMLElement>, url: string) => {
if (state.isTouched) { if (state.isTouched) {
handleSaveFolder(() => onOpenInvite(url)); onSaveFolder(() => onOpenInvite(url));
} else { } else {
onOpenInvite(url); onOpenInvite(url);
} }
}, [handleSaveFolder, onOpenInvite, state.isTouched]); }, [onSaveFolder, 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'
@ -339,38 +326,36 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
</div> </div>
)} )}
{!isCreating && ( <div className="settings-item pt-3">
<div className="settings-item pt-3"> <h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}> {lang('FolderLinkScreen.Title')}
{lang('FolderLinkScreen.Title')} </h4>
</h4>
<ListItem
className="settings-folders-list-item color-primary mb-0"
icon="add"
onClick={handleCreateInviteClick}
>
{lang('ChatListFilter.CreateLinkNew')}
</ListItem>
{invites?.map((invite) => (
<ListItem <ListItem
className="settings-folders-list-item color-primary mb-0" className="settings-folders-list-item mb-0"
icon="add" icon="link"
onClick={handleCreateInviteClick} multiline
// eslint-disable-next-line react/jsx-no-bind
onClick={handleEditInviteClick}
clickArg={invite.url}
> >
{lang('ChatListFilter.CreateLinkNew')} <span className="title" dir="auto">{invite.title || invite.url}</span>
<span className="subtitle">
{lang('ChatListFilter.LinkLabelChatCount', invite.peerIds.length, 'i')}
</span>
</ListItem> </ListItem>
))}
{invites?.map((invite) => ( </div>
<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

View File

@ -1,5 +1,5 @@
import React, { import React, {
memo, useCallback, useEffect, useMemo, useState, memo, useCallback, useEffect, useMemo, useRef, useState,
} from '../../../../lib/teact/teact'; } from '../../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../../global'; import { getActions, getGlobal, withGlobal } from '../../../../global';
@ -93,13 +93,19 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
const [selectedIds, setSelectedIds] = useState<string[]>(peerIds || []); const [selectedIds, setSelectedIds] = useState<string[]>(peerIds || []);
useEffectWithPrevDeps(([prevIsLoading]) => { const isFirstRenderRef = useRef(true);
if (isLoading && !prevIsLoading) { useEffectWithPrevDeps(([prevUrl]) => {
if (prevUrl !== url) {
isFirstRenderRef.current = true;
}
if (!isFirstRenderRef.current) return;
isFirstRenderRef.current = false;
if (!url) {
setSelectedIds(unlockedIds); setSelectedIds(unlockedIds);
} else if (peerIds) { } else if (peerIds) {
setSelectedIds(peerIds); setSelectedIds(peerIds);
} }
}, [isLoading, unlockedIds, peerIds]); }, [url, unlockedIds, peerIds]);
const handleClickDisabled = useCallback((id: string) => { const handleClickDisabled = useCallback((id: string) => {
const global = getGlobal(); const global = getGlobal();
@ -135,9 +141,10 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
}, [folderId, selectedIds, url]); }, [folderId, selectedIds, url]);
const chatsCount = selectedIds.length; const chatsCount = selectedIds.length;
const isDisabled = !chatsCount || isLoading;
return ( return (
<div className="settings-content no-border custom-scroll"> <div className="settings-content no-border custom-scroll SettingsFoldersChatsPicker">
<div className="settings-content-header"> <div className="settings-content-header">
<AnimatedIcon <AnimatedIcon
size={STICKER_SIZE_FOLDER_SETTINGS} size={STICKER_SIZE_FOLDER_SETTINGS}
@ -154,6 +161,7 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
<InviteLink <InviteLink
inviteLink={isLoading ? lang('Loading') : url!} inviteLink={isLoading ? lang('Loading') : url!}
onRevoke={handleRevoke} onRevoke={handleRevoke}
isDisabled={isDisabled}
/> />
<div className="settings-item settings-item-chatlist"> <div className="settings-item settings-item-chatlist">
@ -163,12 +171,13 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
onSelectedIdsChange={handleSelectedIdsChange} onSelectedIdsChange={handleSelectedIdsChange}
selectedIds={selectedIds} selectedIds={selectedIds}
onDisabledClick={handleClickDisabled} onDisabledClick={handleClickDisabled}
isRoundCheckbox
/> />
</div> </div>
<FloatingActionButton <FloatingActionButton
isShown={isLoading || isTouched} isShown={isLoading || isTouched}
disabled={isLoading} disabled={isDisabled}
onClick={handleSubmit} onClick={handleSubmit}
ariaLabel="Save changes" ariaLabel="Save changes"
> >

View File

@ -764,12 +764,29 @@ addActionHandler('addChatFolder', async (global, actions, payload): Promise<void
// Clear fields from recommended folders // Clear fields from recommended folders
const { id: recommendedId, description, ...newFolder } = folder; const { id: recommendedId, description, ...newFolder } = folder;
const newId = maxId + 1;
const folderUpdate = {
id: newId,
...newFolder,
};
await callApi('editChatFolder', { await callApi('editChatFolder', {
id: maxId + 1, id: newId,
folderUpdate: { folderUpdate,
id: maxId + 1, });
...newFolder,
// Update called from the above `callApi` is throttled, but we need to apply changes immediately
actions.apiUpdate({
'@type': 'updateChatFolder',
id: newId,
folder: folderUpdate,
});
actions.requestNextSettingsScreen({
foldersAction: {
type: 'setFolderId',
payload: maxId + 1,
}, },
tabId,
}); });
if (!description) { if (!description) {
@ -2019,33 +2036,43 @@ addActionHandler('editChatlistInvite', async (global, actions, payload): Promise
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
const result = await callApi('editChatlistInvite', { folderId, slug, peers }); try {
const result = await callApi('editChatlistInvite', { folderId, slug, peers });
if (!result) return; if (!result) {
return;
}
global = getGlobal(); global = getGlobal();
global = { global = {
...global, ...global,
chatFolders: { chatFolders: {
...global.chatFolders, ...global.chatFolders,
invites: { invites: {
...global.chatFolders.invites, ...global.chatFolders.invites,
[folderId]: global.chatFolders.invites[folderId]?.map((invite) => { [folderId]: global.chatFolders.invites[folderId]?.map((invite) => {
if (invite.url === url) { if (invite.url === url) {
return result; return result;
} }
return invite; return invite;
}), }),
},
}, },
}, };
}; setGlobal(global);
global = updateTabState(global, { } catch (error) {
shareFolderScreen: { actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId });
...selectTabState(global, tabId).shareFolderScreen!, } finally {
isLoading: false, global = getGlobal();
},
}, tabId); global = updateTabState(global, {
setGlobal(global); shareFolderScreen: {
...selectTabState(global, tabId).shareFolderScreen!,
isLoading: false,
},
}, tabId);
setGlobal(global);
}
}); });
addActionHandler('deleteChatlistInvite', async (global, actions, payload): Promise<void> => { addActionHandler('deleteChatlistInvite', async (global, actions, payload): Promise<void> => {

View File

@ -4,7 +4,7 @@ import type { ApiUpdateChat } from '../../../api/types';
import type { ActionReturnType } from '../../types'; import type { ActionReturnType } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config'; import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { buildCollectionByKey, omit } from '../../../util/iteratees';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications'; import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { import {
@ -271,6 +271,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
...global.chatFolders, ...global.chatFolders,
byId: newChatFoldersById, byId: newChatFoldersById,
orderedIds: newOrderedIds, orderedIds: newOrderedIds,
invites: omit(global.chatFolders.invites, [id]),
}, },
}; };
} }

View File

@ -252,7 +252,7 @@ export function selectCanInviteToChat<T extends GlobalState>(global: T, chatId:
if (!chat) return false; if (!chat) return false;
// https://github.com/TelegramMessenger/Telegram-iOS/blob/5126be83b3b9578fb014eb52ca553da9e7a8b83a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift#L6 // https://github.com/TelegramMessenger/Telegram-iOS/blob/5126be83b3b9578fb014eb52ca553da9e7a8b83a/submodules/TelegramCore/Sources/TelegramEngine/Peers/Communities.swift#L6
return Boolean(!isUserId(chatId) && ((isChatChannel(chat) || isChatSuperGroup(chat)) ? ( return !chat.migratedTo && Boolean(!isUserId(chatId) && ((isChatChannel(chat) || isChatSuperGroup(chat)) ? (
chat.isCreator || getHasAdminRight(chat, 'inviteUsers') chat.isCreator || getHasAdminRight(chat, 'inviteUsers')
|| (chat.usernames?.length && !chat.isJoinRequest) || (chat.usernames?.length && !chat.isJoinRequest)
) : (chat.isCreator || getHasAdminRight(chat, 'inviteUsers')))); ) : (chat.isCreator || getHasAdminRight(chat, 'inviteUsers'))));

View File

@ -122,9 +122,10 @@ export type FoldersState = {
}; };
export type FoldersActions = ( export type FoldersActions = (
'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' | 'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' |
'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' 'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' | 'setIsTouched' |
'setFolderId' | 'setIsChatlist'
); );
export type FolderEditDispatch = Dispatch<FoldersActions>; export type FolderEditDispatch = Dispatch<FoldersState, FoldersActions>;
const INITIAL_STATE: FoldersState = { const INITIAL_STATE: FoldersState = {
mode: 'create', mode: 'create',
@ -150,6 +151,12 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
}, },
isTouched: true, isTouched: true,
}; };
case 'setFolderId':
return {
...state,
folderId: action.payload,
mode: 'edit',
};
case 'editIncludeFilters': case 'editIncludeFilters':
return { return {
...state, ...state,
@ -221,6 +228,12 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
chatFilter: action.payload, chatFilter: action.payload,
}; };
} }
case 'setIsTouched': {
return {
...state,
isTouched: action.payload,
};
}
case 'setIsLoading': { case 'setIsLoading': {
return { return {
...state, ...state,
@ -230,9 +243,18 @@ const foldersReducer: StateReducer<FoldersState, FoldersActions> = (
case 'setError': { case 'setError': {
return { return {
...state, ...state,
isLoading: false,
error: action.payload, error: action.payload,
}; };
} }
case 'setIsChatlist':
return {
...state,
folder: {
...state.folder,
isChatList: action.payload,
},
};
case 'reset': case 'reset':
return INITIAL_STATE; return INITIAL_STATE;
default: default:

View File

@ -32,7 +32,7 @@ export type FormActions = (
'changeBillingZip' | 'changeSaveInfo' | 'changeSaveCredentials' | 'setFormErrors' | 'resetState' | 'setTipAmount' | 'changeBillingZip' | 'changeSaveInfo' | 'changeSaveCredentials' | 'setFormErrors' | 'resetState' | 'setTipAmount' |
'changeSavedCredentialId' 'changeSavedCredentialId'
); );
export type FormEditDispatch = Dispatch<FormActions>; export type FormEditDispatch = Dispatch<FormState, FormActions>;
const INITIAL_STATE: FormState = { const INITIAL_STATE: FormState = {
streetLine1: '', streetLine1: '',

View File

@ -4,7 +4,7 @@ import useReducer from '../useReducer';
export type TwoFaActions = ( export type TwoFaActions = (
'setCurrentPassword' | 'setPassword' | 'setHint' | 'setEmail' | 'reset' 'setCurrentPassword' | 'setPassword' | 'setHint' | 'setEmail' | 'reset'
); );
export type TwoFaDispatch = Dispatch<TwoFaActions>; export type TwoFaDispatch = Dispatch<TwoFaState, TwoFaActions>;
export type TwoFaState = { export type TwoFaState = {
currentPassword: string; currentPassword: string;

View File

@ -4,7 +4,7 @@ import useForceUpdate from './useForceUpdate';
export type ReducerAction<Actions> = { type: Actions; payload?: any }; export type ReducerAction<Actions> = { type: Actions; payload?: any };
export type StateReducer<State, Actions> = (state: State, action: ReducerAction<Actions>) => State; export type StateReducer<State, Actions> = (state: State, action: ReducerAction<Actions>) => State;
export type Dispatch<Actions> = (action: ReducerAction<Actions>) => void; export type Dispatch<State, Actions> = (action: ReducerAction<Actions>) => State;
export default function useReducer<State, Actions>( export default function useReducer<State, Actions>(
reducer: StateReducer<State, Actions>, reducer: StateReducer<State, Actions>,
@ -17,10 +17,11 @@ export default function useReducer<State, Actions>(
const dispatch = useCallback((action: ReducerAction<Actions>) => { const dispatch = useCallback((action: ReducerAction<Actions>) => {
state.current = reducerRef.current(state.current, action); state.current = reducerRef.current(state.current, action);
forceUpdate(); forceUpdate();
return state.current;
}, []); }, []);
return [ return [
state.current, state.current,
dispatch, dispatch,
] as [State, Dispatch<Actions>]; ] as [State, Dispatch<State, Actions>];
} }

View File

@ -69,6 +69,8 @@ const READABLE_ERROR_MESSAGES: Record<string, string> = {
FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet', FRESH_CHANGE_ADMINS_FORBIDDEN: 'You were just elected admin, you can\'t add or modify other admins yet',
INPUT_USER_DEACTIVATED: 'The specified user was deleted', INPUT_USER_DEACTIVATED: 'The specified user was deleted',
BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired', BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired',
PEERS_LIST_EMPTY: 'No chats are added to the list',
}; };
if (DEBUG) { if (DEBUG) {