Manage Discussion: Fix creating new linked chat in channels (#5958)

Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
Alexander Zinchuk 2025-06-04 20:41:25 +02:00
parent 9bdd213f8f
commit f2d14ca78f
16 changed files with 229 additions and 17 deletions

View File

@ -347,14 +347,27 @@ export async function fetchSavedChats({
};
}
export function fetchFullChat(chat: ApiChat) {
const fullChatRequestDedupe = new Map<string, Promise<FullChatData | undefined>>();
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,
});

View File

@ -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}";

View File

@ -76,7 +76,6 @@ const ContactList: FC<OwnProps & StateProps> = ({
<ListItem
key={id}
className="chat-item-clickable contact-list-item"
onClick={() => handleClick(id)}
>
<PrivateChatInfo

View File

@ -132,6 +132,7 @@ const NewChatStep2: FC<OwnProps & StateProps> = ({
about,
photo,
memberIds,
isChannel: true,
});
}, [title, createChannel, about, photo, memberIds, channelTitleEmptyError]);

View File

@ -182,6 +182,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
setSelectedChatMemberId(undefined);
setIsPromotedByCurrentUser(undefined);
break;
case ManagementScreens.NewDiscussionGroup:
setManagementScreen(ManagementScreens.Discussion);
break;
case ManagementScreens.ChatAdminRights:
case ManagementScreens.ChatNewAdminRights:
case ManagementScreens.GroupAddAdmins:

View File

@ -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);
}

View File

@ -136,6 +136,7 @@ enum HeaderContent {
CreateTopic,
EditTopic,
SavedDialogs,
NewDiscussionGroup,
}
const RightHeader: FC<OwnProps & StateProps> = ({
@ -318,6 +319,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
)}
</>
);
case HeaderContent.NewDiscussionGroup:
return <h3 className="title">{oldLang('NewGroup')}</h3>;
default:
return (
<>

View File

@ -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<OwnProps & StateProps> = ({
);
}
const handleNewGroupClick = useLastCallback(() => {
onScreenSelect(ManagementScreens.NewDiscussionGroup);
});
function renderDiscussionGroups() {
return (
<div>
@ -208,8 +213,10 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
key="create-group"
icon="group"
ripple
className="create-item"
withPrimaryColor
teactOrderKey={0}
disabled
onClick={handleNewGroupClick}
>
{lang('DiscussionCreateGroup')}
</ListItem>
@ -219,7 +226,6 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
key={id}
teactOrderKey={i + 1}
className="chat-item-clickable scroll-item"
onClick={() => {
onDiscussionClick(id);
}}

View File

@ -294,7 +294,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
</div>
)}
<div className="section" teactFastList>
<ListItem icon="add" withPrimaryColor key="create" className="create-link" onClick={handleCreateNewClick}>
<ListItem icon="add" withPrimaryColor key="create" className="create-item" onClick={handleCreateNewClick}>
{oldLang('CreateNewLink')}
</ListItem>
{(!temporalInvites || !temporalInvites.length) && <NothingFound text="No links found" key="nothing" />}

View File

@ -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;

View File

@ -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<OwnProps & StateProps> = ({
/>
);
case ManagementScreens.NewDiscussionGroup:
return (
<NewDiscussionGroup
chatId={chatId}
onScreenSelect={onScreenSelect}
isActive={isActive}
onClose={onClose}
/>
);
case ManagementScreens.ChatNewAdminRights:
case ManagementScreens.ChatAdminRights:
return (

View File

@ -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<OwnProps & StateProps> = ({
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<File | undefined>();
const [error, setError] = useState<string | undefined>();
const isLoading = creationProgress === ChatCreationProgress.InProgress;
const handleTitleChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="NewChat">
<div className="NewChat-inner step-2">
<AvatarEditable
onChange={setPhoto}
title={lang('AddPhoto')}
/>
<InputText
value={title}
onChange={handleTitleChange}
label={lang('GroupName')}
error={error === lang('NewChatTitleEmptyError')
|| error === lang('NewChannelTitleEmptyError') ? error : undefined}
/>
{renderedError && (
<p className="error">{renderedError}</p>
)}
</div>
<FloatingActionButton
isShown={title.length !== 0}
onClick={handleCreateGroup}
disabled={isLoading}
ariaLabel={lang('DiscussionCreateGroup')}
>
{isLoading ? (
<Spinner color="white" />
) : (
<Icon name="arrow-right" />
)}
</FloatingActionButton>
</div>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const {
progress: creationProgress,
error: creationError,
} = selectTabState(global).chatCreation || {};
const chat = selectChat(global, chatId);
return {
chat,
creationProgress,
creationError,
};
},
)(NewDiscussionGroup));

View File

@ -674,11 +674,12 @@ addActionHandler('updateTopicMutedState', (global, actions, payload): ActionRetu
addActionHandler('createChannel', async (global, actions, payload): Promise<void> => {
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<void
let createdChannel: ApiChat | undefined;
let missingInvitedUsers: ApiMissingInvitedUser[] | undefined;
try {
const result = await callApi('createChannel', { title, about, users });
const result = await callApi('createChannel', {
title,
about,
users,
isBroadcast: isChannel,
isMegagroup: isSuperGroup,
});
createdChannel = result?.channel;
missingInvitedUsers = result?.missingUsers;
} catch (error) {
@ -727,6 +734,13 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
},
}, tabId);
setGlobal(global);
if (discussionChannelId && channelId) {
actions.linkDiscussionGroup({
channelId: discussionChannelId,
chatId: channelId,
tabId,
});
}
actions.openChat({ id: channelId, shouldReplaceHistory: true, tabId });
if (missingInvitedUsers) {

View File

@ -685,8 +685,11 @@ export interface ActionPayloads {
title: string;
about?: string;
photo?: File;
memberIds: string[];
} & WithTabId;
memberIds?: string[];
discussionChannelId?: string;
} & (
{ isChannel: true } | { isSuperGroup: true }
) & WithTabId;
createGroupChat: {
title: string;
memberIds: string[];

View File

@ -331,6 +331,7 @@ export enum RightColumnContent {
CreateTopic,
EditTopic,
MonetizationStatistics,
NewGroup,
}
export type MediaViewerMedia = ApiPhoto | ApiVideo | ApiDocument;
@ -502,6 +503,7 @@ export enum ManagementScreens {
Reactions,
InviteInfo,
JoinRequests,
NewDiscussionGroup,
}
export type ManagementType = 'user' | 'group' | 'channel' | 'bot';

View File

@ -2483,6 +2483,9 @@ export interface LangPairWithVariables<V = LangVariable> {
'ComposerTitleForwardFrom': {
'users': V;
};
'NewDiscussionChatTitle': {
'name': V;
}
}
export interface LangPairPlural {