Composer: Support sending message as channels (#1604)
This commit is contained in:
parent
a65e6c5af9
commit
72ae33139a
@ -22,6 +22,7 @@ import {
|
||||
ApiThreadInfo,
|
||||
ApiInvoice,
|
||||
ApiGroupCall,
|
||||
ApiUser,
|
||||
ApiSponsoredMessage,
|
||||
} from '../../types';
|
||||
|
||||
@ -857,6 +858,7 @@ export function buildLocalMessage(
|
||||
poll?: ApiNewPoll,
|
||||
groupedId?: string,
|
||||
scheduledAt?: number,
|
||||
sendAs?: ApiChat | ApiUser,
|
||||
serverTimeOffset = 0,
|
||||
): ApiMessage {
|
||||
const localId = localMessageCounter++;
|
||||
@ -880,7 +882,7 @@ export function buildLocalMessage(
|
||||
},
|
||||
date: scheduledAt || Math.round(Date.now() / 1000) + serverTimeOffset,
|
||||
isOutgoing: !isChannel,
|
||||
senderId: currentUserId,
|
||||
senderId: sendAs?.id || currentUserId,
|
||||
...(replyingTo && { replyToMessageId: replyingTo }),
|
||||
...(groupedId && {
|
||||
groupedId,
|
||||
|
||||
@ -97,12 +97,13 @@ export async function fetchInlineBotResults({
|
||||
}
|
||||
|
||||
export async function sendInlineBotResult({
|
||||
chat, resultId, queryId, replyingTo,
|
||||
chat, resultId, queryId, replyingTo, sendAs,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
resultId: string;
|
||||
queryId: string;
|
||||
replyingTo?: number;
|
||||
sendAs?: ApiUser | ApiChat;
|
||||
}) {
|
||||
const randomId = generateRandomBigInt();
|
||||
|
||||
@ -113,6 +114,7 @@ export async function sendInlineBotResult({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
id: resultId,
|
||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
}), true);
|
||||
}
|
||||
|
||||
|
||||
@ -402,6 +402,7 @@ async function getFullChannelInfo(
|
||||
hiddenPrehistory,
|
||||
call,
|
||||
botInfo,
|
||||
defaultSendAs,
|
||||
} = result.fullChat;
|
||||
|
||||
const inviteLink = exportedInvite instanceof GramJs.ChatInviteExported
|
||||
@ -452,6 +453,7 @@ async function getFullChannelInfo(
|
||||
groupCallId: call ? String(call.id) : undefined,
|
||||
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined,
|
||||
botCommands,
|
||||
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
|
||||
},
|
||||
users: [...(users || []), ...(bannedUsers || []), ...(adminUsers || [])],
|
||||
groupCall: call ? {
|
||||
|
||||
@ -22,7 +22,8 @@ export {
|
||||
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal,
|
||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage,
|
||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||
saveDefaultSendAs,
|
||||
} from './messages';
|
||||
|
||||
export {
|
||||
|
||||
@ -52,10 +52,15 @@ import {
|
||||
import localDb from '../localDb';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { fetchFile } from '../../../util/files';
|
||||
import { addMessageToLocalDb, deserializeBytes, resolveMessageApiChatId } from '../helpers';
|
||||
import {
|
||||
addEntitiesWithPhotosToLocalDb,
|
||||
addMessageToLocalDb,
|
||||
deserializeBytes,
|
||||
resolveMessageApiChatId,
|
||||
} from '../helpers';
|
||||
import { interpolateArray } from '../../../util/waveform';
|
||||
import { requestChatUpdate } from './chats';
|
||||
import { buildApiPeerId } from '../apiBuilders/peers';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
|
||||
const FAST_SEND_TIMEOUT = 1000;
|
||||
const INPUT_WAVEFORM_LENGTH = 63;
|
||||
@ -200,6 +205,7 @@ export function sendMessage(
|
||||
scheduledAt,
|
||||
groupedId,
|
||||
noWebPage,
|
||||
sendAs,
|
||||
serverTimeOffset,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
@ -214,12 +220,14 @@ export function sendMessage(
|
||||
scheduledAt?: number;
|
||||
groupedId?: string;
|
||||
noWebPage?: boolean;
|
||||
sendAs?: ApiUser | ApiChat;
|
||||
serverTimeOffset?: number;
|
||||
},
|
||||
onProgress?: ApiOnProgress,
|
||||
) {
|
||||
const localMessage = buildLocalMessage(
|
||||
chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt, serverTimeOffset,
|
||||
chat, text, entities, replyingTo, attachment, sticker, gif, poll, groupedId, scheduledAt,
|
||||
sendAs, serverTimeOffset,
|
||||
);
|
||||
onUpdate({
|
||||
'@type': localMessage.isScheduled ? 'newScheduledMessage' : 'newMessage',
|
||||
@ -289,6 +297,7 @@ export function sendMessage(
|
||||
...(replyingTo && { replyToMsgId: replyingTo }),
|
||||
...(media && { media }),
|
||||
...(noWebPage && { noWebpage: noWebPage }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
}), true);
|
||||
})();
|
||||
|
||||
@ -310,6 +319,7 @@ function sendGroupedMedia(
|
||||
groupedId,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
text?: string;
|
||||
@ -319,6 +329,7 @@ function sendGroupedMedia(
|
||||
groupedId: string;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiUser | ApiChat;
|
||||
},
|
||||
randomId: GramJs.long,
|
||||
localMessage: ApiMessage,
|
||||
@ -391,6 +402,7 @@ function sendGroupedMedia(
|
||||
replyToMsgId: replyingTo,
|
||||
...(isSilent && { silent: isSilent }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
}), true);
|
||||
})();
|
||||
|
||||
@ -1052,6 +1064,7 @@ export async function forwardMessages({
|
||||
serverTimeOffset,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
}: {
|
||||
fromChat: ApiChat;
|
||||
toChat: ApiChat;
|
||||
@ -1059,6 +1072,7 @@ export async function forwardMessages({
|
||||
serverTimeOffset: number;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiUser | ApiChat;
|
||||
}) {
|
||||
const messageIds = messages.map(({ id }) => id);
|
||||
const randomIds = messages.map(generateRandomBigInt);
|
||||
@ -1082,6 +1096,7 @@ export async function forwardMessages({
|
||||
id: messageIds,
|
||||
...(isSilent && { sil2ent: isSilent }),
|
||||
...(scheduledAt && { scheduleDate: scheduledAt }),
|
||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||
}), true);
|
||||
}
|
||||
|
||||
@ -1208,6 +1223,43 @@ export async function fetchSeenBy({ chat, messageId }: { chat: ApiChat; messageI
|
||||
return result ? result.map(String) : undefined;
|
||||
}
|
||||
|
||||
export async function fetchSendAs({
|
||||
chat,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.channels.GetSendAs({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addEntitiesWithPhotosToLocalDb(result.users);
|
||||
addEntitiesWithPhotosToLocalDb(result.chats);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter<ApiUser>(Boolean as any);
|
||||
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter<ApiChat>(Boolean as any);
|
||||
|
||||
return {
|
||||
users,
|
||||
chats,
|
||||
ids: result.peers.map(getApiChatIdFromMtpPeer),
|
||||
};
|
||||
}
|
||||
|
||||
export function saveDefaultSendAs({
|
||||
sendAs, chat,
|
||||
}: {
|
||||
sendAs: ApiChat | ApiUser; chat: ApiChat;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.SaveDefaultSendAs({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
sendAs: buildInputPeer(sendAs.id, sendAs.accessHash),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function fetchSponsoredMessages({ chat }: { chat: ApiChat }) {
|
||||
const result = await invokeRequest(new GramJs.channels.GetSponsoredMessages({
|
||||
channel: buildInputPeer(chat.id, chat.accessHash),
|
||||
|
||||
@ -55,6 +55,8 @@ export interface ApiChat {
|
||||
fullInfo?: ApiChatFullInfo;
|
||||
// Obtained with UpdateUserTyping or UpdateChatUserTyping updates
|
||||
typingStatus?: ApiTypingStatus;
|
||||
|
||||
sendAsIds?: string[];
|
||||
}
|
||||
|
||||
export interface ApiTypingStatus {
|
||||
@ -83,6 +85,7 @@ export interface ApiChatFullInfo {
|
||||
};
|
||||
linkedChatId?: string;
|
||||
botCommands?: ApiBotCommand[];
|
||||
sendAsId?: string;
|
||||
}
|
||||
|
||||
export interface ApiChatMember {
|
||||
|
||||
@ -40,6 +40,7 @@ export { default as DropArea } from '../components/middle/composer/DropArea';
|
||||
export { default as TextFormatter } from '../components/middle/composer/TextFormatter';
|
||||
export { default as EmojiTooltip } from '../components/middle/composer/EmojiTooltip';
|
||||
export { default as InlineBotTooltip } from '../components/middle/composer/InlineBotTooltip';
|
||||
export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu';
|
||||
|
||||
export { default as RightSearch } from '../components/right/RightSearch';
|
||||
export { default as StickerSearch } from '../components/right/StickerSearch';
|
||||
|
||||
@ -33,6 +33,22 @@
|
||||
--border-width: 0;
|
||||
}
|
||||
|
||||
@keyframes show-send-as-button {
|
||||
from {
|
||||
width: 1rem;
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
to {
|
||||
width: 3.5rem;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
body:not(.animation-level-0) & .send-as-button {
|
||||
animation: 0.25s ease-in-out forwards show-send-as-button;
|
||||
}
|
||||
|
||||
> .Button {
|
||||
flex-shrink: 0;
|
||||
margin-left: .5rem;
|
||||
@ -118,6 +134,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.send-as-button {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mobile-symbol-menu-button {
|
||||
width: 2.875rem;
|
||||
height: 2.875rem;
|
||||
|
||||
@ -96,6 +96,8 @@ import DropArea, { DropAreaState } from './DropArea.async';
|
||||
import WebPagePreview from './WebPagePreview';
|
||||
import Portal from '../../ui/Portal';
|
||||
import CalendarModal from '../../common/CalendarModal.async';
|
||||
import SendAsMenu from './SendAsMenu.async';
|
||||
import Avatar from '../../common/Avatar';
|
||||
|
||||
import './Composer.scss';
|
||||
|
||||
@ -140,6 +142,9 @@ type StateProps =
|
||||
inlineBots?: Record<string, false | InlineBotSettings>;
|
||||
botCommands?: ApiBotCommand[] | false;
|
||||
chatBotCommands?: ApiBotCommand[];
|
||||
sendAsUser?: ApiUser;
|
||||
sendAsChat?: ApiChat;
|
||||
sendAsId?: string;
|
||||
}
|
||||
& Pick<GlobalState, 'connectionState'>;
|
||||
|
||||
@ -199,6 +204,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
isInlineBotLoading,
|
||||
botCommands,
|
||||
chatBotCommands,
|
||||
sendAsUser,
|
||||
sendAsChat,
|
||||
sendAsId,
|
||||
}) => {
|
||||
const {
|
||||
sendMessage,
|
||||
@ -213,8 +221,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
openChat,
|
||||
addRecentEmoji,
|
||||
sendInlineBotResult,
|
||||
loadSendAs,
|
||||
} = getDispatch();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -227,6 +235,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
scheduledMessageArgs, setScheduledMessageArgs,
|
||||
] = useState<GlobalState['messages']['contentToBeScheduled'] | undefined>();
|
||||
const { width: windowWidth } = windowSize.get();
|
||||
const sendAsIds = chat?.sendAsIds;
|
||||
const sendMessageAction = useSendMessageAction(chatId, threadId);
|
||||
|
||||
// Cache for frequently updated state
|
||||
@ -245,6 +254,12 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [isReady, chatId, loadScheduledHistory, lastSyncTime, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (chatId && lastSyncTime && !sendAsIds && isReady) {
|
||||
loadSendAs({ chatId });
|
||||
}
|
||||
}, [chatId, isReady, lastSyncTime, loadSendAs, sendAsIds]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!appendixRef.current) return;
|
||||
|
||||
@ -264,6 +279,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const [isBotCommandMenuOpen, openBotCommandMenu, closeBotCommandMenu] = useFlag();
|
||||
const [isAttachMenuOpen, openAttachMenu, closeAttachMenu] = useFlag();
|
||||
const [isSymbolMenuOpen, openSymbolMenu, closeSymbolMenu] = useFlag();
|
||||
const [isSendAsMenuOpen, openSendAsMenu, closeSendAsMenu] = useFlag();
|
||||
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
||||
const [isSymbolMenuLoaded, onSymbolMenuLoadingComplete] = useFlag();
|
||||
const [isHoverDisabled, disableHover, enableHover] = useFlag();
|
||||
@ -566,8 +582,9 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleActivateSymbolMenu = useCallback(() => {
|
||||
closeBotCommandMenu();
|
||||
closeSendAsMenu();
|
||||
openSymbolMenu();
|
||||
}, [closeBotCommandMenu, openSymbolMenu]);
|
||||
}, [closeBotCommandMenu, closeSendAsMenu, openSymbolMenu]);
|
||||
|
||||
const handleStickerSelect = useCallback((sticker: ApiSticker, shouldPreserveInput = false) => {
|
||||
sticker = {
|
||||
@ -701,6 +718,22 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
|
||||
}, [openSymbolMenu, closeBotCommandMenu]);
|
||||
|
||||
const handleSendAsMenuOpen = useCallback(() => {
|
||||
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
||||
|
||||
if (!IS_SINGLE_COLUMN_LAYOUT || messageInput !== document.activeElement) {
|
||||
openSendAsMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
messageInput.blur();
|
||||
setTimeout(() => {
|
||||
closeBotCommandMenu();
|
||||
closeSymbolMenu();
|
||||
openSendAsMenu();
|
||||
}, MOBILE_KEYBOARD_HIDE_DELAY_MS);
|
||||
}, [closeBotCommandMenu, closeSymbolMenu, openSendAsMenu]);
|
||||
|
||||
const handleAllScheduledClick = useCallback(() => {
|
||||
openChat({ id: chatId, threadId, type: 'scheduled' });
|
||||
}, [openChat, chatId, threadId]);
|
||||
@ -834,6 +867,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
message={renderedEditedMessage}
|
||||
/>
|
||||
)}
|
||||
<SendAsMenu
|
||||
isOpen={isSendAsMenuOpen}
|
||||
onClose={closeSendAsMenu}
|
||||
chatId={chatId}
|
||||
selectedSendAsId={sendAsId}
|
||||
sendAsIds={sendAsIds}
|
||||
/>
|
||||
<MentionTooltip
|
||||
isOpen={isMentionTooltipOpen}
|
||||
onClose={closeMentionTooltip}
|
||||
@ -881,6 +921,21 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
<i className="icon-bot-commands-filled" />
|
||||
</ResponsiveHoverButton>
|
||||
)}
|
||||
{sendAsIds && (sendAsUser || sendAsChat) && (
|
||||
<Button
|
||||
round
|
||||
color="translucent"
|
||||
onClick={isSendAsMenuOpen ? closeSendAsMenu : handleSendAsMenuOpen}
|
||||
ariaLabel={lang('SendMessageAsTitle')}
|
||||
className="send-as-button"
|
||||
>
|
||||
<Avatar
|
||||
user={sendAsUser}
|
||||
chat={sendAsChat}
|
||||
size="tiny"
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{IS_SINGLE_COLUMN_LAYOUT ? (
|
||||
<Button
|
||||
className={symbolMenuButtonClassName}
|
||||
@ -1079,6 +1134,12 @@ export default memo(withGlobal<OwnProps>(
|
||||
const emojiKeywords = language !== BASE_EMOJI_KEYWORD_LANG ? global.emojiKeywords[language] : undefined;
|
||||
const botKeyboardMessageId = messageWithActualBotKeyboard ? messageWithActualBotKeyboard.id : undefined;
|
||||
const keyboardMessage = botKeyboardMessageId ? selectChatMessage(global, chatId, botKeyboardMessageId) : undefined;
|
||||
const usersById = global.users.byId;
|
||||
const chatsById = global.chats.byId;
|
||||
const { currentUserId } = global;
|
||||
const sendAsId = chat?.fullInfo ? chat?.fullInfo?.sendAsId || currentUserId : undefined;
|
||||
const sendAsUser = sendAsId ? usersById?.[sendAsId] : undefined;
|
||||
const sendAsChat = !sendAsUser && sendAsId ? chatsById?.[sendAsId] : undefined;
|
||||
|
||||
return {
|
||||
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
|
||||
@ -1106,8 +1167,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
stickersForEmoji: global.stickers.forEmoji.stickers,
|
||||
groupChatMembers: chat?.fullInfo?.members,
|
||||
topInlineBotIds: global.topInlineBots?.userIds,
|
||||
currentUserId: global.currentUserId,
|
||||
usersById: global.users.byId,
|
||||
currentUserId,
|
||||
usersById,
|
||||
lastSyncTime: global.lastSyncTime,
|
||||
contentToBeScheduled: global.messages.contentToBeScheduled,
|
||||
shouldSuggestStickers,
|
||||
@ -1119,6 +1180,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
isInlineBotLoading: global.inlineBots.isLoading,
|
||||
chatBotCommands: chat && chat.fullInfo && chat.fullInfo.botCommands,
|
||||
botCommands: chatBot && chatBot.fullInfo ? (chatBot.fullInfo.botCommands || false) : undefined,
|
||||
sendAsUser,
|
||||
sendAsChat,
|
||||
sendAsId,
|
||||
};
|
||||
},
|
||||
)(Composer));
|
||||
|
||||
15
src/components/middle/composer/SendAsMenu.async.tsx
Normal file
15
src/components/middle/composer/SendAsMenu.async.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import React, { FC, memo } from '../../../lib/teact/teact';
|
||||
import { OwnProps } from './SendAsMenu';
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const SendAsMenuAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const SendAsMenu = useModuleLoader(Bundles.Extra, 'SendAsMenu', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return SendAsMenu ? <SendAsMenu {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default memo(SendAsMenuAsync);
|
||||
74
src/components/middle/composer/SendAsMenu.scss
Normal file
74
src/components/middle/composer/SendAsMenu.scss
Normal file
@ -0,0 +1,74 @@
|
||||
.SendAsMenu {
|
||||
.send-as-title {
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
word-break: break-word;
|
||||
margin-inline-start: 1rem;
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
width: calc(100% - 4rem);
|
||||
max-width: 20rem;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
flex-direction: column;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
width: calc(100% - 3rem);
|
||||
}
|
||||
}
|
||||
|
||||
.is-pointer-env & {
|
||||
> .backdrop {
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
left: 0;
|
||||
right: auto;
|
||||
width: 3.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SendAsItem {
|
||||
margin: 0 !important;
|
||||
$border-size: 2px;
|
||||
|
||||
.Avatar.selected {
|
||||
margin-right: 0.75rem;
|
||||
position: relative;
|
||||
width: calc(2.125rem - #{$border-size * 2});
|
||||
height: calc(2.125rem - #{$border-size * 2});
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: #{-$border-size * 2};
|
||||
left: #{-$border-size * 2};
|
||||
border: $border-size solid var(--color-primary);
|
||||
width: calc(100% + #{$border-size * 4});
|
||||
height: calc(100% + #{$border-size * 4});
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.ListItem-button {
|
||||
padding: 0.5625rem 1rem !important;
|
||||
border-radius: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.subtitle {
|
||||
font-size: .9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.3125;
|
||||
}
|
||||
}
|
||||
125
src/components/middle/composer/SendAsMenu.tsx
Normal file
125
src/components/middle/composer/SendAsMenu.tsx
Normal file
@ -0,0 +1,125 @@
|
||||
import React, {
|
||||
FC, useCallback, useEffect, useRef, memo,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
|
||||
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
|
||||
import { IS_TOUCH_ENV } from '../../../util/environment';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { getUserFullName, isUserId } from '../../../modules/helpers';
|
||||
import useMouseInside from '../../../hooks/useMouseInside';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getDispatch, getGlobal } from '../../../lib/teact/teactn';
|
||||
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Menu from '../../ui/Menu';
|
||||
|
||||
import './SendAsMenu.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
chatId?: string;
|
||||
selectedSendAsId?: string;
|
||||
sendAsIds?: string[];
|
||||
};
|
||||
|
||||
const SendAsMenu: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
chatId,
|
||||
selectedSendAsId,
|
||||
sendAsIds,
|
||||
}) => {
|
||||
const { saveDefaultSendAs } = getDispatch();
|
||||
|
||||
// No need for expensive global updates on users and chats, so we avoid them
|
||||
const usersById = getGlobal().users.byId;
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [handleMouseEnter, handleMouseLeave, markMouseInside] = useMouseInside(isOpen, onClose, undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
markMouseInside();
|
||||
}
|
||||
}, [isOpen, markMouseInside]);
|
||||
|
||||
const handleUserSelect = useCallback((id: string) => {
|
||||
onClose();
|
||||
saveDefaultSendAs({ chatId, sendAsId: id });
|
||||
}, [chatId, onClose, saveDefaultSendAs]);
|
||||
|
||||
const selectedSendAsIndex = useKeyboardNavigation({
|
||||
isActive: isOpen,
|
||||
items: sendAsIds,
|
||||
onSelect: handleUserSelect,
|
||||
shouldSelectOnTab: true,
|
||||
shouldSaveSelectionOnUpdateItems: true,
|
||||
onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setTooltipItemVisible('.chat-item-clickable', selectedSendAsIndex, containerRef);
|
||||
}, [selectedSendAsIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (sendAsIds && !sendAsIds.length) {
|
||||
onClose();
|
||||
}
|
||||
}, [sendAsIds, onClose]);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isOpen}
|
||||
positionX="left"
|
||||
positionY="bottom"
|
||||
onClose={onClose}
|
||||
className="SendAsMenu"
|
||||
onCloseAnimationEnd={onClose}
|
||||
onMouseEnter={!IS_TOUCH_ENV ? handleMouseEnter : undefined}
|
||||
onMouseLeave={!IS_TOUCH_ENV ? handleMouseLeave : undefined}
|
||||
noCloseOnBackdrop={!IS_TOUCH_ENV}
|
||||
>
|
||||
<div className="send-as-title" dir="auto">{lang('SendMessageAsTitle')}</div>
|
||||
{usersById && chatsById && sendAsIds?.map((id, index) => {
|
||||
const user = isUserId(id) ? usersById[id] : undefined;
|
||||
const chat = !user ? chatsById[id] : undefined;
|
||||
const fullName = user ? getUserFullName(user) : chat?.title;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={id}
|
||||
className="SendAsItem chat-item-clickable scroll-item with-avatar"
|
||||
onClick={() => handleUserSelect(id)}
|
||||
focus={selectedSendAsIndex === index}
|
||||
>
|
||||
<Avatar
|
||||
size="small"
|
||||
user={user}
|
||||
chat={chat}
|
||||
className={buildClassName(selectedSendAsId === id && 'selected')}
|
||||
/>
|
||||
<div className="info">
|
||||
<div className="title">
|
||||
<h3 dir="auto">{fullName && renderText(fullName)}</h3>
|
||||
</div>
|
||||
<span className="subtitle">{user
|
||||
? lang('VoipGroupPersonalAccount')
|
||||
: lang('Subscribers', chat?.membersCount, 'i')}
|
||||
</span>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SendAsMenu);
|
||||
@ -98,7 +98,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.Button.bot-commands ~ & {
|
||||
.Button.bot-commands ~ &, .Button.send-as-button ~ & {
|
||||
.is-pointer-env & > .backdrop {
|
||||
left: 3rem;
|
||||
width: 3.25rem;
|
||||
|
||||
@ -505,7 +505,7 @@ export type ActionTypes = (
|
||||
'setReplyingToId' | 'setEditingId' | 'editLastMessage' | 'saveDraft' | 'clearDraft' | 'loadPinnedMessages' |
|
||||
'toggleMessageWebPage' | 'replyToNextMessage' | 'deleteChatUser' | 'deleteChat' |
|
||||
'reportMessages' | 'sendMessageAction' | 'focusNextReply' | 'openChatByInvite' | 'loadSeenBy' |
|
||||
'loadSponsoredMessages' | 'viewSponsoredMessage' |
|
||||
'loadSponsoredMessages' | 'viewSponsoredMessage' | 'loadSendAs' | 'saveDefaultSendAs' |
|
||||
// downloads
|
||||
'downloadSelectedMessages' | 'downloadMessageMedia' | 'cancelMessageMediaDownload' |
|
||||
// scheduled messages
|
||||
|
||||
@ -10,6 +10,10 @@ export default function useMouseInside(
|
||||
) {
|
||||
const isMouseInside = useRef(false);
|
||||
|
||||
const markMouseInside = useCallback(() => {
|
||||
isMouseInside.current = true;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (closeTimeout) {
|
||||
clearTimeout(closeTimeout);
|
||||
@ -44,5 +48,5 @@ export default function useMouseInside(
|
||||
}, menuCloseTimeout);
|
||||
}, [menuCloseTimeout, onClose]);
|
||||
|
||||
return [handleMouseEnter, handleMouseLeave];
|
||||
return [handleMouseEnter, handleMouseLeave, markMouseInside];
|
||||
}
|
||||
|
||||
@ -1080,6 +1080,7 @@ messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Boo
|
||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
@ -1117,6 +1118,7 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
|
||||
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
||||
channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers;
|
||||
channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool;
|
||||
channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages;
|
||||
payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm;
|
||||
|
||||
@ -1081,6 +1081,7 @@ messages.readDiscussion#f731a9f4 peer:InputPeer msg_id:int read_max_id:int = Boo
|
||||
messages.unpinAllMessages#f025bc8b peer:InputPeer = messages.AffectedHistory;
|
||||
messages.deleteChat#5bd0ee50 chat_id:long = Bool;
|
||||
messages.getMessageReadParticipants#2c6f97b7 peer:InputPeer msg_id:int = Vector<long>;
|
||||
messages.saveDefaultSendAs#ccfddf96 peer:InputPeer send_as:InputPeer = Bool;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
@ -1118,6 +1119,7 @@ channels.readMessageContents#eab5dc38 channel:InputChannel id:Vector<int> = Bool
|
||||
channels.togglePreHistoryHidden#eabbb94c channel:InputChannel enabled:Bool = Updates;
|
||||
channels.getGroupsForDiscussion#f5dad378 = messages.Chats;
|
||||
channels.setDiscussionGroup#40582bb2 broadcast:InputChannel group:InputChannel = Bool;
|
||||
channels.getSendAs#dc770ee peer:InputPeer = channels.SendAsPeers;
|
||||
channels.viewSponsoredMessage#beaedb94 channel:InputChannel random_id:bytes = Bool;
|
||||
channels.getSponsoredMessages#ec210fbf channel:InputChannel = messages.SponsoredMessages;
|
||||
payments.getPaymentForm#8a333c8d flags:# peer:InputPeer msg_id:int theme_params:flags.0?DataJSON = payments.PaymentForm;
|
||||
|
||||
@ -2,7 +2,7 @@ import {
|
||||
addReducer, getDispatch, getGlobal, setGlobal,
|
||||
} from '../../../lib/teact/teactn';
|
||||
|
||||
import { ApiChat } from '../../../api/types';
|
||||
import { ApiChat, ApiUser } from '../../../api/types';
|
||||
import { InlineBotSettings } from '../../../types';
|
||||
|
||||
import {
|
||||
@ -11,7 +11,7 @@ import {
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
selectChat, selectChatBot, selectChatMessage, selectCurrentChat, selectCurrentMessageList,
|
||||
selectReplyingToId, selectUser,
|
||||
selectReplyingToId, selectSendAs, selectUser,
|
||||
} from '../../selectors';
|
||||
import { addChats, addUsers, removeBlockedContact } from '../../reducers';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
@ -81,7 +81,9 @@ addReducer('sendBotCommand', (global, actions, payload) => {
|
||||
actions.setReplyingToId({ messageId: undefined });
|
||||
actions.clearWebPagePreview({ chatId: chat.id, threadId, value: false });
|
||||
|
||||
void sendBotCommand(chat, currentUserId, command, selectReplyingToId(global, chat.id, threadId));
|
||||
void sendBotCommand(
|
||||
chat, currentUserId, command, selectReplyingToId(global, chat.id, threadId), selectSendAs(global, chatId),
|
||||
);
|
||||
});
|
||||
|
||||
addReducer('restartBot', (global, actions, payload) => {
|
||||
@ -100,7 +102,7 @@ addReducer('restartBot', (global, actions, payload) => {
|
||||
}
|
||||
|
||||
setGlobal(removeBlockedContact(getGlobal(), bot.id));
|
||||
void sendBotCommand(chat, currentUserId, '/start');
|
||||
void sendBotCommand(chat, currentUserId, '/start', undefined, selectSendAs(global, chatId));
|
||||
})();
|
||||
});
|
||||
|
||||
@ -204,6 +206,7 @@ addReducer('sendInlineBotResult', (global, actions, payload) => {
|
||||
resultId: id,
|
||||
queryId,
|
||||
replyingTo: selectReplyingToId(global, chatId, threadId),
|
||||
sendAs: selectSendAs(global, chatId),
|
||||
});
|
||||
});
|
||||
|
||||
@ -305,11 +308,14 @@ async function searchInlineBot({
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
async function sendBotCommand(chat: ApiChat, currentUserId: string, command: string, replyingTo?: number) {
|
||||
async function sendBotCommand(
|
||||
chat: ApiChat, currentUserId: string, command: string, replyingTo?: number, sendAs?: ApiChat | ApiUser,
|
||||
) {
|
||||
await callApi('sendMessage', {
|
||||
chat,
|
||||
text: command,
|
||||
replyingTo,
|
||||
sendAs,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import {
|
||||
ApiNewPoll,
|
||||
ApiOnProgress,
|
||||
ApiSticker,
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
MAIN_THREAD_ID,
|
||||
MESSAGE_DELETED,
|
||||
@ -56,6 +57,8 @@ import {
|
||||
selectScheduledMessage,
|
||||
selectNoWebPage,
|
||||
selectFirstUnreadId,
|
||||
selectUser,
|
||||
selectSendAs,
|
||||
selectSponsoredMessage,
|
||||
} from '../../selectors';
|
||||
import { debounce, rafPromise } from '../../../util/schedulers';
|
||||
@ -204,6 +207,7 @@ addReducer('sendMessage', (global, actions, payload) => {
|
||||
chat,
|
||||
replyingTo: selectReplyingToId(global, chatId, threadId),
|
||||
noWebPage: selectNoWebPage(global, chatId, threadId),
|
||||
sendAs: selectSendAs(global, chatId),
|
||||
};
|
||||
|
||||
const isSingle = !payload.attachments || payload.attachments.length <= 1;
|
||||
@ -586,6 +590,7 @@ addReducer('forwardMessages', (global, action, payload) => {
|
||||
}
|
||||
|
||||
const { isSilent, scheduledAt } = payload;
|
||||
const sendAs = selectSendAs(global, toChatId!);
|
||||
|
||||
const realMessages = messages.filter((m) => !isServiceNotificationMessage(m));
|
||||
if (realMessages.length) {
|
||||
@ -596,6 +601,7 @@ addReducer('forwardMessages', (global, action, payload) => {
|
||||
serverTimeOffset: getGlobal().serverTimeOffset,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
});
|
||||
}
|
||||
|
||||
@ -613,6 +619,7 @@ addReducer('forwardMessages', (global, action, payload) => {
|
||||
poll,
|
||||
isSilent,
|
||||
scheduledAt,
|
||||
sendAs,
|
||||
});
|
||||
});
|
||||
|
||||
@ -853,6 +860,7 @@ async function sendMessage(params: {
|
||||
serverTimeOffset?: number;
|
||||
isSilent?: boolean;
|
||||
scheduledAt?: number;
|
||||
sendAs?: ApiChat | ApiUser;
|
||||
}) {
|
||||
let localId: number | undefined;
|
||||
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
|
||||
@ -967,6 +975,47 @@ addReducer('loadSeenBy', (global, actions, payload) => {
|
||||
})();
|
||||
});
|
||||
|
||||
addReducer('saveDefaultSendAs', (global, actions, payload) => {
|
||||
const { chatId, sendAsId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
const sendAsChat = selectChat(global, sendAsId) || selectUser(global, sendAsId);
|
||||
if (!chat || !sendAsChat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
void callApi('saveDefaultSendAs', { sendAs: sendAsChat, chat });
|
||||
|
||||
return updateChat(global, chatId, {
|
||||
fullInfo: {
|
||||
...chat.fullInfo,
|
||||
sendAsId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
addReducer('loadSendAs', (global, actions, payload) => {
|
||||
const { chatId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const result = await callApi('fetchSendAs', { chat });
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = updateChat(global, chatId, {
|
||||
sendAsIds: result.ids,
|
||||
});
|
||||
setGlobal(global);
|
||||
})();
|
||||
});
|
||||
|
||||
async function loadPinnedMessages(chat: ApiChat) {
|
||||
const result = await callApi('fetchPinnedMessages', { chat });
|
||||
if (!result) {
|
||||
|
||||
@ -185,3 +185,13 @@ export function selectCountNotMutedUnread(global: GlobalState) {
|
||||
export function selectIsServiceChatReady(global: GlobalState) {
|
||||
return Boolean(selectChat(global, SERVICE_NOTIFICATIONS_USER_ID));
|
||||
}
|
||||
|
||||
export function selectSendAs(global: GlobalState, chatId: string) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
const id = chat?.fullInfo?.sendAsId;
|
||||
if (!id) return undefined;
|
||||
|
||||
return selectUser(global, id) || selectChat(global, id);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user