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; const { id } = chat;
if (fullChatRequestDedupe.has(id)) {
return fullChatRequestDedupe.get(id);
}
const type = getEntityTypeById(chat.id); const type = getEntityTypeById(chat.id);
return type === 'channel' const promise = type === 'channel'
? getFullChannelInfo(chat) ? getFullChannelInfo(chat)
: getFullChatInfo(id); : getFullChatInfo(id);
fullChatRequestDedupe.set(id, promise);
promise.finally(() => {
fullChatRequestDedupe.delete(id);
});
return promise;
} }
export async function fetchPeerSettings(peer: ApiPeer) { export async function fetchPeerSettings(peer: ApiPeer) {
@ -801,14 +814,15 @@ export function updateTopicMutedState({
} }
export async function createChannel({ 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({ const result = await invokeRequest(new GramJs.channels.CreateChannel({
broadcast: true, broadcast: isBroadcast,
title, title,
about, about,
megagroup: isMegagroup,
}), { }), {
shouldThrow: true, shouldThrow: true,
}); });

View File

@ -1083,6 +1083,7 @@
"ChannelPersmissionDeniedSendMessagesForever" = "The admins of this group have restricted your ability to send messages."; "ChannelPersmissionDeniedSendMessagesForever" = "The admins of this group have restricted your ability to send messages.";
"ChannelPersmissionDeniedSendMessagesDefaultRestrictedText" = "Sending messages is not allowed in this group."; "ChannelPersmissionDeniedSendMessagesDefaultRestrictedText" = "Sending messages is not allowed in this group.";
"Chats" = "Chats"; "Chats" = "Chats";
"NewDiscussionChatTitle" = "{name} Chat";
"FilterBots" = "Bots"; "FilterBots" = "Bots";
"FilterContacts" = "Contacts"; "FilterContacts" = "Contacts";
"FilterNonContacts" = "Non-Contacts"; "FilterNonContacts" = "Non-Contacts";
@ -1750,6 +1751,7 @@
"ActionChangedTitleYou" = "You changed group name to «{title}»"; "ActionChangedTitleYou" = "You changed group name to «{title}»";
"ActionChangedTitleChannel" = "Channel name was changed to «{title}»"; "ActionChangedTitleChannel" = "Channel name was changed to «{title}»";
"ActionCreatedChat" = "{from} created the group «{title}»"; "ActionCreatedChat" = "{from} created the group «{title}»";
"ActionCreatedChatYou" = "You created the group «{title}»";
"ActionCreatedChannel" = "Channel created"; "ActionCreatedChannel" = "Channel created";
"ActionGameScore_one" = "{from} scored {count} in {game}"; "ActionGameScore_one" = "{from} scored {count} in {game}";
"ActionGameScore_other" = "{from} scored {count} in {game}"; "ActionGameScore_other" = "{from} scored {count} in {game}";

View File

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

View File

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

View File

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

View File

@ -23,7 +23,7 @@
.title { .title {
margin-bottom: 0; margin-bottom: 0;
margin-left: 1.375rem; margin-left: 1.1875rem;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
} }

View File

@ -136,6 +136,7 @@ enum HeaderContent {
CreateTopic, CreateTopic,
EditTopic, EditTopic,
SavedDialogs, SavedDialogs,
NewDiscussionGroup,
} }
const RightHeader: FC<OwnProps & StateProps> = ({ const RightHeader: FC<OwnProps & StateProps> = ({
@ -318,6 +319,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
HeaderContent.ManageInviteInfo HeaderContent.ManageInviteInfo
) : managementScreen === ManagementScreens.JoinRequests ? ( ) : managementScreen === ManagementScreens.JoinRequests ? (
HeaderContent.ManageJoinRequests HeaderContent.ManageJoinRequests
) : managementScreen === ManagementScreens.NewDiscussionGroup ? (
HeaderContent.NewDiscussionGroup
) : undefined // Never reached ) : undefined // Never reached
) : isStatistics ? ( ) : isStatistics ? (
HeaderContent.Statistics HeaderContent.Statistics
@ -588,6 +591,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
)} )}
</> </>
); );
case HeaderContent.NewDiscussionGroup:
return <h3 className="title">{oldLang('NewGroup')}</h3>;
default: default:
return ( return (
<> <>

View File

@ -15,6 +15,7 @@ import renderText from '../../common/helpers/renderText';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLastCallback from '../../../hooks/useLastCallback.ts';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
@ -198,6 +199,10 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
); );
} }
const handleNewGroupClick = useLastCallback(() => {
onScreenSelect(ManagementScreens.NewDiscussionGroup);
});
function renderDiscussionGroups() { function renderDiscussionGroups() {
return ( return (
<div> <div>
@ -208,8 +213,10 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
key="create-group" key="create-group"
icon="group" icon="group"
ripple ripple
className="create-item"
withPrimaryColor
teactOrderKey={0} teactOrderKey={0}
disabled onClick={handleNewGroupClick}
> >
{lang('DiscussionCreateGroup')} {lang('DiscussionCreateGroup')}
</ListItem> </ListItem>
@ -219,7 +226,6 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
key={id} key={id}
teactOrderKey={i + 1} teactOrderKey={i + 1}
className="chat-item-clickable scroll-item" className="chat-item-clickable scroll-item"
onClick={() => { onClick={() => {
onDiscussionClick(id); onDiscussionClick(id);
}} }}

View File

@ -294,7 +294,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
</div> </div>
)} )}
<div className="section" teactFastList> <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')} {oldLang('CreateNewLink')}
</ListItem> </ListItem>
{(!temporalInvites || !temporalInvites.length) && <NothingFound text="No links found" key="nothing" />} {(!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 { .Spinner {
margin: 2rem auto; margin: 2rem auto;
} }
@ -172,7 +179,7 @@
} }
.ManageInvites { .ManageInvites {
.create-link { .create-item {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
.icon-add { .icon-add {
margin-inline-start: 0.1875rem; margin-inline-start: 0.1875rem;

View File

@ -25,6 +25,7 @@ import ManageInvites from './ManageInvites';
import ManageJoinRequests from './ManageJoinRequests'; import ManageJoinRequests from './ManageJoinRequests';
import ManageReactions from './ManageReactions'; import ManageReactions from './ManageReactions';
import ManageUser from './ManageUser'; import ManageUser from './ManageUser';
import NewDiscussionGroup from './NewDiscussionGroup.tsx';
export type OwnProps = { export type OwnProps = {
chatId: string; 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.ChatNewAdminRights:
case ManagementScreens.ChatAdminRights: case ManagementScreens.ChatAdminRights:
return ( 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> => { addActionHandler('createChannel', async (global, actions, payload): Promise<void> => {
const { const {
title, about, photo, memberIds, tabId = getCurrentTabId(), title, about, photo, memberIds, discussionChannelId, tabId = getCurrentTabId(),
} = payload; } = payload;
const isChannel = 'isChannel' in payload ? payload.isChannel : undefined;
const isSuperGroup = 'isSuperGroup' in payload ? payload.isSuperGroup : undefined;
const users = (memberIds) const users = memberIds?.map((id) => selectUser(global, id))
.map((id) => selectUser(global, id))
.filter(Boolean); .filter(Boolean);
global = updateTabState(global, { global = updateTabState(global, {
@ -691,7 +692,13 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
let createdChannel: ApiChat | undefined; let createdChannel: ApiChat | undefined;
let missingInvitedUsers: ApiMissingInvitedUser[] | undefined; let missingInvitedUsers: ApiMissingInvitedUser[] | undefined;
try { try {
const result = await callApi('createChannel', { title, about, users }); const result = await callApi('createChannel', {
title,
about,
users,
isBroadcast: isChannel,
isMegagroup: isSuperGroup,
});
createdChannel = result?.channel; createdChannel = result?.channel;
missingInvitedUsers = result?.missingUsers; missingInvitedUsers = result?.missingUsers;
} catch (error) { } catch (error) {
@ -727,6 +734,13 @@ addActionHandler('createChannel', async (global, actions, payload): Promise<void
}, },
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
if (discussionChannelId && channelId) {
actions.linkDiscussionGroup({
channelId: discussionChannelId,
chatId: channelId,
tabId,
});
}
actions.openChat({ id: channelId, shouldReplaceHistory: true, tabId }); actions.openChat({ id: channelId, shouldReplaceHistory: true, tabId });
if (missingInvitedUsers) { if (missingInvitedUsers) {

View File

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

View File

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

View File

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