From 4b96cc5bfea7bf8119393a7fb8092b0b1419e633 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 5 May 2023 15:53:15 +0400 Subject: [PATCH] Folders: Fix sharing (#3142) --- src/api/gramjs/methods/chats.ts | 2 +- src/components/common/InviteLink.module.scss | 1 + src/components/common/InviteLink.tsx | 16 ++- src/components/common/Picker.tsx | 52 +++++--- .../left/settings/folders/SettingsFolders.tsx | 58 ++++++++- .../settings/folders/SettingsFoldersEdit.tsx | 113 ++++++++---------- .../folders/SettingsShareChatlist.tsx | 21 +++- src/global/actions/api/chats.ts | 83 ++++++++----- src/global/actions/apiUpdaters/chats.ts | 3 +- src/global/selectors/chats.ts | 2 +- src/hooks/reducers/useFoldersReducer.ts | 26 +++- src/hooks/reducers/usePaymentReducer.ts | 2 +- src/hooks/reducers/useTwoFaReducer.ts | 2 +- src/hooks/useReducer.ts | 5 +- src/util/getReadableErrorText.ts | 2 + 15 files changed, 256 insertions(+), 132 deletions(-) diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 03f928c2d..f42e07c9a 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1681,7 +1681,7 @@ export async function editChatlistInvite({ slug, title, peers: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)), - })); + }), undefined, true); if (!result) return undefined; return buildApiChatlistExportedInvite(result); diff --git a/src/components/common/InviteLink.module.scss b/src/components/common/InviteLink.module.scss index 60810e6d1..6fd7de600 100644 --- a/src/components/common/InviteLink.module.scss +++ b/src/components/common/InviteLink.module.scss @@ -6,6 +6,7 @@ cursor: var(--custom-cursor, pointer); margin-bottom: 1rem; padding-right: 3rem; + text-overflow: ellipsis; } .moreMenu { diff --git a/src/components/common/InviteLink.tsx b/src/components/common/InviteLink.tsx index c42ebc7a2..50e92166f 100644 --- a/src/components/common/InviteLink.tsx +++ b/src/components/common/InviteLink.tsx @@ -18,12 +18,14 @@ type OwnProps = { title?: string; inviteLink: string; onRevoke?: VoidFunction; + isDisabled?: boolean; }; const InviteLink: FC = ({ title, inviteLink, onRevoke, + isDisabled, }) => { const lang = useLang(); const { showNotification, openChatWithDraft } = getActions(); @@ -85,10 +87,20 @@ const InviteLink: FC = ({
- -
diff --git a/src/components/common/Picker.tsx b/src/components/common/Picker.tsx index fff6ad814..ebfbc585c 100644 --- a/src/components/common/Picker.tsx +++ b/src/components/common/Picker.tsx @@ -6,6 +6,7 @@ import { requestMutation } from '../../lib/fasterdom/fasterdom'; import type { FC } from '../../lib/teact/teact'; import { isUserId } from '../../global/helpers'; +import buildClassName from '../../util/buildClassName'; import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import useInfiniteScroll from '../../hooks/useInfiniteScroll'; import useLang from '../../hooks/useLang'; @@ -32,6 +33,7 @@ type OwnProps = { isLoading?: boolean; noScrollRestore?: boolean; isSearchable?: boolean; + isRoundCheckbox?: boolean; lockedIds?: string[]; onSelectedIdsChange?: (ids: string[]) => void; onFilterChange?: (value: string) => void; @@ -55,6 +57,7 @@ const Picker: FC = ({ isLoading, noScrollRestore, isSearchable, + isRoundCheckbox, lockedIds, onSelectedIdsChange, onFilterChange, @@ -161,24 +164,37 @@ const Picker: FC = ({ onLoadMore={getMore} noScrollRestore={noScrollRestore} > - {viewportIds.map((id) => ( - handleItemClick(id)} - ripple - > - - {isUserId(id) ? ( - - ) : ( - - )} - - ))} + {viewportIds.map((id) => { + const renderCheckbox = () => { + return ( + + ); + }; + return ( + handleItemClick(id)} + ripple + > + {!isRoundCheckbox ? renderCheckbox() : undefined} + {isUserId(id) ? ( + + ) : ( + + )} + {isRoundCheckbox ? renderCheckbox() : undefined} + + ); + })} ) : !isLoading && viewportIds && !viewportIds.length ? (

{notFoundText || 'Sorry, nothing found.'}

diff --git a/src/components/left/settings/folders/SettingsFolders.tsx b/src/components/left/settings/folders/SettingsFolders.tsx index 65e396fc6..d0a68080b 100644 --- a/src/components/left/settings/folders/SettingsFolders.tsx +++ b/src/components/left/settings/folders/SettingsFolders.tsx @@ -6,8 +6,10 @@ import type { ApiChatFolder } from '../../../../api/types'; import { SettingsScreens } from '../../../../types'; import type { FolderEditDispatch, FoldersState } from '../../../../hooks/reducers/useFoldersReducer'; +import { selectChatFilters } from '../../../../hooks/reducers/useFoldersReducer'; + import SettingsFoldersMain from './SettingsFoldersMain'; -import SettingsFoldersEdit from './SettingsFoldersEdit'; +import SettingsFoldersEdit, { ERROR_NO_CHATS, ERROR_NO_TITLE } from './SettingsFoldersEdit'; import SettingsFoldersChatFilters from './SettingsFoldersChatFilters'; import SettingsShareChatlist from './SettingsShareChatlist'; @@ -34,7 +36,11 @@ const SettingsFolders: FC = ({ onScreenSelect, onReset, }) => { - const { openShareChatFolderModal } = getActions(); + const { + openShareChatFolderModal, + editChatFolder, + addChatFolder, + } = getActions(); const handleReset = useCallback(() => { if ( @@ -66,10 +72,50 @@ const SettingsFolders: FC = ({ 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(() => { - dispatch({ type: 'saveFilters' }); + const newState = dispatch({ type: 'saveFilters' }); handleReset(); - }, [dispatch, handleReset]); + saveState(newState); + }, [dispatch, handleReset, saveState]); const handleCreateFolder = useCallback(() => { dispatch({ type: 'reset' }); @@ -97,8 +143,9 @@ const SettingsFolders: FC = ({ const handleShareFolder = useCallback(() => { openShareChatFolderModal({ folderId: state.folderId!, noRequestNextScreen: true }); + dispatch({ type: 'setIsChatlist', payload: true }); onScreenSelect(SettingsScreens.FoldersShare); - }, [onScreenSelect, state.folderId]); + }, [dispatch, onScreenSelect, state.folderId]); const handleOpenInvite = useCallback((url: string) => { openShareChatFolderModal({ folderId: state.folderId!, url, noRequestNextScreen: true }); @@ -139,6 +186,7 @@ const SettingsFolders: FC = ({ ].includes(shownScreen)} isOnlyInvites={currentScreen === SettingsScreens.FoldersEditFolderInvites} onBack={onReset} + onSaveFolder={handleSaveFolder} /> ); case SettingsScreens.FoldersIncludedChats: diff --git a/src/components/left/settings/folders/SettingsFoldersEdit.tsx b/src/components/left/settings/folders/SettingsFoldersEdit.tsx index 3bdce725e..edcf55d3c 100644 --- a/src/components/left/settings/folders/SettingsFoldersEdit.tsx +++ b/src/components/left/settings/folders/SettingsFoldersEdit.tsx @@ -4,7 +4,7 @@ import React, { } from '../../../../lib/teact/teact'; 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 { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets'; @@ -45,6 +45,7 @@ type OwnProps = { isOnlyInvites?: boolean; onReset: () => void; onBack: () => void; + onSaveFolder: (cb?: VoidFunction) => void; }; type StateProps = { @@ -59,8 +60,8 @@ const SUBMIT_TIMEOUT = 500; const INITIAL_CHATS_LIMIT = 5; -const ERROR_NO_TITLE = 'Please provide a title for this folder.'; -const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty'; +export const ERROR_NO_TITLE = 'Please provide a title for this folder.'; +export const ERROR_NO_CHATS = 'ChatList.Filter.Error.Empty'; const SettingsFoldersEdit: FC = ({ state, @@ -78,10 +79,9 @@ const SettingsFoldersEdit: FC = ({ loadedArchivedChatIds, invites, maxInviteLinks, + onSaveFolder, }) => { const { - editChatFolder, - addChatFolder, loadChatlistInvites, openLimitReachedModal, showNotification, @@ -154,38 +154,23 @@ const SettingsFoldersEdit: FC = ({ dispatch({ type: 'setTitle', payload: currentTarget.value.trim() }); }, [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(() => { - handleSaveFolder(); - dispatch({ type: 'setIsLoading', payload: true }); - setTimeout(() => { - onReset(); - }, SUBMIT_TIMEOUT); - }, [dispatch, handleSaveFolder, onReset]); + + onSaveFolder(() => { + setTimeout(() => { + onReset(); + }, SUBMIT_TIMEOUT); + }); + }, [dispatch, onSaveFolder, onReset]); const handleCreateInviteClick = useCallback(() => { - if (!invites) return; + if (!invites) { + if (isCreating) { + onSaveFolder(onShareFolder); + } + return; + } // Ignoring global updates is a known drawback here if (!selectCanShareFolder(getGlobal(), state.folderId!)) { @@ -195,7 +180,7 @@ const SettingsFoldersEdit: FC = ({ if (invites.length < maxInviteLinks) { if (state.isTouched) { - handleSaveFolder(onShareFolder); + onSaveFolder(onShareFolder); } else { onShareFolder(); } @@ -204,15 +189,17 @@ const SettingsFoldersEdit: FC = ({ 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, url: string) => { if (state.isTouched) { - handleSaveFolder(() => onOpenInvite(url)); + onSaveFolder(() => onOpenInvite(url)); } else { onOpenInvite(url); } - }, [handleSaveFolder, onOpenInvite, state.isTouched]); + }, [onSaveFolder, onOpenInvite, state.isTouched]); function renderChatType(key: string, mode: 'included' | 'excluded') { const chatType = mode === 'included' @@ -339,38 +326,36 @@ const SettingsFoldersEdit: FC = ({ )} - {!isCreating && ( -
-

- {lang('FolderLinkScreen.Title')} -

+
+

+ {lang('FolderLinkScreen.Title')} +

+ + {lang('ChatListFilter.CreateLinkNew')} + + + {invites?.map((invite) => ( - {lang('ChatListFilter.CreateLinkNew')} + {invite.title || invite.url} + + {lang('ChatListFilter.LinkLabelChatCount', invite.peerIds.length, 'i')} + + ))} - {invites?.map((invite) => ( - - {invite.title || invite.url} - - {lang('ChatListFilter.LinkLabelChatCount', invite.peerIds.length, 'i')} - - - ))} - -
- )} +
= ({ const [selectedIds, setSelectedIds] = useState(peerIds || []); - useEffectWithPrevDeps(([prevIsLoading]) => { - if (isLoading && !prevIsLoading) { + const isFirstRenderRef = useRef(true); + useEffectWithPrevDeps(([prevUrl]) => { + if (prevUrl !== url) { + isFirstRenderRef.current = true; + } + if (!isFirstRenderRef.current) return; + isFirstRenderRef.current = false; + if (!url) { setSelectedIds(unlockedIds); } else if (peerIds) { setSelectedIds(peerIds); } - }, [isLoading, unlockedIds, peerIds]); + }, [url, unlockedIds, peerIds]); const handleClickDisabled = useCallback((id: string) => { const global = getGlobal(); @@ -135,9 +141,10 @@ const SettingsShareChatlist: FC = ({ }, [folderId, selectedIds, url]); const chatsCount = selectedIds.length; + const isDisabled = !chatsCount || isLoading; return ( -
+
= ({
@@ -163,12 +171,13 @@ const SettingsShareChatlist: FC = ({ onSelectedIdsChange={handleSelectedIdsChange} selectedIds={selectedIds} onDisabledClick={handleClickDisabled} + isRoundCheckbox />
diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index dcedb022c..6105e8c1c 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -764,12 +764,29 @@ addActionHandler('addChatFolder', async (global, actions, payload): Promise { - if (invite.url === url) { - return result; - } - return invite; - }), + 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); + }; + setGlobal(global); + } catch (error) { + actions.showDialog({ data: { ...(error as ApiError), hasErrorKey: true }, tabId }); + } finally { + global = getGlobal(); + + global = updateTabState(global, { + shareFolderScreen: { + ...selectTabState(global, tabId).shareFolderScreen!, + isLoading: false, + }, + }, tabId); + setGlobal(global); + } }); addActionHandler('deleteChatlistInvite', async (global, actions, payload): Promise => { diff --git a/src/global/actions/apiUpdaters/chats.ts b/src/global/actions/apiUpdaters/chats.ts index bbf465310..dd26166c3 100644 --- a/src/global/actions/apiUpdaters/chats.ts +++ b/src/global/actions/apiUpdaters/chats.ts @@ -4,7 +4,7 @@ import type { ApiUpdateChat } from '../../../api/types'; import type { ActionReturnType } from '../../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 { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications'; import { @@ -271,6 +271,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { ...global.chatFolders, byId: newChatFoldersById, orderedIds: newOrderedIds, + invites: omit(global.chatFolders.invites, [id]), }, }; } diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 4ec79dad1..5b9dc969b 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -252,7 +252,7 @@ export function selectCanInviteToChat(global: T, 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)) ? ( + return !chat.migratedTo && Boolean(!isUserId(chatId) && ((isChatChannel(chat) || isChatSuperGroup(chat)) ? ( chat.isCreator || getHasAdminRight(chat, 'inviteUsers') || (chat.usernames?.length && !chat.isJoinRequest) ) : (chat.isCreator || getHasAdminRight(chat, 'inviteUsers')))); diff --git a/src/hooks/reducers/useFoldersReducer.ts b/src/hooks/reducers/useFoldersReducer.ts index a6a4e3b9e..e7d94f1ff 100644 --- a/src/hooks/reducers/useFoldersReducer.ts +++ b/src/hooks/reducers/useFoldersReducer.ts @@ -122,9 +122,10 @@ export type FoldersState = { }; export type FoldersActions = ( 'setTitle' | 'saveFilters' | 'editFolder' | 'reset' | 'setChatFilter' | 'setIsLoading' | 'setError' | - 'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' + 'editIncludeFilters' | 'editExcludeFilters' | 'setIncludeFilters' | 'setExcludeFilters' | 'setIsTouched' | + 'setFolderId' | 'setIsChatlist' ); -export type FolderEditDispatch = Dispatch; +export type FolderEditDispatch = Dispatch; const INITIAL_STATE: FoldersState = { mode: 'create', @@ -150,6 +151,12 @@ const foldersReducer: StateReducer = ( }, isTouched: true, }; + case 'setFolderId': + return { + ...state, + folderId: action.payload, + mode: 'edit', + }; case 'editIncludeFilters': return { ...state, @@ -221,6 +228,12 @@ const foldersReducer: StateReducer = ( chatFilter: action.payload, }; } + case 'setIsTouched': { + return { + ...state, + isTouched: action.payload, + }; + } case 'setIsLoading': { return { ...state, @@ -230,9 +243,18 @@ const foldersReducer: StateReducer = ( case 'setError': { return { ...state, + isLoading: false, error: action.payload, }; } + case 'setIsChatlist': + return { + ...state, + folder: { + ...state.folder, + isChatList: action.payload, + }, + }; case 'reset': return INITIAL_STATE; default: diff --git a/src/hooks/reducers/usePaymentReducer.ts b/src/hooks/reducers/usePaymentReducer.ts index e18c255a6..e38fede47 100644 --- a/src/hooks/reducers/usePaymentReducer.ts +++ b/src/hooks/reducers/usePaymentReducer.ts @@ -32,7 +32,7 @@ export type FormActions = ( 'changeBillingZip' | 'changeSaveInfo' | 'changeSaveCredentials' | 'setFormErrors' | 'resetState' | 'setTipAmount' | 'changeSavedCredentialId' ); -export type FormEditDispatch = Dispatch; +export type FormEditDispatch = Dispatch; const INITIAL_STATE: FormState = { streetLine1: '', diff --git a/src/hooks/reducers/useTwoFaReducer.ts b/src/hooks/reducers/useTwoFaReducer.ts index 346ba14f7..5a70f7fc4 100644 --- a/src/hooks/reducers/useTwoFaReducer.ts +++ b/src/hooks/reducers/useTwoFaReducer.ts @@ -4,7 +4,7 @@ import useReducer from '../useReducer'; export type TwoFaActions = ( 'setCurrentPassword' | 'setPassword' | 'setHint' | 'setEmail' | 'reset' ); -export type TwoFaDispatch = Dispatch; +export type TwoFaDispatch = Dispatch; export type TwoFaState = { currentPassword: string; diff --git a/src/hooks/useReducer.ts b/src/hooks/useReducer.ts index e0b7e1905..b76b38967 100644 --- a/src/hooks/useReducer.ts +++ b/src/hooks/useReducer.ts @@ -4,7 +4,7 @@ import useForceUpdate from './useForceUpdate'; export type ReducerAction = { type: Actions; payload?: any }; export type StateReducer = (state: State, action: ReducerAction) => State; -export type Dispatch = (action: ReducerAction) => void; +export type Dispatch = (action: ReducerAction) => State; export default function useReducer( reducer: StateReducer, @@ -17,10 +17,11 @@ export default function useReducer( const dispatch = useCallback((action: ReducerAction) => { state.current = reducerRef.current(state.current, action); forceUpdate(); + return state.current; }, []); return [ state.current, dispatch, - ] as [State, Dispatch]; + ] as [State, Dispatch]; } diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index 42c4dfd8f..7ebe0626f 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -69,6 +69,8 @@ const READABLE_ERROR_MESSAGES: Record = { 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', BOT_PRECHECKOUT_TIMEOUT: 'The request for payment has expired', + + PEERS_LIST_EMPTY: 'No chats are added to the list', }; if (DEBUG) {