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.
- **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.
- Use rem units for all measurements.
- Prefer rem units for all measurements. Exceptions are possible, but usually rare.
- **Code Style:**
- 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`).
- 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.
- 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.
```typescript
// ❌ BAD - No type checking
{ ...condition && { field: value } }
// ✅ GOOD - Full type checking
{ 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
// ✅ CORRECT
style={`transform: translateX(${value}%)`}
// ❌ WRONG
style={{ transform: `translateX(${value}%)` }}
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
font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-bold);
// ❌ WRONG
font-weight: 600;
font-weight: bold;
@ -65,7 +66,7 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
- **Localization & Text Rules:**
- **ALWAYS** use `lang()` for all text content - never hardcode strings.
- `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:**
1. Critique it—identify any shortcomings.
@ -158,8 +159,8 @@ addActionHandler('loadUser', async (global, actions, { userId }) => {
### 1. Basics & Imports
* 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.
* Built-in hooks live in `src/lib/teact/teact`. Import them from there.
* Only import from `'react'` when you need React **types** that are not provided in Teact.
* Built-in hooks live in Teact library. Import them from there.
### 2. Props & Types
@ -225,12 +226,12 @@ const MAX_ITEMS = 10
const Component = ({ id, className, stateValue, onClick }: OwnProps & StateProps) => {
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 [isOpen, open, close] = useFlag();
const lang = useLang();
const lang = useLang(); // Somewhere near the top, after state definition
const handleClick = useLastCallback(() => {
if (!ref.current) return;
@ -415,4 +416,4 @@ lang('MarkdownKey', undefined, { withNodes: true, withMarkdown: true });
* Flags: `lang.isRtl`, `lang.code`, `lang.rawCode`
**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";
"FilterAddTo" = "Add to folder";
"Draft" = "Draft";
"ChatDraftPrefix" = "Draft:";
"FilterAllChatsShort" = "All";
"FilterAllChats" = "All Chats";
"CreateNewContact" = "Create New Contact";
@ -2024,6 +2025,7 @@
"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available";
"NotificationGiftIsSale" = "{gift} is now for sale!";
"NotificationGiftIsUnlist" = "{gift} is removed from sale.";
"NotificationMessageTextHidden" = "New message";
"GiftRibbonSale" = "sale";
"ButtonBuyGift" = "Buy for {stars}";
"GiftInfoBuyGift" = "{user} is selling this gift and you can buy it.";

View File

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

View File

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

View File

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

View File

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

View File

@ -111,7 +111,7 @@ export function getPeerFullTitle(lang: OldLangFn | LangFn, peer: ApiPeer | Custo
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
if (isUserId(chatId)) return undefined;

View File

@ -279,6 +279,7 @@ export interface LangPair {
'ArchivedChats': undefined;
'FilterAddTo': undefined;
'Draft': undefined;
'ChatDraftPrefix': undefined;
'FilterAllChatsShort': undefined;
'FilterAllChats': undefined;
'CreateNewContact': undefined;
@ -1545,6 +1546,7 @@ export interface LangPair {
'ActionPaidMessagePriceFreeYou': undefined;
'NotificationTitleNotSupportedInFrozenAccount': undefined;
'NotificationMessageNotSupportedInFrozenAccount': undefined;
'NotificationMessageTextHidden': undefined;
'GiftRibbonSale': undefined;
'StarsGiftBought': 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 jsxToHtml from './element/jsxToHtml';
import { buildCollectionByKey } from './iteratees';
import { getTranslationFn } from './localization';
import * as mediaLoader from './mediaLoader';
import { oldTranslate } from './oldLangProvider';
import { debounce } from './schedulers';
@ -313,7 +314,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
!isScreenLocked
&& 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 || '';
if (hasReaction) {
@ -323,7 +324,7 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A
body = senderName ? `${senderName}: ${summary}` : summary;
} else {
body = 'New message';
body = getTranslationFn()('NotificationMessageTextHidden');
}
let title = isScreenLocked ? APP_NAME : getChatTitle(oldTranslate, chat, isSelf);