Chat: Fix incorrect draft display (#6192)

This commit is contained in:
zubiden 2025-09-09 20:26:17 +02:00 committed by Alexander Zinchuk
parent 77449ad407
commit b2d58b6cfd
9 changed files with 45 additions and 39 deletions

View File

@ -26,7 +26,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
- Use [buildClassName.ts](mdc:src/util/buildClassName.ts) to merge multiple class names. - Use [buildClassName.ts](mdc:src/util/buildClassName.ts) to merge multiple class names.
- **Always extract styles to files** - avoid inline styles unless absolutely necessary. - **Always extract styles to files** - avoid inline styles unless absolutely necessary.
- **If file already imports styles**, check where they come from and add new styles there - don't create new style files. - **If file already imports styles**, check where they come from and add new styles there - don't create new style files.
- Use rem units for all measurements. - Prefer rem units for all measurements. Exceptions are possible, but usually rare.
- **Code Style:** - **Code Style:**
- Early returns. - Early returns.
@ -34,19 +34,20 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
- Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`). - Functions should start with a verb (e.q. `openModal`, `closeDialog`, `handleClick`).
- Prefer checking required parameter before calling a function, avoid making it optinal and checking at the beginning of the function. - Prefer checking required parameter before calling a function, avoid making it optinal and checking at the beginning of the function.
- Only leave comments for complex logic. - Only leave comments for complex logic.
- Do not use `null`. There's linter rule to enforce it.
- **IMPORTANT: Avoid conditional spread operators** - TypeScript doesn't check if spread fields match the target type. - **IMPORTANT: Avoid conditional spread operators** - TypeScript doesn't check if spread fields match the target type.
```typescript ```typescript
// ❌ BAD - No type checking // ❌ BAD - No type checking
{ ...condition && { field: value } } { ...condition && { field: value } }
// ✅ GOOD - Full type checking // ✅ GOOD - Full type checking
{ field: condition ? value : undefined } { field: condition ? value : undefined }
``` ```
- **IMPORTANT: Use string templates for inline styles** - Always use template literals for style prop: - **IMPORTANT: Use string templates for inline styles** - Always use template literals for style prop. Teact does not support object:
```typescript ```typescript
// ✅ CORRECT // ✅ CORRECT
style={`transform: translateX(${value}%)`} style={`transform: translateX(${value}%)`}
// ❌ WRONG // ❌ WRONG
style={{ transform: `translateX(${value}%)` }} style={{ transform: `translateX(${value}%)` }}
style={{ '--custom-prop': value } as React.CSSProperties} style={{ '--custom-prop': value } as React.CSSProperties}
@ -56,7 +57,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
// ✅ CORRECT // ✅ CORRECT
font-weight: var(--font-weight-medium); font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-bold); font-weight: var(--font-weight-bold);
// ❌ WRONG // ❌ WRONG
font-weight: 600; font-weight: 600;
font-weight: bold; font-weight: bold;
@ -65,7 +66,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
- **Localization & Text Rules:** - **Localization & Text Rules:**
- **ALWAYS** use `lang()` for all text content - never hardcode strings. - **ALWAYS** use `lang()` for all text content - never hardcode strings.
- `lang()` can accept parameters: `lang('Key', { param: value })`. - `lang()` can accept parameters: `lang('Key', { param: value })`.
- Add new translations to end of `src/assets/localization/fallback.strings`. - Add new translations to `src/assets/localization/fallback.strings`.
- **After your solution:** - **After your solution:**
1. Critique it—identify any shortcomings. 1. Critique it—identify any shortcomings.
@ -158,8 +159,8 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
### 1. Basics & Imports ### 1. Basics & Imports
* All components use JSX and render with Teact. * All components use JSX and render with Teact.
* **Always** import React from teact library, for JSX compatibility reasons. Only import from `'react'` when you need React **types** that are not provided in Teact. * Only import from `'react'` when you need React **types** that are not provided in Teact.
* Built-in hooks live in `src/lib/teact/teact`. Import them from there. * Built-in hooks live in Teact library. Import them from there.
### 2. Props & Types ### 2. Props & Types
@ -225,12 +226,12 @@ const MAX_ITEMS = 10
const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => { const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => {
const { someAction } = getActions(); // Should always be first, if actions are used const { someAction } = getActions(); // Should always be first, if actions are used
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>();
const [color, setColor] = useState('#FF00FF'); const [color, setColor] = useState('#FF00FF');
const [isOpen, open, close] = useFlag(); const [isOpen, open, close] = useFlag();
const lang = useLang(); const lang = useLang(); // Somewhere near the top, after state definition
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (!ref.current) return; if (!ref.current) return;
@ -415,4 +416,4 @@ lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true });
* Flags: `lang.isRtl`, `lang.code`, `lang.rawCode` * Flags: `lang.isRtl`, `lang.code`, `lang.rawCode`
**7. Beyond React** **7. Beyond React**
Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax. Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax.

View File

@ -321,6 +321,7 @@
"ArchivedChats" = "Archived Chats"; "ArchivedChats" = "Archived Chats";
"FilterAddTo" = "Add to folder"; "FilterAddTo" = "Add to folder";
"Draft" = "Draft"; "Draft" = "Draft";
"ChatDraftPrefix" = "Draft:";
"FilterAllChatsShort" = "All"; "FilterAllChatsShort" = "All";
"FilterAllChats" = "All Chats"; "FilterAllChats" = "All Chats";
"CreateNewContact" = "Create New Contact"; "CreateNewContact" = "Create New Contact";
@ -2024,6 +2025,7 @@
"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available"; "NotificationMessageNotSupportedInFrozenAccount" = "This action is not available";
"NotificationGiftIsSale" = "{gift} is now for sale!"; "NotificationGiftIsSale" = "{gift} is now for sale!";
"NotificationGiftIsUnlist" = "{gift} is removed from sale."; "NotificationGiftIsUnlist" = "{gift} is removed from sale.";
"NotificationMessageTextHidden" = "New message";
"GiftRibbonSale" = "sale"; "GiftRibbonSale" = "sale";
"ButtonBuyGift" = "Buy for {stars}"; "ButtonBuyGift" = "Buy for {stars}";
"GiftInfoBuyGift" = "{user} is selling this gift and you can buy it."; "GiftInfoBuyGift" = "{user} is selling this gift and you can buy it.";

View File

@ -260,10 +260,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.draft { .draft {
&::after { margin-inline-end: 0.5ch;
content: ": ";
}
} }
.colon, .chat-prefix-icon { .colon, .chat-prefix-icon {
@ -271,9 +270,8 @@
} }
.chat-prefix-icon { .chat-prefix-icon {
transform: translateY(1px);
display: inline-block; display: inline-block;
font-size: 0.875rem; align-self: center;
color: var(--color-list-icon); color: var(--color-list-icon);
} }

View File

@ -25,8 +25,8 @@ import { ChatAnimationTypes } from './useChatAnimationType';
import useMessageMediaHash from '../../../../hooks/media/useMessageMediaHash'; import useMessageMediaHash from '../../../../hooks/media/useMessageMediaHash';
import useThumbnail from '../../../../hooks/media/useThumbnail'; import useThumbnail from '../../../../hooks/media/useThumbnail';
import useEnsureStory from '../../../../hooks/useEnsureStory'; import useEnsureStory from '../../../../hooks/useEnsureStory';
import useLang from '../../../../hooks/useLang';
import useMedia from '../../../../hooks/useMedia'; import useMedia from '../../../../hooks/useMedia';
import useOldLang from '../../../../hooks/useOldLang';
import ChatForumLastMessage from '../../../common/ChatForumLastMessage'; import ChatForumLastMessage from '../../../common/ChatForumLastMessage';
import Icon from '../../../common/icons/Icon'; import Icon from '../../../common/icons/Icon';
@ -73,7 +73,7 @@ export default function useChatListEntry({
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
noForumTitle?: boolean; noForumTitle?: boolean;
}) { }) {
const oldLang = useOldLang(); const lang = useLang();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
const storyData = lastMessage?.content.storyData; const storyData = lastMessage?.content.storyData;
@ -105,8 +105,8 @@ export default function useChatListEntry({
if (canDisplayDraft) { if (canDisplayDraft) {
return ( return (
<p className="last-message" dir={oldLang.isRtl ? 'auto' : 'ltr'}> <p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
<span className="draft">{oldLang('Draft')}</span> <span className="draft">{lang('ChatDraftPrefix')}</span>
<span className="last-message-summary" dir="auto"> <span className="last-message-summary" dir="auto">
{renderTextWithEntities({ {renderTextWithEntities({
text: draft.text?.text || '', text: draft.text?.text || '',
@ -124,11 +124,11 @@ export default function useChatListEntry({
} }
const senderName = lastMessageSender const senderName = lastMessageSender
? getMessageSenderName(oldLang, chatId, lastMessageSender) ? getMessageSenderName(lang, chatId, lastMessageSender)
: undefined; : undefined;
return ( return (
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}> <p className="last-message shared-canvas-container" dir={lang.isRtl ? 'auto' : 'ltr'}>
{senderName && ( {senderName && (
<> <>
<span className="sender-name">{renderText(senderName)}</span> <span className="sender-name">{renderText(senderName)}</span>
@ -143,7 +143,7 @@ export default function useChatListEntry({
</p> </p>
); );
}, [ }, [
chat, chatId, draft, isRoundVideo, isTopic, oldLang, lastMessage, lastMessageSender, lastMessageTopic, chat, chatId, draft, isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic,
mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview, mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview,
]); ]);

View File

@ -6,8 +6,8 @@ import { getMessageSenderName } from '../../../global/helpers/peers';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import FullNameTitle from '../../common/FullNameTitle'; import FullNameTitle from '../../common/FullNameTitle';
@ -39,7 +39,7 @@ const MiddleSearchResult = ({
className, className,
onClick, onClick,
}: OwnProps) => { }: OwnProps) => {
const lang = useOldLang(); const lang = useLang();
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName; const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
const peer = shouldShowChat ? messageChat : senderPeer; const peer = shouldShowChat ? messageChat : senderPeer;

View File

@ -3183,19 +3183,21 @@ async function loadChats(
); );
} }
const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById); if (listType === 'active' || listType === 'archived') {
idsToUpdateDraft.forEach((chatId) => { const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById);
const draft = result.draftsById[chatId]; idsToUpdateDraft.forEach((chatId) => {
const thread = selectThread(global, chatId, MAIN_THREAD_ID); const draft = result.draftsById[chatId];
const thread = selectThread(global, chatId, MAIN_THREAD_ID);
if (!draft && !thread) return; if (!draft && !thread) return;
if (!selectDraft(global, chatId, MAIN_THREAD_ID)?.isLocal) { if (!selectDraft(global, chatId, MAIN_THREAD_ID)?.isLocal) {
global = replaceThreadParam( global = replaceThreadParam(
global, chatId, MAIN_THREAD_ID, 'draft', draft, global, chatId, MAIN_THREAD_ID, 'draft', draft,
); );
} }
}); });
}
if ((chatIds.length === 0 || chatIds.length === result.totalChatCount) && !global.chats.isFullyLoaded[listType]) { if ((chatIds.length === 0 || chatIds.length === result.totalChatCount) && !global.chats.isFullyLoaded[listType]) {
global = { global = {

View File

@ -111,7 +111,7 @@ export function getPeerFullTitle(lang: OldLangFn | LangFn, peer: ApiPeer | Custo
return isApiPeerUser(peer) ? getUserFullName(peer) : getChatTitle(lang, peer); return isApiPeerUser(peer) ? getUserFullName(peer) : getChatTitle(lang, peer);
} }
export function getMessageSenderName(lang: OldLangFn, chatId: string, sender: ApiPeer) { export function getMessageSenderName(lang: LangFn, chatId: string, sender: ApiPeer) {
// Hide sender name for private chats // Hide sender name for private chats
if (isUserId(chatId)) return undefined; if (isUserId(chatId)) return undefined;

View File

@ -279,6 +279,7 @@ export interface LangPair {
'ArchivedChats': undefined; 'ArchivedChats': undefined;
'FilterAddTo': undefined; 'FilterAddTo': undefined;
'Draft': undefined; 'Draft': undefined;
'ChatDraftPrefix': undefined;
'FilterAllChatsShort': undefined; 'FilterAllChatsShort': undefined;
'FilterAllChats': undefined; 'FilterAllChats': undefined;
'CreateNewContact': undefined; 'CreateNewContact': undefined;
@ -1545,6 +1546,7 @@ export interface LangPair {
'ActionPaidMessagePriceFreeYou': undefined; 'ActionPaidMessagePriceFreeYou': undefined;
'NotificationTitleNotSupportedInFrozenAccount': undefined; 'NotificationTitleNotSupportedInFrozenAccount': undefined;
'NotificationMessageNotSupportedInFrozenAccount': undefined; 'NotificationMessageNotSupportedInFrozenAccount': undefined;
'NotificationMessageTextHidden': undefined;
'GiftRibbonSale': undefined; 'GiftRibbonSale': undefined;
'StarsGiftBought': undefined; 'StarsGiftBought': undefined;
'GiftSellTitle': undefined; 'GiftSellTitle': undefined;

View File

@ -31,6 +31,7 @@ import { callApi } from '../api/gramjs';
import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './browser/windowEnvironment'; import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './browser/windowEnvironment';
import jsxToHtml from './element/jsxToHtml'; import jsxToHtml from './element/jsxToHtml';
import { buildCollectionByKey } from './iteratees'; import { buildCollectionByKey } from './iteratees';
import { getTranslationFn } from './localization';
import * as mediaLoader from './mediaLoader'; import * as mediaLoader from './mediaLoader';
import { oldTranslate } from './oldLangProvider'; import { oldTranslate } from './oldLangProvider';
import { debounce } from './schedulers'; import { debounce } from './schedulers';
@ -313,7 +314,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
!isScreenLocked !isScreenLocked
&& getShouldShowMessagePreview(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id)) && getShouldShowMessagePreview(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id))
) { ) {
const senderName = sender ? getMessageSenderName(oldTranslate, chat.id, sender) : undefined; const senderName = sender ? getMessageSenderName(getTranslationFn(), chat.id, sender) : undefined;
let summary = jsxToHtml(<span><MessageSummary message={message} /></span>)[0].textContent || ''; let summary = jsxToHtml(<span><MessageSummary message={message} /></span>)[0].textContent || '';
if (hasReaction) { if (hasReaction) {
@ -323,7 +324,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
body = senderName ? `${senderName}: ${summary}` : summary; body = senderName ? `${senderName}: ${summary}` : summary;
} else { } else {
body = 'New message'; body = getTranslationFn()('NotificationMessageTextHidden');
} }
let title = isScreenLocked ? APP_NAME : getChatTitle(oldTranslate, chat, isSelf); let title = isScreenLocked ? APP_NAME : getChatTitle(oldTranslate, chat, isSelf);