diff --git a/CLAUDE.md b/CLAUDE.md index 68ab89c9a..a8e99497d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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(null); + const ref = useRef(); 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. \ No newline at end of file +Use `getTranslationFn()` to grab the same `lang` function in non-component code. Discouraged, use object syntax. diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 0bf059bf3..e065f5aeb 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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."; diff --git a/src/components/left/main/Chat.scss b/src/components/left/main/Chat.scss index afa5d5cbf..65d6f5d4b 100644 --- a/src/components/left/main/Chat.scss +++ b/src/components/left/main/Chat.scss @@ -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); } diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 82f4479c6..fc0a5d0a4 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -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(); const storyData = lastMessage?.content.storyData; @@ -105,8 +105,8 @@ export default function useChatListEntry({ if (canDisplayDraft) { return ( -

- {oldLang('Draft')} +

+ {lang('ChatDraftPrefix')} {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 ( -

+

{senderName && ( <> {renderText(senderName)} @@ -143,7 +143,7 @@ export default function useChatListEntry({

); }, [ - chat, chatId, draft, isRoundVideo, isTopic, oldLang, lastMessage, lastMessageSender, lastMessageTopic, + chat, chatId, draft, isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview, ]); diff --git a/src/components/middle/search/MiddleSearchResult.tsx b/src/components/middle/search/MiddleSearchResult.tsx index 05f0db168..3b9fbba11 100644 --- a/src/components/middle/search/MiddleSearchResult.tsx +++ b/src/components/middle/search/MiddleSearchResult.tsx @@ -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; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index fe390012d..abc423c1a 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -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 = { diff --git a/src/global/helpers/peers.ts b/src/global/helpers/peers.ts index d403a585f..c43af4227 100644 --- a/src/global/helpers/peers.ts +++ b/src/global/helpers/peers.ts @@ -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; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 6c16b32b0..201abb4ad 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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; diff --git a/src/util/notifications.tsx b/src/util/notifications.tsx index 2425cd713..13f992d02 100644 --- a/src/util/notifications.tsx +++ b/src/util/notifications.tsx @@ -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()[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);