From bd3afbca75e7fdb5675ecfa6581d71e73c9be771 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 19 Apr 2024 13:37:34 +0400 Subject: [PATCH] Support displaying business profiles (#4407) --- src/api/gramjs/apiBuilders/messages.ts | 12 +- src/api/gramjs/apiBuilders/misc.ts | 11 +- src/api/gramjs/apiBuilders/users.ts | 41 +- src/api/gramjs/methods/chats.ts | 5 +- src/api/gramjs/methods/index.ts | 14 +- src/api/gramjs/methods/messages.ts | 51 +- src/api/gramjs/methods/settings.ts | 15 + src/api/gramjs/updates/updater.ts | 33 ++ src/api/types/messages.ts | 6 + src/api/types/misc.ts | 6 + src/api/types/updates.ts | 26 +- src/api/types/users.ts | 19 +- src/assets/font-icons/clock.svg | 1 + src/bundles/extra.ts | 2 +- src/components/calls/phone/PhoneCall.tsx | 2 +- src/components/common/Audio.tsx | 2 +- .../common/BusinessHours.module.scss | 78 +++ src/components/common/BusinessHours.tsx | 198 ++++++++ src/components/common/CalendarModal.tsx | 2 +- src/components/common/ChatExtra.tsx | 106 +++- src/components/common/Composer.tsx | 42 +- src/components/common/File.tsx | 2 +- src/components/common/LastMessageMeta.tsx | 2 +- src/components/common/Media.tsx | 2 +- .../common/MessageOutgoingStatus.tsx | 5 +- src/components/common/SeenByModal.tsx | 2 +- src/components/common/WebLink.tsx | 2 +- src/components/left/main/LeftMainHeader.tsx | 2 +- src/components/left/search/AudioResults.tsx | 2 +- src/components/left/search/ChatMessage.tsx | 2 +- src/components/left/search/DateSuggest.tsx | 2 +- src/components/left/search/FileResults.tsx | 2 +- src/components/left/search/LeftSearch.tsx | 2 +- src/components/left/search/LinkResults.tsx | 2 +- src/components/left/settings/Settings.scss | 9 + .../left/settings/SettingsActiveSession.tsx | 2 +- .../left/settings/SettingsActiveSessions.tsx | 2 +- .../left/settings/SettingsActiveWebsites.tsx | 2 +- .../settings/folders/SettingsFoldersMain.tsx | 1 + src/components/main/Main.tsx | 4 + src/components/mediaViewer/SeekLine.tsx | 2 +- src/components/mediaViewer/SenderInfo.tsx | 2 +- .../mediaViewer/VideoPlayerControls.tsx | 2 +- src/components/middle/MessageListContent.tsx | 2 +- src/components/middle/MobileSearch.tsx | 2 +- src/components/middle/ReactorListModal.tsx | 2 +- .../middle/composer/AttachmentModalItem.tsx | 2 +- src/components/middle/composer/BotCommand.tsx | 49 -- .../middle/composer/BotCommandMenu.tsx | 12 +- .../composer/BotCommandTooltip.async.tsx | 18 - .../middle/composer/BotCommandTooltip.tsx | 110 ----- .../{BotCommand.scss => ChatCommand.scss} | 0 .../middle/composer/ChatCommand.tsx | 58 +++ .../composer/ChatCommandTooltip.async.tsx | 18 + ...ip.scss => ChatCommandTooltip.module.scss} | 2 +- .../middle/composer/ChatCommandTooltip.tsx | 169 +++++++ ...andTooltip.ts => useChatCommandTooltip.ts} | 24 +- .../composer/hooks/useKeyboardNavigation.ts | 4 + .../middle/helpers/groupMessages.ts | 2 +- src/components/middle/message/BaseStory.tsx | 2 +- src/components/middle/message/Giveaway.tsx | 2 +- .../middle/message/InvoiceMediaPreview.tsx | 2 +- src/components/middle/message/Location.tsx | 11 +- src/components/middle/message/MessageMeta.tsx | 2 +- .../middle/message/MessagePhoneCall.tsx | 2 +- src/components/middle/message/Poll.tsx | 2 +- .../middle/message/ReadTimeMenuItem.tsx | 2 +- src/components/middle/message/RoundVideo.tsx | 2 +- src/components/middle/message/Video.tsx | 2 +- src/components/modals/boost/BoostModal.tsx | 2 +- .../modals/giftcode/GiftCodeModal.tsx | 2 +- src/components/right/Profile.scss | 9 + src/components/right/RightHeader.tsx | 2 +- .../right/management/JoinRequest.tsx | 2 +- .../right/management/ManageInvite.tsx | 2 +- .../right/management/ManageInviteInfo.tsx | 2 +- .../right/management/ManageInvites.tsx | 2 +- .../right/statistics/BoostStatistics.tsx | 2 +- .../right/statistics/StatisticsOverview.tsx | 2 +- .../statistics/StatisticsRecentMessage.tsx | 2 +- .../statistics/StatisticsRecentStory.tsx | 2 +- src/components/story/Story.tsx | 2 +- src/components/story/StoryView.tsx | 2 +- src/components/ui/ListItem.scss | 6 + src/components/ui/ListItem.tsx | 13 +- src/components/ui/RippleEffect.scss | 16 +- src/components/ui/RippleEffect.tsx | 3 +- src/components/ui/TextTimer.tsx | 2 +- src/config.ts | 2 +- src/global/actions/api/globalSearch.ts | 2 +- src/global/actions/api/messages.ts | 33 +- src/global/actions/api/settings.ts | 16 + src/global/actions/apiUpdaters/messages.ts | 47 +- src/global/cache.ts | 5 + src/global/helpers/chats.ts | 2 +- src/global/helpers/users.ts | 2 +- src/global/initialState.ts | 5 + src/global/reducers/messages.ts | 102 +++- src/global/selectors/messages.ts | 4 + src/global/types.ts | 17 + src/hooks/useSchedule.tsx | 2 +- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 462 +++++++++++++++++- src/lib/gramjs/tl/apiTl.js | 57 ++- src/lib/gramjs/tl/static/api.json | 3 + src/lib/gramjs/tl/static/api.tl | 99 +++- src/styles/icons.scss | 380 +++++++------- src/styles/icons.woff | Bin 29464 -> 29544 bytes src/styles/icons.woff2 | Bin 24692 -> 24696 bytes src/types/icons/font.ts | 1 + src/util/{ => date}/dateFormat.ts | 21 +- src/util/date/workHours.ts | 81 +++ 112 files changed, 2072 insertions(+), 578 deletions(-) create mode 100644 src/assets/font-icons/clock.svg create mode 100644 src/components/common/BusinessHours.module.scss create mode 100644 src/components/common/BusinessHours.tsx delete mode 100644 src/components/middle/composer/BotCommand.tsx delete mode 100644 src/components/middle/composer/BotCommandTooltip.async.tsx delete mode 100644 src/components/middle/composer/BotCommandTooltip.tsx rename src/components/middle/composer/{BotCommand.scss => ChatCommand.scss} (100%) create mode 100644 src/components/middle/composer/ChatCommand.tsx create mode 100644 src/components/middle/composer/ChatCommandTooltip.async.tsx rename src/components/middle/composer/{BotCommandTooltip.scss => ChatCommandTooltip.module.scss} (88%) create mode 100644 src/components/middle/composer/ChatCommandTooltip.tsx rename src/components/middle/composer/hooks/{useBotCommandTooltip.ts => useChatCommandTooltip.ts} (62%) rename src/util/{ => date}/dateFormat.ts (96%) create mode 100644 src/util/date/workHours.ts diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 2f3139be8..96e0fe426 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -16,6 +16,7 @@ import type { ApiNewPoll, ApiPeer, ApiPhoto, + ApiQuickReply, ApiReplyInfo, ApiReplyKeyboard, ApiSponsoredMessage, @@ -159,7 +160,7 @@ export type UniversalMessage = ( 'out' | 'message' | 'entities' | 'fromId' | 'peerId' | 'fwdFrom' | 'replyTo' | 'replyMarkup' | 'post' | 'media' | 'action' | 'views' | 'editDate' | 'editHide' | 'mediaUnread' | 'groupedId' | 'mentioned' | 'viaBotId' | 'replies' | 'fromScheduled' | 'postAuthor' | 'noforwards' | 'reactions' | 'forwards' | 'silent' | 'pinned' | - 'savedPeerId' | 'fromBoostsApplied' + 'savedPeerId' | 'fromBoostsApplied' | 'quickReplyShortcutId' )> ); @@ -1069,6 +1070,15 @@ export function buildApiThreadInfo( }; } +export function buildApiQuickReply(reply: GramJs.TypeQuickReply): ApiQuickReply { + const { shortcutId, shortcut, topMessage } = reply; + return { + id: shortcutId, + shortcut, + topMessageId: topMessage, + }; +} + function buildSponsoredWebPage(webPage: GramJs.TypeSponsoredWebPage): ApiSponsoredWebPage { let photo: ApiPhoto | undefined; if (webPage.photo instanceof GramJs.Photo) { diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 5713d86d9..0bc5c0be5 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -4,7 +4,7 @@ import type { ApiPrivacyKey } from '../../../types'; import type { ApiConfig, ApiCountry, ApiLangString, ApiPeerColors, - ApiSession, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, + ApiSession, ApiTimezone, ApiUrlAuthResult, ApiWallpaper, ApiWebSession, } from '../../types'; import { buildCollectionByCallback, omit, pick } from '../../../util/iteratees'; @@ -252,3 +252,12 @@ export function buildApiPeerColors(wrapper: GramJs.help.TypePeerColors): ApiPeer }]; }); } + +export function buildApiTimezone(timezone: GramJs.TypeTimezone): ApiTimezone { + const { id, name, utcOffset } = timezone; + return { + id, + name, + utcOffset, + }; +} diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 54c3cf590..10d0bd163 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -1,6 +1,8 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { + ApiBusinessLocation, + ApiBusinessWorkHours, ApiPremiumGiftOption, ApiUser, ApiUserFullInfo, @@ -8,8 +10,10 @@ import type { ApiUserType, } from '../../types'; +import { omitUndefined } from '../../../util/iteratees'; import { buildApiBotInfo } from './bots'; import { buildApiPhoto, buildApiUsernames } from './common'; +import { buildGeoPoint } from './messageContent'; import { buildApiEmojiStatus, buildApiPeerColor, buildApiPeerId } from './peers'; export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUserFullInfo { @@ -18,14 +22,14 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse about, commonChatsCount, pinnedMsgId, botInfo, blocked, profilePhoto, voiceMessagesForbidden, premiumGifts, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, - contactRequirePremium, + contactRequirePremium, businessWorkHours, businessLocation, }, users, } = mtpUserFull; const userId = buildApiPeerId(users[0].id, 'user'); - return { + return omitUndefined({ bio: about, commonChatsCount, pinnedMessageId: pinnedMsgId, @@ -36,10 +40,12 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse profilePhoto: profilePhoto instanceof GramJs.Photo ? buildApiPhoto(profilePhoto) : undefined, fallbackPhoto: fallbackPhoto instanceof GramJs.Photo ? buildApiPhoto(fallbackPhoto) : undefined, personalPhoto: personalPhoto instanceof GramJs.Photo ? buildApiPhoto(personalPhoto) : undefined, - ...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }), - ...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }), + premiumGifts: premiumGifts?.map((gift) => buildApiPremiumGiftOption(gift)), + botInfo: botInfo && buildApiBotInfo(botInfo, userId), isContactRequirePremium: contactRequirePremium, - }; + businessLocation: businessLocation && buildApiBusinessLocation(businessLocation), + businessWorkHours: businessWorkHours && buildApiBusinessWorkHours(businessWorkHours), + }); } export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { @@ -153,3 +159,28 @@ export function buildApiPremiumGiftOption(option: GramJs.TypePremiumGiftOption): botUrl, }; } + +export function buildApiBusinessLocation(location: GramJs.TypeBusinessLocation): ApiBusinessLocation { + const { + address, geoPoint, + } = location; + + return { + address, + geo: geoPoint && buildGeoPoint(geoPoint), + }; +} + +export function buildApiBusinessWorkHours(workHours: GramJs.TypeBusinessWorkHours): ApiBusinessWorkHours { + const { + timezoneId, weeklyOpen, + } = workHours; + + return { + timezoneId, + workHours: weeklyOpen.map(({ startMinute, endMinute }) => ({ + startMinute, + endMinute, + })), + }; +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 7e84c6424..31e142e6d 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1024,9 +1024,10 @@ export async function fetchChatFolders() { if (!result) { return undefined; } + const { filters } = result; - const defaultFolderPosition = result.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault); - const dialogFilters = result.filter(isChatFolder); + const defaultFolderPosition = filters.findIndex((folder) => folder instanceof GramJs.DialogFilterDefault); + const dialogFilters = filters.filter(isChatFolder); const orderedIds = dialogFilters.map(({ id }) => id); if (defaultFolderPosition !== -1) { orderedIds.splice(defaultFolderPosition, 0, ALL_FOLDER_ID); diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 207c62ac8..a6453a0f0 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -35,7 +35,7 @@ export { reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, - fetchOutboxReadDate, exportMessageLink, + fetchOutboxReadDate, exportMessageLink, fetchQuickReplies, sendQuickReply, deleteSavedHistory, } from './messages'; @@ -60,17 +60,7 @@ export { hideChatReportPanel, } from './management'; -export { - updateProfile, checkUsername, updateUsername, fetchBlockedUsers, blockUser, unblockUser, - updateProfilePhoto, uploadProfilePhoto, deleteProfilePhotos, fetchWallpapers, uploadWallpaper, - fetchAuthorizations, terminateAuthorization, terminateAllAuthorizations, - fetchWebAuthorizations, terminateWebAuthorization, terminateAllWebAuthorizations, - fetchNotificationExceptions, fetchNotificationSettings, updateContactSignUpNotification, updateNotificationSettings, - fetchLanguages, fetchLangPack, fetchPrivacySettings, setPrivacySettings, registerDevice, unregisterDevice, - updateIsOnline, fetchContentSettings, updateContentSettings, fetchLangStrings, fetchCountryList, fetchAppConfig, - fetchGlobalPrivacySettings, updateGlobalPrivacySettings, toggleUsername, reorderUsernames, fetchConfig, - uploadContactProfilePhoto, fetchPeerColors, -} from './settings'; +export * from './settings'; export { getPasswordInfo, checkPassword, clearPassword, updatePassword, updateRecoveryEmail, provideRecoveryEmailCode, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index c63f48cd9..b938e67fe 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -50,6 +50,7 @@ import { buildApiFormattedText } from '../apiBuilders/common'; import { buildMessageMediaContent, buildMessageTextContent, buildWebPage } from '../apiBuilders/messageContent'; import { buildApiMessage, + buildApiQuickReply, buildApiSponsoredMessage, buildApiThreadInfo, buildLocalForwardedMessage, @@ -1529,7 +1530,7 @@ export async function sendScheduledMessages({ chat, ids }: { chat: ApiChat; ids: function updateLocalDb(result: ( GramJs.messages.MessagesSlice | GramJs.messages.Messages | GramJs.messages.ChannelMessages | - GramJs.messages.DiscussionMessage | GramJs.messages.SponsoredMessages + GramJs.messages.DiscussionMessage | GramJs.messages.SponsoredMessages | GramJs.messages.QuickReplies )) { addEntitiesToLocalDb(result.users); addEntitiesToLocalDb(result.chats); @@ -1928,6 +1929,54 @@ export async function fetchOutboxReadDate({ chat, messageId }: { chat: ApiChat; return { date: result.date }; } +export async function fetchQuickReplies() { + const result = await invokeRequest(new GramJs.messages.GetQuickReplies({})); + if (!result || result instanceof GramJs.messages.QuickRepliesNotModified) return undefined; + + updateLocalDb(result); + + const messages = result.messages.map(buildApiMessage).filter(Boolean); + dispatchThreadInfoUpdates(result.messages); + + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); + const users = result.users.map(buildApiUser).filter(Boolean); + + const quickReplies = result.quickReplies.map(buildApiQuickReply); + + return { + messages, + chats, + users, + quickReplies, + }; +} + +export async function sendQuickReply({ + chat, + shortcutId, +}: { + chat: ApiChat; + shortcutId: number; +}) { + const result = await invokeRequest(new GramJs.messages.SendQuickReplyMessages({ + peer: buildInputPeer(chat.id, chat.accessHash), + shortcutId, + }), { + shouldIgnoreUpdates: true, + }); + + if (!result) return; + + // Hack to prevent client from thinking that those messages were local + if ('updates' in result) { + const filteredUpdates = result.updates + .filter((u): u is GramJs.UpdateMessageID => !(u instanceof GramJs.UpdateMessageID)); + result.updates = filteredUpdates; + } + + handleGramJsUpdate(result); +} + export async function exportMessageLink({ id, chat, shouldIncludeThread, shouldIncludeGrouped, }: { diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index f367adcb1..de02e6db8 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -26,6 +26,7 @@ import { buildApiNotifyException, buildApiPeerColors, buildApiSession, + buildApiTimezone, buildApiWallpaper, buildApiWebSession, buildLangPack, buildLangPackString, } from '../apiBuilders/misc'; @@ -584,6 +585,20 @@ export async function fetchPeerColors(hash?: number) { }; } +export async function fetchTimezones(hash?: number) { + const result = await invokeRequest(new GramJs.help.GetTimezonesList({ + hash, + })); + if (!result || result instanceof GramJs.help.TimezonesListNotModified) return undefined; + + const timezones = result.timezones.map(buildApiTimezone); + + return { + timezones, + hash: result.hash, + }; +} + function updateLocalDb( result: ( GramJs.account.PrivacyRules | GramJs.contacts.Blocked | GramJs.contacts.BlockedSlice | diff --git a/src/api/gramjs/updates/updater.ts b/src/api/gramjs/updates/updater.ts index 28f2e1485..16bc709da 100644 --- a/src/api/gramjs/updates/updater.ts +++ b/src/api/gramjs/updates/updater.ts @@ -38,6 +38,7 @@ import { buildApiMessageFromNotification, buildApiMessageFromShort, buildApiMessageFromShortChat, + buildApiQuickReply, buildApiThreadInfoFromMessage, buildMessageDraft, } from '../apiBuilders/messages'; @@ -342,6 +343,38 @@ export function updater(update: Update) { }); } } + } else if (update instanceof GramJs.UpdateQuickReplyMessage) { + const message = buildApiMessage(update.message); + if (!message) return; + + onUpdate({ + '@type': 'updateQuickReplyMessage', + id: message.id, + message, + }); + } else if (update instanceof GramJs.UpdateDeleteQuickReplyMessages) { + onUpdate({ + '@type': 'deleteQuickReplyMessages', + quickReplyId: update.shortcutId, + messageIds: update.messages, + }); + } else if (update instanceof GramJs.UpdateQuickReplies) { + const quickReplies = update.quickReplies.map(buildApiQuickReply); + onUpdate({ + '@type': 'updateQuickReplies', + quickReplies, + }); + } else if (update instanceof GramJs.UpdateNewQuickReply) { + const quickReply = buildApiQuickReply(update.quickReply); + onUpdate({ + '@type': 'updateQuickReplies', + quickReplies: [quickReply], + }); + } else if (update instanceof GramJs.UpdateDeleteQuickReply) { + onUpdate({ + '@type': 'deleteQuickReply', + quickReplyId: update.shortcutId, + }); } else if ( update instanceof GramJs.UpdateEditMessage || update instanceof GramJs.UpdateEditChannelMessage diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index de7130b72..ccb940d42 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -788,6 +788,12 @@ export type ApiMessagesBotApp = ApiBotApp & { shouldRequestWriteAccess?: boolean; }; +export type ApiQuickReply = { + id: number; + shortcut: string; + topMessageId: number; +}; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index ac700f211..c056d5731 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -222,6 +222,12 @@ export interface ApiPeerColors { generalHash?: number; } +export interface ApiTimezone { + id: string; + name: string; + utcOffset: number; +} + export interface GramJsEmojiInteraction { v: number; a: { diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 2d704b9f4..4006ce012 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -24,6 +24,7 @@ import type { ApiMessageExtendedMediaPreview, ApiPhoto, ApiPoll, + ApiQuickReply, ApiReaction, ApiReactions, ApiStickerSet, @@ -234,6 +235,28 @@ export type ApiUpdateScheduledMessage = { message: Partial; }; +export type ApiUpdateQuickReplyMessage = { + '@type': 'updateQuickReplyMessage'; + id: number; + message: Partial; +}; + +export type ApiUpdateDeleteQuickReplyMessages = { + '@type': 'deleteQuickReplyMessages'; + quickReplyId: number; + messageIds: number[]; +}; + +export type ApiUpdateQuickReplies = { + '@type': 'updateQuickReplies'; + quickReplies: ApiQuickReply[]; +}; + +export type ApiDeleteQuickReply = { + '@type': 'deleteQuickReply'; + quickReplyId: number; +}; + export type ApiUpdatePinnedMessageIds = { '@type': 'updatePinnedIds'; chatId: string; @@ -740,7 +763,8 @@ export type ApiUpdate = ( ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage | - ApiUpdateDeleteSavedHistory + ApiUpdateDeleteSavedHistory | + ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index e0f1a4939..58e1cd734 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,7 +1,7 @@ import type { API_CHAT_TYPES } from '../../config'; import type { ApiBotInfo } from './bots'; import type { ApiPeerColor } from './chats'; -import type { ApiDocument, ApiPhoto } from './messages'; +import type { ApiDocument, ApiGeoPoint, ApiPhoto } from './messages'; export interface ApiUser { id: string; @@ -54,6 +54,8 @@ export interface ApiUserFullInfo { isTranslationDisabled?: true; hasPinnedStories?: boolean; isContactRequirePremium?: boolean; + businessLocation?: ApiBusinessLocation; + businessWorkHours?: ApiBusinessWorkHours; } export type ApiFakeType = 'fake' | 'scam'; @@ -113,3 +115,18 @@ export interface ApiEmojiStatus { documentId: string; until?: number; } + +export interface ApiBusinessLocation { + geo?: ApiGeoPoint; + address: string; +} + +export interface ApiBusinessTimetableSegment { + startMinute: number; + endMinute: number; +} + +export interface ApiBusinessWorkHours { + timezoneId: string; + workHours: ApiBusinessTimetableSegment[]; +} diff --git a/src/assets/font-icons/clock.svg b/src/assets/font-icons/clock.svg new file mode 100644 index 000000000..89ca3c84f --- /dev/null +++ b/src/assets/font-icons/clock.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 3c4e34c74..479cd48f4 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -55,7 +55,7 @@ export { default as ReactionPicker } from '../components/middle/message/reaction export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal'; export { default as PollModal } from '../components/middle/composer/PollModal'; export { default as SymbolMenu } from '../components/middle/composer/SymbolMenu'; -export { default as BotCommandTooltip } from '../components/middle/composer/BotCommandTooltip'; +export { default as ChatCommandTooltip } from '../components/middle/composer/ChatCommandTooltip'; export { default as BotCommandMenu } from '../components/middle/composer/BotCommandMenu'; export { default as MentionTooltip } from '../components/middle/composer/MentionTooltip'; export { default as StickerTooltip } from '../components/middle/composer/StickerTooltip'; diff --git a/src/components/calls/phone/PhoneCall.tsx b/src/components/calls/phone/PhoneCall.tsx index d72d97220..14cf2ca5d 100644 --- a/src/components/calls/phone/PhoneCall.tsx +++ b/src/components/calls/phone/PhoneCall.tsx @@ -14,7 +14,7 @@ import { import { selectTabState } from '../../../global/selectors'; import { selectPhoneCallUser } from '../../../global/selectors/calls'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import { IS_ANDROID, IS_IOS, diff --git a/src/components/common/Audio.tsx b/src/components/common/Audio.tsx index 6565f4eee..9283c4aa9 100644 --- a/src/components/common/Audio.tsx +++ b/src/components/common/Audio.tsx @@ -24,7 +24,7 @@ import { import { makeTrackId } from '../../util/audioPlayer'; import buildClassName from '../../util/buildClassName'; import { captureEvents } from '../../util/captureEvents'; -import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/dateFormat'; +import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '../../util/date/dateFormat'; import { decodeWaveform, interpolateArray } from '../../util/waveform'; import { LOCAL_TGS_URLS } from './helpers/animatedAssets'; import { getFileSizeString } from './helpers/documentInfo'; diff --git a/src/components/common/BusinessHours.module.scss b/src/components/common/BusinessHours.module.scss new file mode 100644 index 000000000..798845044 --- /dev/null +++ b/src/components/common/BusinessHours.module.scss @@ -0,0 +1,78 @@ +.root { + cursor: pointer; +} + +.top { + display: flex; + justify-content: space-between; + align-items: center; +} + +.icon { + align-self: flex-start; + margin-top: 0.75rem; +} + +.left, .bottom { + display: flex; + flex-direction: column; +} + +.status { + color: var(--color-error); +} + +.status-open { + color: var(--color-green); +} + +.arrow { + color: var(--color-text-secondary); +} + +.offset-trigger { + display: inline-block; + padding: 0 0.5rem; + border-radius: 0.75rem; + color: var(--color-primary); + background-color: var(--color-primary-tint); + align-self: flex-end; + margin-bottom: 0.25rem; + z-index: 1; + + transition: background-color 0.2s ease-in-out; + + &:hover { + background-color: var(--color-primary-opacity); + } +} + +.transition { + /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ + transition: height 0.25s ease-in-out; +} + +.timetable { + margin-bottom: 0; + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.schedule { + color: var(--color-text-secondary); + font-size: 0.875rem; + margin-bottom: 0; + justify-self: end; + text-align: end; +} + +.weekday { + word-break: break-all; + line-height: 1.25; +} + +.current-day { + color: var(--color-primary); +} diff --git a/src/components/common/BusinessHours.tsx b/src/components/common/BusinessHours.tsx new file mode 100644 index 000000000..93c8ab459 --- /dev/null +++ b/src/components/common/BusinessHours.tsx @@ -0,0 +1,198 @@ +import React, { + memo, useEffect, useMemo, useRef, +} from '../../lib/teact/teact'; + +import type { ApiBusinessWorkHours } from '../../api/types'; + +import { requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom'; +import buildClassName from '../../util/buildClassName'; +import { formatTime, formatWeekday } from '../../util/date/dateFormat'; +import { + getUtcOffset, getWeekStart, shiftTimeRanges, splitDays, +} from '../../util/date/workHours'; +import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; + +import useInterval from '../../hooks/schedulers/useInterval'; +import useDerivedState from '../../hooks/useDerivedState'; +import useFlag from '../../hooks/useFlag'; +import useForceUpdate from '../../hooks/useForceUpdate'; +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; +import useSelectorSignal from '../../hooks/useSelectorSignal'; + +import ListItem from '../ui/ListItem'; +import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../ui/Transition'; +import Icon from './Icon'; + +import styles from './BusinessHours.module.scss'; + +const DAYS = Array.from({ length: 7 }, (_, i) => i); + +type OwnProps = { + businessHours: ApiBusinessWorkHours; +}; + +const BusinessHours = ({ + businessHours, +}: OwnProps) => { + // eslint-disable-next-line no-null/no-null + const transitionRef = useRef(null); + const [isExpanded, expand, collapse] = useFlag(false); + const [isMyTime, showInMyTime, showInLocalTime] = useFlag(false); + const lang = useLang(); + const forceUpdate = useForceUpdate(); + + useInterval(forceUpdate, 60 * 1000); + + const timezoneSignal = useSelectorSignal((global) => global.timezones?.byId); + const timezones = useDerivedState(timezoneSignal, [timezoneSignal]); + const timezoneMinuteDifference = useMemo(() => { + if (!timezones) return 0; + const timezone = timezones[businessHours.timezoneId]; + const myOffset = getUtcOffset(); + return (myOffset - timezone.utcOffset) / 60; + }, [businessHours.timezoneId, timezones]); + + const workHours = useMemo(() => { + const weekStart = getWeekStart(); + const shiftedHours = shiftTimeRanges(businessHours.workHours, isMyTime ? timezoneMinuteDifference : 0); + const days = splitDays(shiftedHours); + const result: Record = {}; + + DAYS.forEach((day) => { + const segments = days[day]; + if (!segments) { + result[day] = [lang('BusinessHoursDayClosed')]; + return; + } + + result[day] = segments.map(({ startMinute, endMinute }) => { + if (endMinute - startMinute === 24 * 60) return lang('BusinessHoursDayFullOpened'); + const start = formatTime(lang, weekStart + startMinute * 60 * 1000); + const end = formatTime(lang, weekStart + endMinute * 60 * 1000); + return `${start} – ${end}`; + }); + }); + + return result; + }, [businessHours.workHours, isMyTime, lang, timezoneMinuteDifference]); + + const isBusinessOpen = useMemo(() => { + const localTimeHours = shiftTimeRanges(businessHours.workHours, timezoneMinuteDifference); + + const weekStart = getWeekStart(); + const now = new Date().getTime(); + const minutesSinceWeekStart = (now - weekStart) / 1000 / 60; + + return localTimeHours.some(({ startMinute, endMinute }) => ( + startMinute <= minutesSinceWeekStart && minutesSinceWeekStart <= endMinute + )); + }, [businessHours.workHours, timezoneMinuteDifference]); + + const currentDay = useMemo(() => { + const now = new Date(Date.now() - (isMyTime ? 0 : timezoneMinuteDifference * 60 * 1000)); + return (now.getDay() + 6) % 7; + }, [isMyTime, timezoneMinuteDifference]); + + const handleClick = useLastCallback(() => { + if (isExpanded) { + collapse(); + } else { + expand(); + } + }); + + const handleTriggerOffset = useLastCallback((e: React.MouseEvent) => { + e.stopPropagation(); + + if (isMyTime) { + showInLocalTime(); + } else { + showInMyTime(); + } + }); + + useEffect(() => { + if (!isExpanded) return; + const slide = document.querySelector(`.${ACTIVE_SLIDE_CLASS_NAME} .${styles.timetable}`); + if (!slide) return; + + const height = slide.offsetHeight; + requestMutation(() => { + transitionRef.current!.style.height = `${height}px`; + }); + }, [isExpanded]); + + const handleAnimationStart = useLastCallback(() => { + const slide = document.querySelector(`.${TO_SLIDE_CLASS_NAME} .${styles.timetable}`)!; + + requestMeasure(() => { + const height = slide.offsetHeight; + requestMutation(() => { + transitionRef.current!.style.height = `${height}px`; + }); + }); + }); + + return ( + +
+
+
{lang('BusinessHoursProfile')}
+
+ {isBusinessOpen ? lang('BusinessHoursProfileNowOpen') : lang('BusinessHoursProfileNowClosed')} +
+
+ +
+ {isExpanded && ( +
+ {Boolean(timezoneMinuteDifference) && ( +
+ {lang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')} +
+ )} + +
+ {DAYS.map((day) => ( + <> +
+ {formatWeekday(lang, day === 6 ? 0 : day + 1)} +
+
+ {workHours[day].map((segment) => ( +
{segment}
+ ))} +
+ + ))} +
+
+
+ )} +
+ ); +}; + +export default memo(BusinessHours); diff --git a/src/components/common/CalendarModal.tsx b/src/components/common/CalendarModal.tsx index dfa975ef0..87323cafe 100644 --- a/src/components/common/CalendarModal.tsx +++ b/src/components/common/CalendarModal.tsx @@ -7,7 +7,7 @@ import type { LangFn } from '../../hooks/useLang'; import { MAX_INT_32 } from '../../config'; import buildClassName from '../../util/buildClassName'; -import { formatDateToString, formatTime, getDayStart } from '../../util/dateFormat'; +import { formatDateToString, formatTime, getDayStart } from '../../util/date/dateFormat'; import useFlag from '../../hooks/useFlag'; import useLang from '../../hooks/useLang'; diff --git a/src/components/common/ChatExtra.tsx b/src/components/common/ChatExtra.tsx index 747ba9aa1..3cac4740f 100644 --- a/src/components/common/ChatExtra.tsx +++ b/src/components/common/ChatExtra.tsx @@ -5,16 +5,16 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { - ApiChat, ApiCountryCode, ApiUser, ApiUsername, + ApiChat, ApiCountryCode, ApiUser, ApiUserFullInfo, ApiUsername, } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types'; import { TME_LINK_PREFIX } from '../../config'; import { + buildStaticMapHash, getChatLink, getHasAdminRight, isChatChannel, - isUserId, isUserRightBanned, selectIsChatMuted, } from '../../global/helpers'; @@ -37,9 +37,13 @@ import renderText from './helpers/renderText'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; +import useMedia from '../../hooks/useMedia'; +import useDevicePixelRatio from '../../hooks/window/useDevicePixelRatio'; import ListItem from '../ui/ListItem'; +import Skeleton from '../ui/placeholder/Skeleton'; import Switcher from '../ui/Switcher'; +import BusinessHours from './BusinessHours'; type OwnProps = { chatOrUserId: string; @@ -47,19 +51,25 @@ type OwnProps = { isInSettings?: boolean; }; -type StateProps = - { - user?: ApiUser; - chat?: ApiChat; - canInviteUsers?: boolean; - isMuted?: boolean; - phoneCodeList: ApiCountryCode[]; - topicId?: number; - description?: string; - chatInviteLink?: string; - topicLink?: string; - hasSavedMessages?: boolean; - }; +type StateProps = { + user?: ApiUser; + chat?: ApiChat; + userFullInfo?: ApiUserFullInfo; + canInviteUsers?: boolean; + isMuted?: boolean; + phoneCodeList: ApiCountryCode[]; + topicId?: number; + description?: string; + chatInviteLink?: string; + topicLink?: string; + hasSavedMessages?: boolean; +}; + +const DEFAULT_MAP_CONFIG = { + width: 64, + height: 64, + zoom: 15, +}; const runDebounced = debounce((cb) => cb(), 500, false); @@ -67,6 +77,7 @@ const ChatExtra: FC = ({ chatOrUserId, user, chat, + userFullInfo, isInSettings, canInviteUsers, isMuted, @@ -78,12 +89,12 @@ const ChatExtra: FC = ({ hasSavedMessages, }) => { const { - loadFullUser, showNotification, updateChatMutedState, updateTopicMutedState, loadPeerStories, openSavedDialog, + openMapModal, } = getActions(); const { @@ -94,6 +105,7 @@ const ChatExtra: FC = ({ } = user || {}; const { id: chatId, usernames: chatUsernames } = chat || {}; const peerId = userId || chatId; + const { businessLocation, businessWorkHours } = userFullInfo || {}; const lang = useLang(); const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted); @@ -102,11 +114,6 @@ const ChatExtra: FC = ({ setAreNotificationsEnabled(!isMuted); }, [isMuted]); - useEffect(() => { - if (!userId) return; - loadFullUser({ userId }); - }, [userId]); - useEffectWithPrevDeps(([prevPeerId]) => { if (!peerId || prevPeerId === peerId) return; if (user || (chat && isChatChannel(chat))) { @@ -114,6 +121,21 @@ const ChatExtra: FC = ({ } }, [peerId, chat, user]); + const { width, height, zoom } = DEFAULT_MAP_CONFIG; + const dpr = useDevicePixelRatio(); + const locationMediaHash = businessLocation?.geo + && buildStaticMapHash(businessLocation.geo, width, height, zoom, dpr); + const locationBlobUrl = useMedia(locationMediaHash); + + const locationRightComponent = useMemo(() => { + if (!businessLocation?.geo) return undefined; + if (locationBlobUrl) { + return ; + } + + return ; + }, [businessLocation, locationBlobUrl]); + const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID); const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium; @@ -137,6 +159,17 @@ const ChatExtra: FC = ({ return isTopicInfo ? topicLink! : getChatLink(chat) || chatInviteLink; }, [chat, isTopicInfo, topicLink, chatInviteLink]); + const handleClickLocation = useLastCallback(() => { + const { address, geo } = businessLocation!; + if (!geo) { + copyTextToClipboard(address); + showNotification({ message: lang('BusinessLocationCopied') }); + return; + } + + openMapModal({ geoPoint: geo, zoom }); + }); + const handleNotificationChange = useLastCallback(() => { setAreNotificationsEnabled((current) => { const newAreNotificationsEnabled = !current; @@ -242,6 +275,7 @@ const ChatExtra: FC = ({ multiline narrow isStatic + allowSelection > { @@ -280,6 +314,21 @@ const ChatExtra: FC = ({ /> )} + {businessWorkHours && ( + + )} + {businessLocation && ( + +
{businessLocation.address}
+ {lang('BusinessProfileLocation')} +
+ )} {hasSavedMessages && !isInSettings && ( {lang('SavedMessagesTab')} @@ -294,16 +343,18 @@ export default memo(withGlobal( const { countryList: { phoneCodes: phoneCodeList } } = global; const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; - const user = isUserId(chatOrUserId) ? selectUser(global, chatOrUserId) : undefined; + const user = chatOrUserId ? selectUser(global, chatOrUserId) : undefined; const isForum = chat?.isForum; const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); const { threadId } = selectCurrentMessageList(global) || {}; const topicId = isForum ? Number(threadId) : undefined; - const chatInviteLink = chat ? selectChatFullInfo(global, chat.id)?.inviteLink : undefined; - let description = user ? selectUserFullInfo(global, user.id)?.bio : undefined; - if (!description && chat) { - description = selectChatFullInfo(global, chat.id)?.about; - } + + const chatFullInfo = chat && selectChatFullInfo(global, chat.id); + const userFullInfo = user && selectUserFullInfo(global, user.id); + + const chatInviteLink = chatFullInfo?.inviteLink; + + const description = userFullInfo?.bio || chatFullInfo?.about; const canInviteUsers = chat && !user && ( (!isChatChannel(chat) && !isUserRightBanned(chat, 'inviteUsers')) @@ -318,6 +369,7 @@ export default memo(withGlobal( phoneCodeList, chat, user, + userFullInfo, canInviteUsers, isMuted, topicId, diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index 7c070ca76..62227c337 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -18,6 +18,7 @@ import type { ApiMessage, ApiMessageEntity, ApiNewPoll, + ApiQuickReply, ApiReaction, ApiStealthMode, ApiSticker, @@ -86,7 +87,7 @@ import { } from '../../global/selectors'; import { selectCurrentLimit } from '../../global/selectors/limits'; import buildClassName from '../../util/buildClassName'; -import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dateFormat'; +import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/date/dateFormat'; import deleteLastCharacterOutsideSelection from '../../util/deleteLastCharacterOutsideSelection'; import { processMessageInputForCustomEmoji } from '../../util/emoji/customEmojiManager'; import focusEditableElement from '../../util/focusEditableElement'; @@ -122,7 +123,7 @@ import useSignal from '../../hooks/useSignal'; import { useStateRef } from '../../hooks/useStateRef'; import useSyncEffect from '../../hooks/useSyncEffect'; import useAttachmentModal from '../middle/composer/hooks/useAttachmentModal'; -import useBotCommandTooltip from '../middle/composer/hooks/useBotCommandTooltip'; +import useChatCommandTooltip from '../middle/composer/hooks/useChatCommandTooltip'; import useClipboardPaste from '../middle/composer/hooks/useClipboardPaste'; import useCustomEmojiTooltip from '../middle/composer/hooks/useCustomEmojiTooltip'; import useDraft from '../middle/composer/hooks/useDraft'; @@ -136,9 +137,9 @@ import useVoiceRecording from '../middle/composer/hooks/useVoiceRecording'; import AttachmentModal from '../middle/composer/AttachmentModal.async'; import AttachMenu from '../middle/composer/AttachMenu'; import BotCommandMenu from '../middle/composer/BotCommandMenu.async'; -import BotCommandTooltip from '../middle/composer/BotCommandTooltip.async'; import BotKeyboardMenu from '../middle/composer/BotKeyboardMenu'; import BotMenuButton from '../middle/composer/BotMenuButton'; +import ChatCommandTooltip from '../middle/composer/ChatCommandTooltip.async'; import ComposerEmbeddedMessage from '../middle/composer/ComposerEmbeddedMessage'; import CustomEmojiTooltip from '../middle/composer/CustomEmojiTooltip.async'; import CustomSendMenu from '../middle/composer/CustomSendMenu.async'; @@ -247,6 +248,9 @@ type StateProps = sentStoryReaction?: ApiReaction; stealthMode?: ApiStealthMode; canSendOneTimeMedia?: boolean; + quickReplyMessages?: Record; + quickReplies?: Record; + canSendQuickReplies?: boolean; }; enum MainButtonState { @@ -349,6 +353,9 @@ const Composer: FC = ({ sentStoryReaction, stealthMode, canSendOneTimeMedia, + quickReplyMessages, + quickReplies, + canSendQuickReplies, onForward, }) => { const { @@ -659,18 +666,22 @@ const Composer: FC = ({ inlineBots, ); + const hasQuickReplies = Boolean(quickReplies && Object.keys(quickReplies).length); + const { - isOpen: isBotCommandTooltipOpen, - close: closeBotCommandTooltip, + isOpen: isChatCommandTooltipOpen, + close: closeChatCommandTooltip, filteredBotCommands: botTooltipCommands, - } = useBotCommandTooltip( + filteredQuickReplies: quickReplyCommands, + } = useChatCommandTooltip( Boolean(isInMessageList && isReady && isForCurrentMessageList - && ((botCommands && botCommands?.length) || chatBotCommands?.length)), + && ((botCommands && botCommands?.length) || chatBotCommands?.length || (hasQuickReplies && canSendQuickReplies))), getHtml, botCommands, chatBotCommands, + canSendQuickReplies ? quickReplies : undefined, ); useDraft({ @@ -1321,7 +1332,7 @@ const Composer: FC = ({ const isComposerHasFocus = isBotKeyboardOpen || isSymbolMenuOpen || isEmojiTooltipOpen || isSendAsMenuOpen || isMentionTooltipOpen || isInlineBotTooltipOpen || isDeleteModalOpen || isBotCommandMenuOpen || isAttachMenuOpen - || isStickerTooltipOpen || isBotCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen + || isStickerTooltipOpen || isChatCommandTooltipOpen || isCustomEmojiTooltipOpen || isBotMenuButtonOpen || isCustomSendMenuOpen || Boolean(activeVoiceRecording) || attachments.length > 0 || isInputHasFocus; const isReactionSelectorOpen = isComposerHasFocus && !isReactionPickerOpen && isInStoryViewer && !isAttachMenuOpen && !isSymbolMenuOpen; @@ -1584,13 +1595,17 @@ const Composer: FC = ({ onInsertUserName={insertMention} onClose={closeMentionTooltip} /> -
@@ -2003,6 +2018,8 @@ export default memo(withGlobal( : undefined; const isInScheduledList = messageListType === 'scheduled'; + const canSendQuickReplies = isChatWithUser && !isChatWithBot && !isInScheduledList && !isChatWithSelf; + return { availableReactions: type === 'story' ? global.reactions.availableReactions : undefined, topReactions: type === 'story' ? global.reactions.topReactions : undefined, @@ -2067,6 +2084,9 @@ export default memo(withGlobal( sentStoryReaction, stealthMode: global.stories.stealthMode, replyToTopic, + quickReplyMessages: global.quickReplies.messagesById, + quickReplies: global.quickReplies.byId, + canSendQuickReplies, }; }, )(Composer)); diff --git a/src/components/common/File.tsx b/src/components/common/File.tsx index cdd742890..99e89bf82 100644 --- a/src/components/common/File.tsx +++ b/src/components/common/File.tsx @@ -6,7 +6,7 @@ import React, { import type { IconName } from '../../types/icons'; import buildClassName from '../../util/buildClassName'; -import { formatMediaDateTime, formatPastTimeShort } from '../../util/dateFormat'; +import { formatMediaDateTime, formatPastTimeShort } from '../../util/date/dateFormat'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/windowEnvironment'; import { getColorFromExtension, getFileSizeString } from './helpers/documentInfo'; import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions'; diff --git a/src/components/common/LastMessageMeta.tsx b/src/components/common/LastMessageMeta.tsx index 16fd0c6ef..ea71c9d26 100644 --- a/src/components/common/LastMessageMeta.tsx +++ b/src/components/common/LastMessageMeta.tsx @@ -3,7 +3,7 @@ import React, { memo } from '../../lib/teact/teact'; import type { ApiMessage, ApiMessageOutgoingStatus } from '../../api/types'; -import { formatPastTimeShort } from '../../util/dateFormat'; +import { formatPastTimeShort } from '../../util/date/dateFormat'; import useLang from '../../hooks/useLang'; diff --git a/src/components/common/Media.tsx b/src/components/common/Media.tsx index f6ab22aeb..3fe5b7990 100644 --- a/src/components/common/Media.tsx +++ b/src/components/common/Media.tsx @@ -12,7 +12,7 @@ import { getMessageVideo, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; -import { formatMediaDuration } from '../../util/dateFormat'; +import { formatMediaDuration } from '../../util/date/dateFormat'; import stopEvent from '../../util/stopEvent'; import useFlag from '../../hooks/useFlag'; diff --git a/src/components/common/MessageOutgoingStatus.tsx b/src/components/common/MessageOutgoingStatus.tsx index 7e69a28a9..28aa03348 100644 --- a/src/components/common/MessageOutgoingStatus.tsx +++ b/src/components/common/MessageOutgoingStatus.tsx @@ -4,6 +4,7 @@ import React, { memo } from '../../lib/teact/teact'; import type { ApiMessageOutgoingStatus } from '../../api/types'; import Transition from '../ui/Transition'; +import Icon from './Icon'; import './MessageOutgoingStatus.scss'; @@ -21,9 +22,9 @@ const MessageOutgoingStatus: FC = ({ status }) => { {status === 'failed' ? (
- +
- ) : } + ) : }
); diff --git a/src/components/common/SeenByModal.tsx b/src/components/common/SeenByModal.tsx index d4c364318..be03b81ad 100644 --- a/src/components/common/SeenByModal.tsx +++ b/src/components/common/SeenByModal.tsx @@ -3,7 +3,7 @@ import { getActions, withGlobal } from '../../global'; import { selectChatMessage, selectTabState } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { formatDateAtTime } from '../../util/dateFormat'; +import { formatDateAtTime } from '../../util/date/dateFormat'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useLang from '../../hooks/useLang'; diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx index 4db269113..cfaab6e68 100644 --- a/src/components/common/WebLink.tsx +++ b/src/components/common/WebLink.tsx @@ -10,7 +10,7 @@ import { getMessageWebPage, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; -import { formatPastTimeShort } from '../../util/dateFormat'; +import { formatPastTimeShort } from '../../util/date/dateFormat'; import trimText from '../../util/trimText'; import { renderMessageSummary } from './helpers/renderMessageText'; import renderText from './helpers/renderText'; diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index fee74a56f..3b06caf6a 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -22,7 +22,7 @@ import { } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import captureEscKeyListener from '../../../util/captureEscKeyListener'; -import { formatDateToString } from '../../../util/dateFormat'; +import { formatDateToString } from '../../../util/date/dateFormat'; import { IS_APP, IS_ELECTRON, IS_MAC_OS } from '../../../util/windowEnvironment'; import useAppLayout from '../../../hooks/useAppLayout'; diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index 7eaf593f1..56c789d3b 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -7,7 +7,7 @@ import { AudioOrigin, LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import buildClassName from '../../../util/buildClassName'; -import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; +import { formatMonthAndYear, toYearMonth } from '../../../util/date/dateFormat'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { throttle } from '../../../util/schedulers'; import { createMapStateToProps } from './helpers/createMapStateToProps'; diff --git a/src/components/left/search/ChatMessage.tsx b/src/components/left/search/ChatMessage.tsx index 2a431be9b..2619940b2 100644 --- a/src/components/left/search/ChatMessage.tsx +++ b/src/components/left/search/ChatMessage.tsx @@ -19,7 +19,7 @@ import { } from '../../../global/helpers'; import { selectChat, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatPastTimeShort } from '../../../util/dateFormat'; +import { formatPastTimeShort } from '../../../util/date/dateFormat'; import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import useAppLayout from '../../../hooks/useAppLayout'; diff --git a/src/components/left/search/DateSuggest.tsx b/src/components/left/search/DateSuggest.tsx index 94028e9a2..baf065c65 100644 --- a/src/components/left/search/DateSuggest.tsx +++ b/src/components/left/search/DateSuggest.tsx @@ -1,7 +1,7 @@ import type { FC } from '../../../lib/teact/teact'; import React, { memo, useMemo } from '../../../lib/teact/teact'; -import { formatDateToString } from '../../../util/dateFormat'; +import { formatDateToString } from '../../../util/date/dateFormat'; import './DateSuggest.scss'; diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index 8c632f92c..bcca2f592 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -11,7 +11,7 @@ import { LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import { getMessageDocument } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; +import { formatMonthAndYear, toYearMonth } from '../../../util/date/dateFormat'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { throttle } from '../../../util/schedulers'; import { createMapStateToProps } from './helpers/createMapStateToProps'; diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index b47aa7bb6..856ead852 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -8,7 +8,7 @@ import { getActions, withGlobal } from '../../../global'; import { GlobalSearchContent } from '../../../types'; import { selectTabState } from '../../../global/selectors'; -import { parseDateString } from '../../../util/dateFormat'; +import { parseDateString } from '../../../util/date/dateFormat'; import useHistoryBack from '../../../hooks/useHistoryBack'; import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation'; diff --git a/src/components/left/search/LinkResults.tsx b/src/components/left/search/LinkResults.tsx index 7093b5387..638947415 100644 --- a/src/components/left/search/LinkResults.tsx +++ b/src/components/left/search/LinkResults.tsx @@ -9,7 +9,7 @@ import { LoadMoreDirection } from '../../../types'; import { SLIDE_TRANSITION_DURATION } from '../../../config'; import buildClassName from '../../../util/buildClassName'; -import { formatMonthAndYear, toYearMonth } from '../../../util/dateFormat'; +import { formatMonthAndYear, toYearMonth } from '../../../util/date/dateFormat'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { throttle } from '../../../util/schedulers'; import { createMapStateToProps } from './helpers/createMapStateToProps'; diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index 08aeaf1ad..45ac789a4 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -120,6 +120,15 @@ margin-bottom: 0.25rem; } } + + .business-location { + width: 4rem; + height: 4rem; + object-fit: cover; + border-radius: 0.25rem; + flex-shrink: 0; + margin-inline-start: 0.25rem; + } } .settings-item-simple, diff --git a/src/components/left/settings/SettingsActiveSession.tsx b/src/components/left/settings/SettingsActiveSession.tsx index 602e4a1b3..fc3bf6d35 100644 --- a/src/components/left/settings/SettingsActiveSession.tsx +++ b/src/components/left/settings/SettingsActiveSession.tsx @@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiSession } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { formatDateTimeToString } from '../../../util/dateFormat'; +import { formatDateTimeToString } from '../../../util/date/dateFormat'; import getSessionIcon from './helpers/getSessionIcon'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; diff --git a/src/components/left/settings/SettingsActiveSessions.tsx b/src/components/left/settings/SettingsActiveSessions.tsx index ac4522d2c..83e4e2a6c 100644 --- a/src/components/left/settings/SettingsActiveSessions.tsx +++ b/src/components/left/settings/SettingsActiveSessions.tsx @@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../../global'; import type { ApiSession } from '../../../api/types'; -import { formatPastTimeShort } from '../../../util/dateFormat'; +import { formatPastTimeShort } from '../../../util/date/dateFormat'; import getSessionIcon from './helpers/getSessionIcon'; import useFlag from '../../../hooks/useFlag'; diff --git a/src/components/left/settings/SettingsActiveWebsites.tsx b/src/components/left/settings/SettingsActiveWebsites.tsx index 079104cf0..5f1280d06 100644 --- a/src/components/left/settings/SettingsActiveWebsites.tsx +++ b/src/components/left/settings/SettingsActiveWebsites.tsx @@ -7,7 +7,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global'; import type { ApiWebSession } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { formatPastTimeShort } from '../../../util/dateFormat'; +import { formatPastTimeShort } from '../../../util/date/dateFormat'; import useFlag from '../../../hooks/useFlag'; import useHistoryBack from '../../../hooks/useHistoryBack'; diff --git a/src/components/left/settings/folders/SettingsFoldersMain.tsx b/src/components/left/settings/folders/SettingsFoldersMain.tsx index 58108fec5..079def0b3 100644 --- a/src/components/left/settings/folders/SettingsFoldersMain.tsx +++ b/src/components/left/settings/folders/SettingsFoldersMain.tsx @@ -248,6 +248,7 @@ const SettingsFoldersMain: FC = ({ inactive multiline isStatic + allowSelection > {folder.title} diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index c0f90f773..ec22420d1 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -271,6 +271,8 @@ const Main: FC = ({ loadAuthorizations, loadPeerColors, loadSavedReactionTags, + loadTimezones, + loadQuickReplies, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -348,6 +350,8 @@ const Main: FC = ({ loadFeaturedEmojiStickers(); loadAuthorizations(); loadSavedReactionTags(); + loadTimezones(); + loadQuickReplies(); } }, [isMasterTab, isSynced]); diff --git a/src/components/mediaViewer/SeekLine.tsx b/src/components/mediaViewer/SeekLine.tsx index 7a9a15f4c..c621d1dcf 100644 --- a/src/components/mediaViewer/SeekLine.tsx +++ b/src/components/mediaViewer/SeekLine.tsx @@ -11,7 +11,7 @@ import { createVideoPreviews, getPreviewDimensions, renderVideoPreview } from '. import { animateNumber } from '../../util/animation'; import buildClassName from '../../util/buildClassName'; import { captureEvents } from '../../util/captureEvents'; -import { formatMediaDuration } from '../../util/dateFormat'; +import { formatMediaDuration } from '../../util/date/dateFormat'; import { clamp, round } from '../../util/math'; import { IS_TOUCH_ENV } from '../../util/windowEnvironment'; diff --git a/src/components/mediaViewer/SenderInfo.tsx b/src/components/mediaViewer/SenderInfo.tsx index 1bf471c23..1d81eb2f7 100644 --- a/src/components/mediaViewer/SenderInfo.tsx +++ b/src/components/mediaViewer/SenderInfo.tsx @@ -10,7 +10,7 @@ import { selectPeer, selectSender, } from '../../global/selectors'; -import { formatMediaDateTime } from '../../util/dateFormat'; +import { formatMediaDateTime } from '../../util/date/dateFormat'; import renderText from '../common/helpers/renderText'; import useAppLayout from '../../hooks/useAppLayout'; diff --git a/src/components/mediaViewer/VideoPlayerControls.tsx b/src/components/mediaViewer/VideoPlayerControls.tsx index 4a7d0a0ef..8dd60e09a 100644 --- a/src/components/mediaViewer/VideoPlayerControls.tsx +++ b/src/components/mediaViewer/VideoPlayerControls.tsx @@ -8,7 +8,7 @@ import type { ApiDimensions } from '../../api/types'; import type { BufferedRange } from '../../hooks/useBuffering'; import buildClassName from '../../util/buildClassName'; -import { formatMediaDuration } from '../../util/dateFormat'; +import { formatMediaDuration } from '../../util/date/dateFormat'; import { formatFileSize } from '../../util/textFormat'; import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment'; diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx index 2aaf8a5d0..9d9eff96e 100644 --- a/src/components/middle/MessageListContent.tsx +++ b/src/components/middle/MessageListContent.tsx @@ -15,7 +15,7 @@ import { getMessageHtmlId, getMessageOriginalId, isActionMessage, isOwnMessage, isServiceNotificationMessage, } from '../../global/helpers'; import buildClassName from '../../util/buildClassName'; -import { formatHumanDate } from '../../util/dateFormat'; +import { formatHumanDate } from '../../util/date/dateFormat'; import { compact } from '../../util/iteratees'; import { isAlbum } from './helpers/groupMessages'; import { preventMessageInputBlur } from './helpers/preventMessageInputBlur'; diff --git a/src/components/middle/MobileSearch.tsx b/src/components/middle/MobileSearch.tsx index f000f7ab4..fb42e24de 100644 --- a/src/components/middle/MobileSearch.tsx +++ b/src/components/middle/MobileSearch.tsx @@ -21,7 +21,7 @@ import { selectIsCurrentUserPremium, selectTabState, } from '../../global/selectors'; -import { getDayStartAt } from '../../util/dateFormat'; +import { getDayStartAt } from '../../util/date/dateFormat'; import { debounce } from '../../util/schedulers'; import { IS_IOS } from '../../util/windowEnvironment'; diff --git a/src/components/middle/ReactorListModal.tsx b/src/components/middle/ReactorListModal.tsx index d2bb7dd42..57e8901e6 100644 --- a/src/components/middle/ReactorListModal.tsx +++ b/src/components/middle/ReactorListModal.tsx @@ -14,7 +14,7 @@ import { selectTabState, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { formatDateAtTime } from '../../util/dateFormat'; +import { formatDateAtTime } from '../../util/date/dateFormat'; import { unique } from '../../util/iteratees'; import { formatIntegerCompact } from '../../util/textFormat'; diff --git a/src/components/middle/composer/AttachmentModalItem.tsx b/src/components/middle/composer/AttachmentModalItem.tsx index 2928315f1..daf7d6755 100644 --- a/src/components/middle/composer/AttachmentModalItem.tsx +++ b/src/components/middle/composer/AttachmentModalItem.tsx @@ -5,7 +5,7 @@ import type { ApiAttachment } from '../../../api/types'; import { SUPPORTED_IMAGE_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES } from '../../../config'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import { getFileExtension } from '../../common/helpers/documentInfo'; import { REM } from '../../common/helpers/mediaDimensions'; diff --git a/src/components/middle/composer/BotCommand.tsx b/src/components/middle/composer/BotCommand.tsx deleted file mode 100644 index 067f7fa63..000000000 --- a/src/components/middle/composer/BotCommand.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo } from '../../../lib/teact/teact'; - -import type { ApiBotCommand, ApiUser } from '../../../api/types'; - -import buildClassName from '../../../util/buildClassName'; -import renderText from '../../common/helpers/renderText'; - -import Avatar from '../../common/Avatar'; -import ListItem from '../../ui/ListItem'; - -import './BotCommand.scss'; - -type OwnProps = { - botCommand: ApiBotCommand; - bot?: ApiUser; - withAvatar?: boolean; - focus?: boolean; - onClick: (botCommand: ApiBotCommand) => void; -}; - -const BotCommand: FC = ({ - withAvatar, - focus, - botCommand, - bot, - onClick, -}) => { - return ( - onClick(botCommand)} - focus={focus} - > - {withAvatar && ( - - )} -
- /{botCommand.command} - {renderText(botCommand.description)} -
-
- ); -}; - -export default memo(BotCommand); diff --git a/src/components/middle/composer/BotCommandMenu.tsx b/src/components/middle/composer/BotCommandMenu.tsx index 60f1016da..f6912f5a9 100644 --- a/src/components/middle/composer/BotCommandMenu.tsx +++ b/src/components/middle/composer/BotCommandMenu.tsx @@ -11,7 +11,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import useMouseInside from '../../../hooks/useMouseInside'; import Menu from '../../ui/Menu'; -import BotCommand from './BotCommand'; +import ChatCommand from './ChatCommand'; import './BotCommandMenu.scss'; @@ -29,9 +29,9 @@ const BotCommandMenu: FC = ({ const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose, undefined, isMobile); - const handleClick = useLastCallback((botCommand: ApiBotCommand) => { + const handleClick = useLastCallback((command: string) => { sendBotCommand({ - command: `/${botCommand.command}`, + command: `/${command}`, }); onClose(); }); @@ -50,9 +50,11 @@ const BotCommandMenu: FC = ({ noCompact > {botCommands.map((botCommand) => ( - ))} diff --git a/src/components/middle/composer/BotCommandTooltip.async.tsx b/src/components/middle/composer/BotCommandTooltip.async.tsx deleted file mode 100644 index 9e92afc42..000000000 --- a/src/components/middle/composer/BotCommandTooltip.async.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type { FC } from '../../../lib/teact/teact'; -import React from '../../../lib/teact/teact'; - -import type { OwnProps } from './BotCommandTooltip'; - -import { Bundles } from '../../../util/moduleLoader'; - -import useModuleLoader from '../../../hooks/useModuleLoader'; - -const BotCommandTooltipAsync: FC = (props) => { - const { isOpen } = props; - const BotCommandTooltip = useModuleLoader(Bundles.Extra, 'BotCommandTooltip', !isOpen); - - // eslint-disable-next-line react/jsx-props-no-spreading - return BotCommandTooltip ? : undefined; -}; - -export default BotCommandTooltipAsync; diff --git a/src/components/middle/composer/BotCommandTooltip.tsx b/src/components/middle/composer/BotCommandTooltip.tsx deleted file mode 100644 index 5e3b6b850..000000000 --- a/src/components/middle/composer/BotCommandTooltip.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { FC } from '../../../lib/teact/teact'; -import React, { memo, useEffect, useRef } from '../../../lib/teact/teact'; -import { getActions, getGlobal } from '../../../global'; - -import type { ApiBotCommand } from '../../../api/types'; -import type { Signal } from '../../../util/signals'; - -import buildClassName from '../../../util/buildClassName'; -import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; - -import useLastCallback from '../../../hooks/useLastCallback'; -import usePrevious from '../../../hooks/usePrevious'; -import useShowTransition from '../../../hooks/useShowTransition'; -import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; - -import BotCommand from './BotCommand'; - -import './BotCommandTooltip.scss'; - -export type OwnProps = { - isOpen: boolean; - withUsername?: boolean; - botCommands?: ApiBotCommand[]; - getHtml: Signal; - onClick: NoneToVoidFunction; - onClose: NoneToVoidFunction; -}; - -const BotCommandTooltip: FC = ({ - isOpen, - withUsername, - botCommands, - getHtml, - onClick, - onClose, -}) => { - const { sendBotCommand } = getActions(); - - // eslint-disable-next-line no-null/no-null - const containerRef = useRef(null); - const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); - - const handleSendCommand = useLastCallback(({ botId, command }: ApiBotCommand) => { - // No need for expensive global updates on users and chats, so we avoid them - const usersById = getGlobal().users.byId; - const bot = usersById[botId]; - - sendBotCommand({ - command: `/${command}${withUsername && bot ? `@${bot.usernames![0].username}` : ''}`, - }); - onClick(); - }); - - const handleSelect = useLastCallback((botCommand: ApiBotCommand) => { - // We need an additional check because tooltip is updated with throttling - if (!botCommand.command.startsWith(getHtml().slice(1))) { - return false; - } - - handleSendCommand(botCommand); - return true; - }); - - const selectedCommandIndex = useKeyboardNavigation({ - isActive: isOpen, - items: botCommands, - onSelect: handleSelect, - onClose, - }); - - useEffect(() => { - if (botCommands && !botCommands.length) { - onClose(); - } - }, [botCommands, onClose]); - - useEffect(() => { - setTooltipItemVisible('.chat-item-clickable', selectedCommandIndex, containerRef); - }, [selectedCommandIndex]); - - const prevCommands = usePrevious(botCommands && botCommands.length ? botCommands : undefined, shouldRender); - const renderedCommands = botCommands && !botCommands.length ? prevCommands : botCommands; - - if (!shouldRender || (renderedCommands && !renderedCommands.length)) { - return undefined; - } - - const className = buildClassName( - 'BotCommandTooltip composer-tooltip custom-scroll', - transitionClassNames, - ); - - return ( -
- {renderedCommands && renderedCommands.map((chatBotCommand, index) => ( - - ))} -
- ); -}; - -export default memo(BotCommandTooltip); diff --git a/src/components/middle/composer/BotCommand.scss b/src/components/middle/composer/ChatCommand.scss similarity index 100% rename from src/components/middle/composer/BotCommand.scss rename to src/components/middle/composer/ChatCommand.scss diff --git a/src/components/middle/composer/ChatCommand.tsx b/src/components/middle/composer/ChatCommand.tsx new file mode 100644 index 000000000..e172c62a3 --- /dev/null +++ b/src/components/middle/composer/ChatCommand.tsx @@ -0,0 +1,58 @@ +import React, { memo } from '../../../lib/teact/teact'; + +import type { ApiUser } from '../../../api/types'; + +import buildClassName from '../../../util/buildClassName'; +import renderText from '../../common/helpers/renderText'; + +import useLastCallback from '../../../hooks/useLastCallback'; + +import Avatar from '../../common/Avatar'; +import ListItem from '../../ui/ListItem'; + +import './ChatCommand.scss'; + +type OwnProps = { + command: string; + description: string; + peer?: ApiUser; + withAvatar?: boolean; + focus?: boolean; + clickArg: T; + onClick: (arg: T) => void; +}; + +// eslint-disable-next-line @typescript-eslint/comma-dangle +const ChatCommand = ({ + withAvatar, + focus, + command, + description, + peer, + clickArg, + onClick, +}: OwnProps) => { + const handleClick = useLastCallback(() => { + onClick(clickArg); + }); + + return ( + + {withAvatar && ( + + )} +
+ /{command} + {renderText(description)} +
+
+ ); +}; + +export default memo(ChatCommand); diff --git a/src/components/middle/composer/ChatCommandTooltip.async.tsx b/src/components/middle/composer/ChatCommandTooltip.async.tsx new file mode 100644 index 000000000..c19caedb0 --- /dev/null +++ b/src/components/middle/composer/ChatCommandTooltip.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; + +import type { OwnProps } from './ChatCommandTooltip'; + +import { Bundles } from '../../../util/moduleLoader'; + +import useModuleLoader from '../../../hooks/useModuleLoader'; + +const ChatCommandTooltipAsync: FC = (props) => { + const { isOpen } = props; + const ChatCommandTooltip = useModuleLoader(Bundles.Extra, 'ChatCommandTooltip', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ChatCommandTooltip ? : undefined; +}; + +export default ChatCommandTooltipAsync; diff --git a/src/components/middle/composer/BotCommandTooltip.scss b/src/components/middle/composer/ChatCommandTooltip.module.scss similarity index 88% rename from src/components/middle/composer/BotCommandTooltip.scss rename to src/components/middle/composer/ChatCommandTooltip.module.scss index 8c65d4006..e71b15385 100644 --- a/src/components/middle/composer/BotCommandTooltip.scss +++ b/src/components/middle/composer/ChatCommandTooltip.module.scss @@ -1,4 +1,4 @@ -.BotCommandTooltip { +.root { width: calc(100% - 4rem); max-width: 26rem; flex-direction: column; diff --git a/src/components/middle/composer/ChatCommandTooltip.tsx b/src/components/middle/composer/ChatCommandTooltip.tsx new file mode 100644 index 000000000..4f105b4d6 --- /dev/null +++ b/src/components/middle/composer/ChatCommandTooltip.tsx @@ -0,0 +1,169 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { + memo, useEffect, useMemo, useRef, +} from '../../../lib/teact/teact'; +import { getActions, getGlobal } from '../../../global'; + +import type { + ApiBotCommand, ApiMessage, ApiQuickReply, ApiUser, +} from '../../../api/types'; +import type { Signal } from '../../../util/signals'; + +import buildClassName from '../../../util/buildClassName'; +import freezeWhenClosed from '../../../util/hoc/freezeWhenClosed'; +import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; + +import useLastCallback from '../../../hooks/useLastCallback'; +import useShowTransition from '../../../hooks/useShowTransition'; +import { useKeyboardNavigation } from './hooks/useKeyboardNavigation'; + +import ChatCommand from './ChatCommand'; + +import styles from './ChatCommandTooltip.module.scss'; + +export type OwnProps = { + isOpen: boolean; + chatId: string; + withUsername?: boolean; + botCommands?: ApiBotCommand[]; + quickReplies?: ApiQuickReply[]; + quickReplyMessages?: Record; + self: ApiUser; + getHtml: Signal; + onClick: NoneToVoidFunction; + onClose: NoneToVoidFunction; +}; + +type QuickReplyWithDescription = { + id: number; + command: string; + description: string; +}; + +const ChatCommandTooltip: FC = ({ + isOpen, + chatId, + withUsername, + botCommands, + quickReplies, + quickReplyMessages, + self, + getHtml, + onClick, + onClose, +}) => { + const { sendBotCommand, sendQuickReply } = getActions(); + + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); + const { shouldRender, transitionClassNames } = useShowTransition(isOpen, undefined, undefined, false); + + const handleSendCommand = useLastCallback(({ botId, command }: ApiBotCommand) => { + // No need for expensive global updates on users and chats, so we avoid them + const usersById = getGlobal().users.byId; + const bot = usersById[botId]; + + sendBotCommand({ + command: `/${command}${withUsername && bot ? `@${bot.usernames![0].username}` : ''}`, + }); + onClick(); + }); + + const handleSendQuickReply = useLastCallback((id: number) => { + sendQuickReply({ chatId, quickReplyId: id }); + onClick(); + }); + + const quickRepliesWithDescription = useMemo(() => { + if (!quickReplies?.length || !quickReplyMessages) return undefined; + return quickReplies.map((reply) => { + const message = quickReplyMessages[reply.topMessageId]; + return { + id: reply.id, + command: reply.shortcut, + description: message?.content.text?.text || '', + } satisfies QuickReplyWithDescription; + }); + }, [quickReplies, quickReplyMessages]); + + const handleKeyboardSelect = useLastCallback((item: ApiBotCommand | QuickReplyWithDescription) => { + if (!item.command.startsWith(getHtml().slice(1))) { + return false; + } + + if ('id' in item) { + handleSendQuickReply(item.id); + } else { + handleSendCommand(item); + } + + return true; + }); + + const keyboardNavigationItems = useMemo(() => { + if (!botCommands && !quickRepliesWithDescription) return undefined; + return ([] as (ApiBotCommand | QuickReplyWithDescription)[]) + .concat(quickRepliesWithDescription || [], botCommands || []); + }, [botCommands, quickRepliesWithDescription]); + + const selectedCommandIndex = useKeyboardNavigation({ + isActive: isOpen, + items: keyboardNavigationItems, + onSelect: handleKeyboardSelect, + onClose, + }); + + const isEmpty = (botCommands && !botCommands.length) || (quickReplies && !quickReplies.length); + + useEffect(() => { + if (isEmpty) { + onClose(); + } + }, [isEmpty, onClose]); + + useEffect(() => { + setTooltipItemVisible('.chat-item-clickable', selectedCommandIndex, containerRef); + }, [selectedCommandIndex]); + + if (!shouldRender || isEmpty) { + return undefined; + } + + const className = buildClassName( + styles.root, + 'composer-tooltip custom-scroll', + transitionClassNames, + ); + + return ( +
+ {quickRepliesWithDescription?.map((reply, index) => ( + + ))} + {botCommands?.map((command, index) => ( + + ))} +
+ ); +}; + +export default memo(freezeWhenClosed(ChatCommandTooltip)); diff --git a/src/components/middle/composer/hooks/useBotCommandTooltip.ts b/src/components/middle/composer/hooks/useChatCommandTooltip.ts similarity index 62% rename from src/components/middle/composer/hooks/useBotCommandTooltip.ts rename to src/components/middle/composer/hooks/useChatCommandTooltip.ts index c045661f9..dcc3b37e7 100644 --- a/src/components/middle/composer/hooks/useBotCommandTooltip.ts +++ b/src/components/middle/composer/hooks/useChatCommandTooltip.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from '../../../../lib/teact/teact'; -import type { ApiBotCommand } from '../../../../api/types'; +import type { ApiBotCommand, ApiQuickReply } from '../../../../api/types'; import type { Signal } from '../../../../util/signals'; import { prepareForRegExp } from '../helpers/prepareForRegExp'; @@ -13,13 +13,15 @@ const RE_COMMAND = /^\/([\w@]{1,32})?$/i; const THROTTLE = 300; -export default function useBotCommandTooltip( +export default function useChatCommandTooltip( isEnabled: boolean, getHtml: Signal, botCommands?: ApiBotCommand[] | false, chatBotCommands?: ApiBotCommand[], + quickReplies?: Record, ) { const [filteredBotCommands, setFilteredBotCommands] = useState(); + const [filteredQuickReplies, setFilteredQuickReplies] = useState(); const [isManuallyClosed, markManuallyClosed, unmarkManuallyClosed] = useFlag(false); const detectCommandThrottled = useThrottledResolver(() => { @@ -34,24 +36,34 @@ export default function useBotCommandTooltip( useEffect(() => { const command = getCommand(); const commands = botCommands || chatBotCommands; - if (!command || !commands) { + if (!command || (!commands && !quickReplies)) { setFilteredBotCommands(undefined); + setFilteredQuickReplies(undefined); return; } const filter = command.substring(1); - const nextFilteredBotCommands = commands.filter((c) => !filter || c.command.startsWith(filter)); + const nextFilteredBotCommands = commands?.filter((c) => !filter || c.command.startsWith(filter)); setFilteredBotCommands( nextFilteredBotCommands?.length ? nextFilteredBotCommands : undefined, ); - }, [getCommand, botCommands, chatBotCommands]); + + const newFilteredQuickReplies = Object.values(quickReplies || {}).filter((quickReply) => ( + !filter || quickReply.shortcut.startsWith(filter) + )); + + setFilteredQuickReplies( + newFilteredQuickReplies?.length ? newFilteredQuickReplies : undefined, + ); + }, [getCommand, botCommands, chatBotCommands, quickReplies]); useEffect(unmarkManuallyClosed, [unmarkManuallyClosed, getHtml]); return { - isOpen: Boolean(filteredBotCommands?.length && !isManuallyClosed), + isOpen: Boolean((filteredBotCommands?.length || filteredQuickReplies?.length) && !isManuallyClosed), close: markManuallyClosed, filteredBotCommands, + filteredQuickReplies, }; } diff --git a/src/components/middle/composer/hooks/useKeyboardNavigation.ts b/src/components/middle/composer/hooks/useKeyboardNavigation.ts index 8be089c8b..6f61186a0 100644 --- a/src/components/middle/composer/hooks/useKeyboardNavigation.ts +++ b/src/components/middle/composer/hooks/useKeyboardNavigation.ts @@ -60,6 +60,10 @@ export function useKeyboardNavigation({ return true; }); + useEffect(() => { + if (!isActive) setSelectedItemIndex(shouldRemoveSelectionOnReset ? -1 : 0); + }, [isActive, shouldRemoveSelectionOnReset]); + const isSelectionOutOfRange = !items || selectedItemIndex > items.length - 1; useEffect(() => { if (!shouldSaveSelectionOnUpdateItems || isSelectionOutOfRange) { diff --git a/src/components/middle/helpers/groupMessages.ts b/src/components/middle/helpers/groupMessages.ts index c017e591c..f4ab6469f 100644 --- a/src/components/middle/helpers/groupMessages.ts +++ b/src/components/middle/helpers/groupMessages.ts @@ -2,7 +2,7 @@ import type { ApiMessage } from '../../../api/types'; import type { IAlbum } from '../../../types'; import { isActionMessage } from '../../../global/helpers'; -import { getDayStartAt } from '../../../util/dateFormat'; +import { getDayStartAt } from '../../../util/date/dateFormat'; type SenderGroup = (ApiMessage | IAlbum)[]; diff --git a/src/components/middle/message/BaseStory.tsx b/src/components/middle/message/BaseStory.tsx index 108cec1ac..39f1fdbf9 100644 --- a/src/components/middle/message/BaseStory.tsx +++ b/src/components/middle/message/BaseStory.tsx @@ -5,7 +5,7 @@ import type { ApiMessageStoryData, ApiTypeStory } from '../../../api/types'; import { getStoryMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../../util/windowEnvironment'; import useAppLayout from '../../../hooks/useAppLayout'; diff --git a/src/components/middle/message/Giveaway.tsx b/src/components/middle/message/Giveaway.tsx index ddc5d0a2f..493e6d7ae 100644 --- a/src/components/middle/message/Giveaway.tsx +++ b/src/components/middle/message/Giveaway.tsx @@ -17,7 +17,7 @@ import { selectGiftStickerForDuration, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatDateAtTime, formatDateTimeToString } from '../../../util/dateFormat'; +import { formatDateAtTime, formatDateTimeToString } from '../../../util/date/dateFormat'; import { isoToEmoji } from '../../../util/emoji/emoji'; import { getServerTime } from '../../../util/serverTime'; import { callApi } from '../../../api/gramjs'; diff --git a/src/components/middle/message/InvoiceMediaPreview.tsx b/src/components/middle/message/InvoiceMediaPreview.tsx index fb94178b2..2be5b4633 100644 --- a/src/components/middle/message/InvoiceMediaPreview.tsx +++ b/src/components/middle/message/InvoiceMediaPreview.tsx @@ -6,7 +6,7 @@ import type { ApiMessage } from '../../../api/types'; import { getMessageInvoice } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import { formatCurrency } from '../../../util/formatCurrency'; import useInterval from '../../../hooks/schedulers/useInterval'; diff --git a/src/components/middle/message/Location.tsx b/src/components/middle/message/Location.tsx index 184ff4695..3c7ebc1ae 100644 --- a/src/components/middle/message/Location.tsx +++ b/src/components/middle/message/Location.tsx @@ -14,7 +14,7 @@ import { isGeoLiveExpired, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatCountdownShort, formatLastUpdated } from '../../../util/dateFormat'; +import { formatCountdownShort, formatLastUpdated } from '../../../util/date/dateFormat'; import { getMetersPerPixel, getVenueColor, getVenueIconUrl, } from '../../../util/map'; @@ -27,6 +27,7 @@ import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import usePrevious from '../../../hooks/usePrevious'; +import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio'; import Avatar from '../../common/Avatar'; import Skeleton from '../../ui/placeholder/Skeleton'; @@ -42,7 +43,6 @@ const DEFAULT_MAP_CONFIG = { width: 400, height: 300, zoom: 16, - scale: 2, }; type OwnProps = { @@ -76,11 +76,10 @@ const Location: FC = ({ const [point, setPoint] = useState(geo); const shouldRenderText = type === 'venue' || (type === 'geoLive' && !isExpired); - const { - width, height, zoom, scale, - } = DEFAULT_MAP_CONFIG; + const { width, height, zoom } = DEFAULT_MAP_CONFIG; + const dpr = useDevicePixelRatio(); - const mediaHash = buildStaticMapHash(point, width, height, zoom, scale); + const mediaHash = buildStaticMapHash(point, width, height, zoom, dpr); const mediaBlobUrl = useMedia(mediaHash); const prevMediaBlobUrl = usePrevious(mediaBlobUrl, true); const mapBlobUrl = mediaBlobUrl || prevMediaBlobUrl; diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx index 92703fb7b..8de90e32b 100644 --- a/src/components/middle/message/MessageMeta.tsx +++ b/src/components/middle/message/MessageMeta.tsx @@ -7,7 +7,7 @@ import type { } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { formatDateTimeToString, formatPastTimeShort, formatTime } from '../../../util/dateFormat'; +import { formatDateTimeToString, formatPastTimeShort, formatTime } from '../../../util/date/dateFormat'; import { formatIntegerCompact } from '../../../util/textFormat'; import renderText from '../../common/helpers/renderText'; diff --git a/src/components/middle/message/MessagePhoneCall.tsx b/src/components/middle/message/MessagePhoneCall.tsx index 5a5e60d12..d4d90baea 100644 --- a/src/components/middle/message/MessagePhoneCall.tsx +++ b/src/components/middle/message/MessagePhoneCall.tsx @@ -5,7 +5,7 @@ import { getActions } from '../../../global'; import type { ApiMessage, PhoneCallAction } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { formatTime, formatTimeDuration } from '../../../util/dateFormat'; +import { formatTime, formatTimeDuration } from '../../../util/date/dateFormat'; import { ARE_CALLS_SUPPORTED } from '../../../util/windowEnvironment'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/middle/message/Poll.tsx b/src/components/middle/message/Poll.tsx index 02ce51f7b..f7a211e48 100644 --- a/src/components/middle/message/Poll.tsx +++ b/src/components/middle/message/Poll.tsx @@ -14,7 +14,7 @@ import type { } from '../../../api/types'; import type { LangFn } from '../../../hooks/useLang'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import renderText from '../../common/helpers/renderText'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; diff --git a/src/components/middle/message/ReadTimeMenuItem.tsx b/src/components/middle/message/ReadTimeMenuItem.tsx index b5984efbb..828a5f437 100644 --- a/src/components/middle/message/ReadTimeMenuItem.tsx +++ b/src/components/middle/message/ReadTimeMenuItem.tsx @@ -3,7 +3,7 @@ import { getActions } from '../../../lib/teact/teactn'; import type { ApiMessage } from '../../../api/types'; -import { formatDateAtTime } from '../../../util/dateFormat'; +import { formatDateAtTime } from '../../../util/date/dateFormat'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index 3c4cdbd64..1d1f13861 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -16,7 +16,7 @@ import { } from '../../../global/helpers'; import { stopCurrentAudio } from '../../../util/audioPlayer'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import safePlay from '../../../util/safePlay'; import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions'; diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index e7c865f82..3a40b638d 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -16,7 +16,7 @@ import { isOwnMessage, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatMediaDuration } from '../../../util/dateFormat'; +import { formatMediaDuration } from '../../../util/date/dateFormat'; import * as mediaLoader from '../../../util/mediaLoader'; import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions'; import { MIN_MEDIA_HEIGHT } from './helpers/mediaDimensions'; diff --git a/src/components/modals/boost/BoostModal.tsx b/src/components/modals/boost/BoostModal.tsx index 3de495118..2345ac6d8 100644 --- a/src/components/modals/boost/BoostModal.tsx +++ b/src/components/modals/boost/BoostModal.tsx @@ -7,7 +7,7 @@ import type { TabState } from '../../../global/types'; import { getChatTitle, isChatAdmin, isChatChannel } from '../../../global/helpers'; import { selectChat, selectChatFullInfo, selectIsCurrentUserPremium } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatDateInFuture } from '../../../util/dateFormat'; +import { formatDateInFuture } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; import renderText from '../../common/helpers/renderText'; diff --git a/src/components/modals/giftcode/GiftCodeModal.tsx b/src/components/modals/giftcode/GiftCodeModal.tsx index dd29a2140..3ede0acab 100644 --- a/src/components/modals/giftcode/GiftCodeModal.tsx +++ b/src/components/modals/giftcode/GiftCodeModal.tsx @@ -7,7 +7,7 @@ import type { TabState } from '../../../global/types'; import { TME_LINK_PREFIX } from '../../../config'; import { selectChatMessage, selectSender } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatDateTimeToString } from '../../../util/dateFormat'; +import { formatDateTimeToString } from '../../../util/date/dateFormat'; import renderText from '../../common/helpers/renderText'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index 62619fd44..217d16084 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -35,6 +35,15 @@ .FloatingActionButton { z-index: 1; } + + .business-location { + width: 4rem; + height: 4rem; + object-fit: cover; + border-radius: 0.25rem; + flex-shrink: 0; + margin-inline-start: 0.25rem; + } } .shared-media { diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx index 2c6fcaf2e..9b7a4b7b2 100644 --- a/src/components/right/RightHeader.tsx +++ b/src/components/right/RightHeader.tsx @@ -22,7 +22,7 @@ import { selectUser, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { getDayStartAt } from '../../util/dateFormat'; +import { getDayStartAt } from '../../util/date/dateFormat'; import { debounce } from '../../util/schedulers'; import useAppLayout from '../../hooks/useAppLayout'; diff --git a/src/components/right/management/JoinRequest.tsx b/src/components/right/management/JoinRequest.tsx index a7b944c08..419c67b00 100644 --- a/src/components/right/management/JoinRequest.tsx +++ b/src/components/right/management/JoinRequest.tsx @@ -7,7 +7,7 @@ import type { ApiUser } from '../../../api/types'; import { getUserFullName } from '../../../global/helpers'; import { selectUser } from '../../../global/selectors'; import { createClassNameBuilder } from '../../../util/buildClassName'; -import { formatHumanDate, formatTime, isToday } from '../../../util/dateFormat'; +import { formatHumanDate, formatTime, isToday } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/right/management/ManageInvite.tsx b/src/components/right/management/ManageInvite.tsx index 421b0c5ab..de52b631d 100644 --- a/src/components/right/management/ManageInvite.tsx +++ b/src/components/right/management/ManageInvite.tsx @@ -7,7 +7,7 @@ import type { ApiExportedInvite } from '../../../api/types'; import { ManagementScreens } from '../../../types'; import { selectTabState } from '../../../global/selectors'; -import { formatFullDate, formatTime } from '../../../util/dateFormat'; +import { formatFullDate, formatTime } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import useFlag from '../../../hooks/useFlag'; diff --git a/src/components/right/management/ManageInviteInfo.tsx b/src/components/right/management/ManageInviteInfo.tsx index 88dac22b4..99222065e 100644 --- a/src/components/right/management/ManageInviteInfo.tsx +++ b/src/components/right/management/ManageInviteInfo.tsx @@ -7,7 +7,7 @@ import type { ApiChatInviteImporter, ApiExportedInvite, ApiUser } from '../../.. import { isChatChannel } from '../../../global/helpers'; import { selectChat, selectTabState } from '../../../global/selectors'; import { copyTextToClipboard } from '../../../util/clipboard'; -import { formatFullDate, formatMediaDateTime, formatTime } from '../../../util/dateFormat'; +import { formatFullDate, formatMediaDateTime, formatTime } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import useHistoryBack from '../../../hooks/useHistoryBack'; diff --git a/src/components/right/management/ManageInvites.tsx b/src/components/right/management/ManageInvites.tsx index b67e8de73..0e4f84185 100644 --- a/src/components/right/management/ManageInvites.tsx +++ b/src/components/right/management/ManageInvites.tsx @@ -11,7 +11,7 @@ import { STICKER_SIZE_INVITES, TME_LINK_PREFIX } from '../../../config'; import { getMainUsername, isChatChannel } from '../../../global/helpers'; import { selectChat, selectTabState } from '../../../global/selectors'; import { copyTextToClipboard } from '../../../util/clipboard'; -import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/dateFormat'; +import { formatCountdown, MILLISECONDS_IN_DAY } from '../../../util/date/dateFormat'; import { getServerTime } from '../../../util/serverTime'; import { LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; diff --git a/src/components/right/statistics/BoostStatistics.tsx b/src/components/right/statistics/BoostStatistics.tsx index 34ad5b124..844a02555 100644 --- a/src/components/right/statistics/BoostStatistics.tsx +++ b/src/components/right/statistics/BoostStatistics.tsx @@ -6,7 +6,7 @@ import type { TabState } from '../../../global/types'; import { selectTabState } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; -import { formatDateAtTime } from '../../../util/dateFormat'; +import { formatDateAtTime } from '../../../util/date/dateFormat'; import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/right/statistics/StatisticsOverview.tsx b/src/components/right/statistics/StatisticsOverview.tsx index c016f703e..2718d35c8 100644 --- a/src/components/right/statistics/StatisticsOverview.tsx +++ b/src/components/right/statistics/StatisticsOverview.tsx @@ -7,7 +7,7 @@ import type { } from '../../../api/types'; import buildClassName from '../../../util/buildClassName'; -import { formatFullDate } from '../../../util/dateFormat'; +import { formatFullDate } from '../../../util/date/dateFormat'; import { formatInteger, formatIntegerCompact } from '../../../util/textFormat'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/right/statistics/StatisticsRecentMessage.tsx b/src/components/right/statistics/StatisticsRecentMessage.tsx index 07340b852..4abcc47e7 100644 --- a/src/components/right/statistics/StatisticsRecentMessage.tsx +++ b/src/components/right/statistics/StatisticsRecentMessage.tsx @@ -12,7 +12,7 @@ import { getMessageVideo, } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatDateTimeToString } from '../../../util/dateFormat'; +import { formatDateTimeToString } from '../../../util/date/dateFormat'; import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import useLang from '../../../hooks/useLang'; diff --git a/src/components/right/statistics/StatisticsRecentStory.tsx b/src/components/right/statistics/StatisticsRecentStory.tsx index 195c81dc4..3ca942245 100644 --- a/src/components/right/statistics/StatisticsRecentStory.tsx +++ b/src/components/right/statistics/StatisticsRecentStory.tsx @@ -10,7 +10,7 @@ import type { LangFn } from '../../../hooks/useLang'; import { getStoryMediaHash } from '../../../global/helpers'; import buildClassName from '../../../util/buildClassName'; -import { formatDateTimeToString } from '../../../util/dateFormat'; +import { formatDateTimeToString } from '../../../util/date/dateFormat'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; diff --git a/src/components/story/Story.tsx b/src/components/story/Story.tsx index 18615f888..d4a86717a 100644 --- a/src/components/story/Story.tsx +++ b/src/components/story/Story.tsx @@ -26,7 +26,7 @@ import { } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import captureKeyboardListeners from '../../util/captureKeyboardListeners'; -import { formatMediaDuration, formatRelativeTime } from '../../util/dateFormat'; +import { formatMediaDuration, formatRelativeTime } from '../../util/date/dateFormat'; import download from '../../util/download'; import { round } from '../../util/math'; import { getServerTime } from '../../util/serverTime'; diff --git a/src/components/story/StoryView.tsx b/src/components/story/StoryView.tsx index 8173d1368..d8307b3ec 100644 --- a/src/components/story/StoryView.tsx +++ b/src/components/story/StoryView.tsx @@ -9,7 +9,7 @@ import type { IconName } from '../../types/icons'; import { getUserFullName, isUserId } from '../../global/helpers'; import { selectPeer } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; -import { formatDateAtTime } from '../../util/dateFormat'; +import { formatDateAtTime } from '../../util/date/dateFormat'; import { REM } from '../common/helpers/mediaDimensions'; import useLang from '../../hooks/useLang'; diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index 3e416e484..3ec8f08d9 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -92,6 +92,12 @@ } } + &.with-color-transition { + .ListItem-button { + transition: background-color 150ms ease-in-out; + } + } + .title, .subtitle { line-height: 1.5rem; } diff --git a/src/components/ui/ListItem.tsx b/src/components/ui/ListItem.tsx index 76babf7d1..6d0be7603 100644 --- a/src/components/ui/ListItem.tsx +++ b/src/components/ui/ListItem.tsx @@ -61,6 +61,8 @@ interface OwnProps { destructive?: boolean; multiline?: boolean; isStatic?: boolean; + allowSelection?: boolean; + withColorTransition?: boolean; contextActions?: MenuItemContextAction[]; withPortalForMenu?: boolean; menuBubbleClassName?: string; @@ -95,6 +97,8 @@ const ListItem: FC = ({ destructive, multiline, isStatic, + allowSelection, + withColorTransition, contextActions, withPortalForMenu, href, @@ -202,7 +206,7 @@ const ListItem: FC = ({ const fullClassName = buildClassName( 'ListItem', className, - isStatic && 'allow-selection', + allowSelection && 'allow-selection', ripple && 'has-ripple', narrow && 'narrow', disabled && 'disabled', @@ -213,6 +217,7 @@ const ListItem: FC = ({ destructive && 'destructive', multiline && 'multiline', isStatic && 'is-static', + withColorTransition && 'with-color-transition', ); const ButtonElementTag = href ? 'a' : 'div'; @@ -236,15 +241,15 @@ const ListItem: FC = ({ onMouseDown={handleMouseDown} onContextMenu={onContextMenu || ((!inactive && contextActions) ? handleContextMenu : undefined)} > + {!disabled && !inactive && ripple && ( + + )} {leftElement} {icon && ( )} {multiline && (
{children}
)} {!multiline && children} - {!disabled && !inactive && ripple && ( - - )} {secondaryIcon && (