From f2d14ca78fd127dcc62b2ed9793743ea74fe0dad Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Wed, 4 Jun 2025 20:41:25 +0200 Subject: [PATCH] Manage Discussion: Fix creating new linked chat in channels (#5958) Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com> --- src/api/gramjs/methods/chats.ts | 24 ++- src/assets/localization/fallback.strings | 2 + src/components/left/main/ContactList.tsx | 1 - src/components/left/newChat/NewChatStep2.tsx | 1 + src/components/right/RightColumn.tsx | 3 + src/components/right/RightHeader.scss | 2 +- src/components/right/RightHeader.tsx | 5 + .../right/management/ManageDiscussion.tsx | 10 +- .../right/management/ManageInvites.tsx | 2 +- .../right/management/Management.scss | 9 +- .../right/management/Management.tsx | 11 ++ .../right/management/NewDiscussionGroup.tsx | 142 ++++++++++++++++++ src/global/actions/api/chats.ts | 22 ++- src/global/types/actions.ts | 7 +- src/types/index.ts | 2 + src/types/language.d.ts | 3 + 16 files changed, 229 insertions(+), 17 deletions(-) create mode 100644 src/components/right/management/NewDiscussionGroup.tsx diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index ab1c75363..88f949eaa 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -347,14 +347,27 @@ export async function fetchSavedChats({ }; } -export function fetchFullChat(chat: ApiChat) { +const fullChatRequestDedupe = new Map>(); +export async function fetchFullChat(chat: ApiChat) { const { id } = chat; + if (fullChatRequestDedupe.has(id)) { + return fullChatRequestDedupe.get(id); + } + const type = getEntityTypeById(chat.id); - return type === 'channel' + const promise = type === 'channel' ? getFullChannelInfo(chat) : getFullChatInfo(id); + + fullChatRequestDedupe.set(id, promise); + + promise.finally(() => { + fullChatRequestDedupe.delete(id); + }); + + return promise; } export async function fetchPeerSettings(peer: ApiPeer) { @@ -801,14 +814,15 @@ export function updateTopicMutedState({ } export async function createChannel({ - title, about = '', users, + title, about = '', users, isBroadcast, isMegagroup, }: { - title: string; about?: string; users?: ApiUser[]; + title: string; about?: string; users?: ApiUser[]; isBroadcast?: true; isMegagroup?: true; }) { const result = await invokeRequest(new GramJs.channels.CreateChannel({ - broadcast: true, + broadcast: isBroadcast, title, about, + megagroup: isMegagroup, }), { shouldThrow: true, }); diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 6581ca48e..0c34e2f00 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1083,6 +1083,7 @@ "ChannelPersmissionDeniedSendMessagesForever" = "The admins of this group have restricted your ability to send messages."; "ChannelPersmissionDeniedSendMessagesDefaultRestrictedText" = "Sending messages is not allowed in this group."; "Chats" = "Chats"; +"NewDiscussionChatTitle" = "{name} Chat"; "FilterBots" = "Bots"; "FilterContacts" = "Contacts"; "FilterNonContacts" = "Non-Contacts"; @@ -1750,6 +1751,7 @@ "ActionChangedTitleYou" = "You changed group name to «{title}»"; "ActionChangedTitleChannel" = "Channel name was changed to «{title}»"; "ActionCreatedChat" = "{from} created the group «{title}»"; +"ActionCreatedChatYou" = "You created the group «{title}»"; "ActionCreatedChannel" = "Channel created"; "ActionGameScore_one" = "{from} scored {count} in {game}"; "ActionGameScore_other" = "{from} scored {count} in {game}"; diff --git a/src/components/left/main/ContactList.tsx b/src/components/left/main/ContactList.tsx index faad291f4..552ed1584 100644 --- a/src/components/left/main/ContactList.tsx +++ b/src/components/left/main/ContactList.tsx @@ -76,7 +76,6 @@ const ContactList: FC = ({ handleClick(id)} > = ({ about, photo, memberIds, + isChannel: true, }); }, [title, createChannel, about, photo, memberIds, channelTitleEmptyError]); diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx index 776d1c48d..fea7b4aaf 100644 --- a/src/components/right/RightColumn.tsx +++ b/src/components/right/RightColumn.tsx @@ -182,6 +182,9 @@ const RightColumn: FC = ({ setSelectedChatMemberId(undefined); setIsPromotedByCurrentUser(undefined); break; + case ManagementScreens.NewDiscussionGroup: + setManagementScreen(ManagementScreens.Discussion); + break; case ManagementScreens.ChatAdminRights: case ManagementScreens.ChatNewAdminRights: case ManagementScreens.GroupAddAdmins: diff --git a/src/components/right/RightHeader.scss b/src/components/right/RightHeader.scss index 4416121e1..d0f5ab550 100644 --- a/src/components/right/RightHeader.scss +++ b/src/components/right/RightHeader.scss @@ -23,7 +23,7 @@ .title { margin-bottom: 0; - margin-left: 1.375rem; + margin-left: 1.1875rem; font-size: 1.25rem; font-weight: var(--font-weight-medium); } diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index f40d39467..9b6842ecb 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -136,6 +136,7 @@ enum HeaderContent { CreateTopic, EditTopic, SavedDialogs, + NewDiscussionGroup, } const RightHeader: FC = ({ @@ -318,6 +319,8 @@ const RightHeader: FC = ({ HeaderContent.ManageInviteInfo ) : managementScreen === ManagementScreens.JoinRequests ? ( HeaderContent.ManageJoinRequests + ) : managementScreen === ManagementScreens.NewDiscussionGroup ? ( + HeaderContent.NewDiscussionGroup ) : undefined // Never reached ) : isStatistics ? ( HeaderContent.Statistics @@ -588,6 +591,8 @@ const RightHeader: FC = ({ )} ); + case HeaderContent.NewDiscussionGroup: + return

{oldLang('NewGroup')}

; default: return ( <> diff --git a/src/components/right/management/ManageDiscussion.tsx b/src/components/right/management/ManageDiscussion.tsx index 39f3f285d..3877023ad 100644 --- a/src/components/right/management/ManageDiscussion.tsx +++ b/src/components/right/management/ManageDiscussion.tsx @@ -15,6 +15,7 @@ import renderText from '../../common/helpers/renderText'; import useFlag from '../../../hooks/useFlag'; import useHistoryBack from '../../../hooks/useHistoryBack'; +import useLastCallback from '../../../hooks/useLastCallback.ts'; import useOldLang from '../../../hooks/useOldLang'; import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; @@ -198,6 +199,10 @@ const ManageDiscussion: FC = ({ ); } + const handleNewGroupClick = useLastCallback(() => { + onScreenSelect(ManagementScreens.NewDiscussionGroup); + }); + function renderDiscussionGroups() { return (
@@ -208,8 +213,10 @@ const ManageDiscussion: FC = ({ key="create-group" icon="group" ripple + className="create-item" + withPrimaryColor teactOrderKey={0} - disabled + onClick={handleNewGroupClick} > {lang('DiscussionCreateGroup')} @@ -219,7 +226,6 @@ const ManageDiscussion: FC = ({ key={id} teactOrderKey={i + 1} className="chat-item-clickable scroll-item" - onClick={() => { onDiscussionClick(id); }} diff --git a/src/components/right/management/ManageInvites.tsx b/src/components/right/management/ManageInvites.tsx index b33f1fbe3..4c8fa7db2 100644 --- a/src/components/right/management/ManageInvites.tsx +++ b/src/components/right/management/ManageInvites.tsx @@ -294,7 +294,7 @@ const ManageInvites: FC = ({
)}
- + {oldLang('CreateNewLink')} {(!temporalInvites || !temporalInvites.length) && } diff --git a/src/components/right/management/Management.scss b/src/components/right/management/Management.scss index bf3365bbd..72d3566cb 100644 --- a/src/components/right/management/Management.scss +++ b/src/components/right/management/Management.scss @@ -162,6 +162,13 @@ } } + .create-item { + .icon-group { + margin-inline-start: 0.1875rem; + margin-inline-end: 1.1875rem; + } + } + .Spinner { margin: 2rem auto; } @@ -172,7 +179,7 @@ } .ManageInvites { - .create-link { + .create-item { margin-bottom: 0.5rem; .icon-add { margin-inline-start: 0.1875rem; diff --git a/src/components/right/management/Management.tsx b/src/components/right/management/Management.tsx index 18a3be59f..75bde1d58 100644 --- a/src/components/right/management/Management.tsx +++ b/src/components/right/management/Management.tsx @@ -25,6 +25,7 @@ import ManageInvites from './ManageInvites'; import ManageJoinRequests from './ManageJoinRequests'; import ManageReactions from './ManageReactions'; import ManageUser from './ManageUser'; +import NewDiscussionGroup from './NewDiscussionGroup.tsx'; export type OwnProps = { chatId: string; @@ -202,6 +203,16 @@ const Management: FC = ({ /> ); + case ManagementScreens.NewDiscussionGroup: + return ( + + ); + case ManagementScreens.ChatNewAdminRights: case ManagementScreens.ChatAdminRights: return ( diff --git a/src/components/right/management/NewDiscussionGroup.tsx b/src/components/right/management/NewDiscussionGroup.tsx new file mode 100644 index 000000000..861f31a4c --- /dev/null +++ b/src/components/right/management/NewDiscussionGroup.tsx @@ -0,0 +1,142 @@ +import type { FC } from '../../../lib/teact/teact.ts'; +import { useState } from '../../../lib/teact/teact.ts'; +import React, { memo } from '../../../lib/teact/teact.ts'; + +import type { ApiChat } from '../../../api/types/index.ts'; +import type { ManagementScreens } from '../../../types/index.ts'; +import { ChatCreationProgress } from '../../../types/index.ts'; + +import { getActions, withGlobal } from '../../../global/index.ts'; +import { selectChat, selectTabState } from '../../../global/selectors/index.ts'; + +import useHistoryBack from '../../../hooks/useHistoryBack.ts'; +import useLang from '../../../hooks/useLang.ts'; +import useLastCallback from '../../../hooks/useLastCallback.ts'; + +import Icon from '../../common/icons/Icon.tsx'; +import AvatarEditable from '../../ui/AvatarEditable.tsx'; +import FloatingActionButton from '../../ui/FloatingActionButton.tsx'; +import InputText from '../../ui/InputText.tsx'; +import Spinner from '../../ui/Spinner.tsx'; + +type OwnProps = { + chatId: string; + isActive: boolean; + onScreenSelect: (screen: ManagementScreens) => void; + onClose: NoneToVoidFunction; +}; + +type StateProps = { + chat?: ApiChat; + creationProgress?: ChatCreationProgress; + creationError?: string; +}; + +const NewDiscussionGroup: FC = ({ + chat, + onClose, + isActive, + creationProgress, + creationError, +}) => { + const { createChannel } = getActions(); + const lang = useLang(); + + useHistoryBack({ + isActive, + onBack: onClose, + }); + + const [title, setTitle] = useState(lang('NewDiscussionChatTitle', { name: chat?.title })); + const [photo, setPhoto] = useState(); + const [error, setError] = useState(); + + const isLoading = creationProgress === ChatCreationProgress.InProgress; + + const handleTitleChange = useLastCallback((e: React.ChangeEvent) => { + const { value } = e.currentTarget; + const newValue = value.trimStart(); + + setTitle(newValue); + + if (newValue !== value) { + e.currentTarget.value = newValue; + } + }); + + const renderedError = (creationError && lang('NewChatTitleEmptyError')) || ( + error !== lang('NewChatTitleEmptyError') && error !== lang('NewChannelTitleEmptyError') + ? error + : undefined + ); + + const handleCreateGroup = useLastCallback(() => { + if (!title.length) { + setError(lang('NewChatTitleEmptyError')); + return; + } + if (!chat) return; + + createChannel({ + discussionChannelId: chat.id, + title, + photo, + isSuperGroup: true, + }); + }); + + return ( +
+
+
+
+ + + + {renderedError && ( +

{renderedError}

+ )} +
+ + + {isLoading ? ( + + ) : ( + + )} + +
+
+
+ ); +}; + +export default memo(withGlobal( + (global, { chatId }): StateProps => { + const { + progress: creationProgress, + error: creationError, + } = selectTabState(global).chatCreation || {}; + const chat = selectChat(global, chatId); + + return { + chat, + creationProgress, + creationError, + }; + }, +)(NewDiscussionGroup)); diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 8c59fe399..4974d0698 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -674,11 +674,12 @@ addActionHandler('updateTopicMutedState', (global, actions, payload): ActionRetu addActionHandler('createChannel', async (global, actions, payload): Promise => { const { - title, about, photo, memberIds, tabId = getCurrentTabId(), + title, about, photo, memberIds, discussionChannelId, tabId = getCurrentTabId(), } = payload; + const isChannel = 'isChannel' in payload ? payload.isChannel : undefined; + const isSuperGroup = 'isSuperGroup' in payload ? payload.isSuperGroup : undefined; - const users = (memberIds) - .map((id) => selectUser(global, id)) + const users = memberIds?.map((id) => selectUser(global, id)) .filter(Boolean); global = updateTabState(global, { @@ -691,7 +692,13 @@ addActionHandler('createChannel', async (global, actions, payload): Promise { 'ComposerTitleForwardFrom': { 'users': V; }; + 'NewDiscussionChatTitle': { + 'name': V; + } } export interface LangPairPlural {