Support displaying business profiles (#4407)

This commit is contained in:
Alexander Zinchuk 2024-04-19 13:37:34 +04:00
parent 75f6b47692
commit bd3afbca75
112 changed files with 2072 additions and 578 deletions

View File

@ -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) {

View File

@ -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,
};
}

View File

@ -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<ApiUserFullInfo>({
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,
})),
};
}

View File

@ -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);

View File

@ -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,

View File

@ -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,
}: {

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -222,6 +222,12 @@ export interface ApiPeerColors {
generalHash?: number;
}
export interface ApiTimezone {
id: string;
name: string;
utcOffset: number;
}
export interface GramJsEmojiInteraction {
v: number;
a: {

View File

@ -24,6 +24,7 @@ import type {
ApiMessageExtendedMediaPreview,
ApiPhoto,
ApiPoll,
ApiQuickReply,
ApiReaction,
ApiReactions,
ApiStickerSet,
@ -234,6 +235,28 @@ export type ApiUpdateScheduledMessage = {
message: Partial<ApiMessage>;
};
export type ApiUpdateQuickReplyMessage = {
'@type': 'updateQuickReplyMessage';
id: number;
message: Partial<ApiMessage>;
};
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;

View File

@ -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[];
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M16 1c8.284 0 15 6.716 15 15 0 8.284-6.716 15-15 15-8.284 0-15-6.716-15-15C1 7.716 7.716 1 16 1Zm0 2.727a12.273 12.273 0 1 0 0 24.546 12.273 12.273 0 0 0 0-24.546Zm-1.259 13.046-.049-.083-.108-.262-.045-.201-.014-.114-.004-8.068a1.478 1.478 0 0 1 2.94-.218l.015.218v7.344L21.59 19.5c.52.52.571 1.33.155 1.907l-.155.184a1.478 1.478 0 0 1-1.906.155l-.184-.155-4.618-4.623-.074-.094-.067-.1z" style="stroke-width:1.34941"/></svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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);
}

View File

@ -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<HTMLDivElement>(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<number, string[]> = {};
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<HTMLElement>(`.${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<HTMLElement>(`.${TO_SLIDE_CLASS_NAME} .${styles.timetable}`)!;
requestMeasure(() => {
const height = slide.offsetHeight;
requestMutation(() => {
transitionRef.current!.style.height = `${height}px`;
});
});
});
return (
<ListItem
icon="clock"
iconClassName={styles.icon}
multiline
className={styles.root}
isStatic={isExpanded}
ripple
withColorTransition
onClick={handleClick}
>
<div className={styles.top}>
<div className={styles.left}>
<div>{lang('BusinessHoursProfile')}</div>
<div className={buildClassName(styles.status, isBusinessOpen && styles.statusOpen)}>
{isBusinessOpen ? lang('BusinessHoursProfileNowOpen') : lang('BusinessHoursProfileNowClosed')}
</div>
</div>
<Icon className={styles.arrow} name={isExpanded ? 'up' : 'down'} />
</div>
{isExpanded && (
<div className={styles.bottom}>
{Boolean(timezoneMinuteDifference) && (
<div
className={styles.offsetTrigger}
role="button"
tabIndex={0}
onMouseDown={!IS_TOUCH_ENV ? handleTriggerOffset : undefined}
onClick={IS_TOUCH_ENV ? handleTriggerOffset : undefined}
>
{lang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')}
</div>
)}
<Transition
className={styles.transition}
ref={transitionRef}
name="fade"
activeKey={Number(isMyTime)}
onStart={handleAnimationStart}
>
<dl className={styles.timetable}>
{DAYS.map((day) => (
<>
<dt className={buildClassName(styles.weekday, day === currentDay && styles.currentDay)}>
{formatWeekday(lang, day === 6 ? 0 : day + 1)}
</dt>
<dd className={styles.schedule}>
{workHours[day].map((segment) => (
<div>{segment}</div>
))}
</dd>
</>
))}
</dl>
</Transition>
</div>
)}
</ListItem>
);
};
export default memo(BusinessHours);

View File

@ -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';

View File

@ -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<OwnProps & StateProps> = ({
chatOrUserId,
user,
chat,
userFullInfo,
isInSettings,
canInviteUsers,
isMuted,
@ -78,12 +89,12 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
hasSavedMessages,
}) => {
const {
loadFullUser,
showNotification,
updateChatMutedState,
updateTopicMutedState,
loadPeerStories,
openSavedDialog,
openMapModal,
} = getActions();
const {
@ -94,6 +105,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
} = 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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
}, [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 <img src={locationBlobUrl} alt="" className="business-location" />;
}
return <Skeleton className="business-location" />;
}, [businessLocation, locationBlobUrl]);
const isTopicInfo = Boolean(topicId && topicId !== MAIN_THREAD_ID);
const shouldRenderAllLinks = (chat && isChatChannel(chat)) || user?.isPremium;
@ -137,6 +159,17 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
multiline
narrow
isStatic
allowSelection
>
<span className="title word-break allow-selection" dir="auto">
{
@ -280,6 +314,21 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
/>
</ListItem>
)}
{businessWorkHours && (
<BusinessHours businessHours={businessWorkHours} />
)}
{businessLocation && (
<ListItem
icon="location"
ripple
multiline
rightElement={locationRightComponent}
onClick={handleClickLocation}
>
<div className="title">{businessLocation.address}</div>
<span className="subtitle">{lang('BusinessProfileLocation')}</span>
</ListItem>
)}
{hasSavedMessages && !isInSettings && (
<ListItem icon="saved-messages" ripple onClick={handleOpenSavedDialog}>
<span>{lang('SavedMessagesTab')}</span>
@ -294,16 +343,18 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
phoneCodeList,
chat,
user,
userFullInfo,
canInviteUsers,
isMuted,
topicId,

View File

@ -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<number, ApiMessage>;
quickReplies?: Record<number, ApiQuickReply>;
canSendQuickReplies?: boolean;
};
enum MainButtonState {
@ -349,6 +353,9 @@ const Composer: FC<OwnProps & StateProps> = ({
sentStoryReaction,
stealthMode,
canSendOneTimeMedia,
quickReplyMessages,
quickReplies,
canSendQuickReplies,
onForward,
}) => {
const {
@ -659,18 +666,22 @@ const Composer: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onInsertUserName={insertMention}
onClose={closeMentionTooltip}
/>
<BotCommandTooltip
isOpen={isBotCommandTooltipOpen}
<ChatCommandTooltip
isOpen={isChatCommandTooltipOpen}
chatId={chatId}
withUsername={Boolean(chatBotCommands)}
botCommands={botTooltipCommands}
quickReplies={quickReplyCommands}
getHtml={getHtml}
self={currentUser!}
quickReplyMessages={quickReplyMessages}
onClick={handleBotCommandSelect}
onClose={closeBotCommandTooltip}
onClose={closeChatCommandTooltip}
/>
<div className={buildClassName('composer-wrapper', isInStoryViewer && 'with-story-tweaks')}>
<svg className="svg-appendix" width="9" height="20">
@ -2003,6 +2018,8 @@ export default memo(withGlobal<OwnProps>(
: 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<OwnProps>(
sentStoryReaction,
stealthMode: global.stories.stealthMode,
replyToTopic,
quickReplyMessages: global.quickReplies.messagesById,
quickReplies: global.quickReplies.byId,
canSendQuickReplies,
};
},
)(Composer));

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<OwnProps> = ({ status }) => {
<Transition name="reveal" activeKey={Keys[status]}>
{status === 'failed' ? (
<div className="MessageOutgoingStatus--failed">
<i className="icon icon-message-failed" />
<Icon name="message-failed" />
</div>
) : <i className={`icon icon-message-${status}`} />}
) : <Icon name={`message-${status}`} />}
</Transition>
</div>
);

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -248,6 +248,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
inactive
multiline
isStatic
allowSelection
>
<span className="title">
{folder.title}

View File

@ -271,6 +271,8 @@ const Main: FC<OwnProps & StateProps> = ({
loadAuthorizations,
loadPeerColors,
loadSavedReactionTags,
loadTimezones,
loadQuickReplies,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -348,6 +350,8 @@ const Main: FC<OwnProps & StateProps> = ({
loadFeaturedEmojiStickers();
loadAuthorizations();
loadSavedReactionTags();
loadTimezones();
loadQuickReplies();
}
}, [isMasterTab, isSynced]);

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<OwnProps> = ({
withAvatar,
focus,
botCommand,
bot,
onClick,
}) => {
return (
<ListItem
key={botCommand.command}
className={buildClassName('BotCommand chat-item-clickable scroll-item', withAvatar && 'with-avatar')}
multiline
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onClick(botCommand)}
focus={focus}
>
{withAvatar && (
<Avatar size="small" peer={bot} />
)}
<div className="content-inner">
<span className="title">/{botCommand.command}</span>
<span className="subtitle">{renderText(botCommand.description)}</span>
</div>
</ListItem>
);
};
export default memo(BotCommand);

View File

@ -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<OwnProps> = ({
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<OwnProps> = ({
noCompact
>
{botCommands.map((botCommand) => (
<BotCommand
<ChatCommand
key={botCommand.command}
botCommand={botCommand}
command={botCommand.command}
description={botCommand.description}
clickArg={botCommand.command}
onClick={handleClick}
/>
))}

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const BotCommandTooltip = useModuleLoader(Bundles.Extra, 'BotCommandTooltip', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return BotCommandTooltip ? <BotCommandTooltip {...props} /> : undefined;
};
export default BotCommandTooltipAsync;

View File

@ -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<string>;
onClick: NoneToVoidFunction;
onClose: NoneToVoidFunction;
};
const BotCommandTooltip: FC<OwnProps> = ({
isOpen,
withUsername,
botCommands,
getHtml,
onClick,
onClose,
}) => {
const { sendBotCommand } = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(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 (
<div className={className} ref={containerRef}>
{renderedCommands && renderedCommands.map((chatBotCommand, index) => (
<BotCommand
key={`${chatBotCommand.botId}_${chatBotCommand.command}`}
botCommand={chatBotCommand}
// No need for expensive global updates on users and chats, so we avoid them
bot={getGlobal().users.byId[chatBotCommand.botId]}
withAvatar
onClick={handleSendCommand}
focus={selectedCommandIndex === index}
/>
))}
</div>
);
};
export default memo(BotCommandTooltip);

View File

@ -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<T = undefined> = {
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 = <T,>({
withAvatar,
focus,
command,
description,
peer,
clickArg,
onClick,
}: OwnProps<T>) => {
const handleClick = useLastCallback(() => {
onClick(clickArg);
});
return (
<ListItem
key={command}
className={buildClassName('BotCommand chat-item-clickable scroll-item', withAvatar && 'with-avatar')}
multiline
onClick={handleClick}
focus={focus}
>
{withAvatar && (
<Avatar size="small" peer={peer} />
)}
<div className="content-inner">
<span className="title">/{command}</span>
<span className="subtitle">{renderText(description)}</span>
</div>
</ListItem>
);
};
export default memo(ChatCommand);

View File

@ -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<OwnProps> = (props) => {
const { isOpen } = props;
const ChatCommandTooltip = useModuleLoader(Bundles.Extra, 'ChatCommandTooltip', !isOpen);
// eslint-disable-next-line react/jsx-props-no-spreading
return ChatCommandTooltip ? <ChatCommandTooltip {...props} /> : undefined;
};
export default ChatCommandTooltipAsync;

View File

@ -1,4 +1,4 @@
.BotCommandTooltip {
.root {
width: calc(100% - 4rem);
max-width: 26rem;
flex-direction: column;

View File

@ -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<number, ApiMessage>;
self: ApiUser;
getHtml: Signal<string>;
onClick: NoneToVoidFunction;
onClose: NoneToVoidFunction;
};
type QuickReplyWithDescription = {
id: number;
command: string;
description: string;
};
const ChatCommandTooltip: FC<OwnProps> = ({
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<HTMLDivElement>(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 (
<div className={className} ref={containerRef}>
{quickRepliesWithDescription?.map((reply, index) => (
<ChatCommand
key={`quickReply_${reply.id}`}
command={reply.command}
description={reply.description}
peer={self}
withAvatar
clickArg={reply.id}
onClick={handleSendQuickReply}
focus={selectedCommandIndex === index}
/>
))}
{botCommands?.map((command, index) => (
<ChatCommand
key={`${command.botId}_${command.command}`}
command={command.command}
description={command.description}
// No need for expensive global updates on users and chats, so we avoid them
peer={getGlobal().users.byId[command.botId]}
withAvatar
clickArg={command}
onClick={handleSendCommand}
focus={selectedCommandIndex + (quickRepliesWithDescription?.length || 0) === index}
/>
))}
</div>
);
};
export default memo(freezeWhenClosed(ChatCommandTooltip));

View File

@ -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<string>,
botCommands?: ApiBotCommand[] | false,
chatBotCommands?: ApiBotCommand[],
quickReplies?: Record<number, ApiQuickReply>,
) {
const [filteredBotCommands, setFilteredBotCommands] = useState<ApiBotCommand[] | undefined>();
const [filteredQuickReplies, setFilteredQuickReplies] = useState<ApiQuickReply[] | undefined>();
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,
};
}

View File

@ -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) {

View File

@ -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)[];

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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<OwnProps> = ({
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;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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 {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -92,6 +92,12 @@
}
}
&.with-color-transition {
.ListItem-button {
transition: background-color 150ms ease-in-out;
}
}
.title, .subtitle {
line-height: 1.5rem;
}

View File

@ -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<OwnProps> = ({
destructive,
multiline,
isStatic,
allowSelection,
withColorTransition,
contextActions,
withPortalForMenu,
href,
@ -202,7 +206,7 @@ const ListItem: FC<OwnProps> = ({
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<OwnProps> = ({
destructive && 'destructive',
multiline && 'multiline',
isStatic && 'is-static',
withColorTransition && 'with-color-transition',
);
const ButtonElementTag = href ? 'a' : 'div';
@ -236,15 +241,15 @@ const ListItem: FC<OwnProps> = ({
onMouseDown={handleMouseDown}
onContextMenu={onContextMenu || ((!inactive && contextActions) ? handleContextMenu : undefined)}
>
{!disabled && !inactive && ripple && (
<RippleEffect />
)}
{leftElement}
{icon && (
<i className={buildClassName('icon', `icon-${icon}`, iconClassName)} />
)}
{multiline && (<div className="multiline-item">{children}</div>)}
{!multiline && children}
{!disabled && !inactive && ripple && (
<RippleEffect />
)}
{secondaryIcon && (
<Button
className="secondary-icon"

View File

@ -1,17 +1,3 @@
@-webkit-keyframes ripple-animation {
from {
transform: scale(0);
opacity: 1;
}
50% {
opacity: 1;
}
to {
opacity: 0;
transform: scale(2);
}
}
@keyframes ripple-animation {
from {
transform: scale(0);
@ -37,7 +23,7 @@
display: none;
}
span {
.ripple-wave {
position: absolute;
display: block;
background-color: var(--ripple-color, rgba(0, 0, 0, 0.08));

View File

@ -48,7 +48,8 @@ const RippleEffect: FC = () => {
return (
<div className="ripple-container" onMouseDown={handleMouseDown}>
{ripples.map(({ x, y, size }) => (
<span
<div
className="ripple-wave"
style={`left: ${x}px; top: ${y}px; width: ${size}px; height: ${size}px;`}
/>
))}

View File

@ -1,6 +1,6 @@
import React, { type FC, memo, useEffect } from '../../lib/teact/teact';
import { formatMediaDuration } from '../../util/dateFormat';
import { formatMediaDuration } from '../../util/date/dateFormat';
import { getServerTime } from '../../util/serverTime';
import useInterval from '../../hooks/schedulers/useInterval';

View File

@ -49,7 +49,7 @@ export const MEDIA_PROGRESSIVE_CACHE_DISABLED = false;
export const MEDIA_PROGRESSIVE_CACHE_NAME = 'tt-media-progressive';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v32';
export const LANG_CACHE_NAME = 'tt-lang-packs-v33';
export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';

View File

@ -4,7 +4,7 @@ import type {
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { GLOBAL_SEARCH_SLICE, GLOBAL_TOPIC_SEARCH_SLICE } from '../../../config';
import { timestampPlusDay } from '../../../util/dateFormat';
import { timestampPlusDay } from '../../../util/date/dateFormat';
import { isDeepLink, tryParseDeepLink } from '../../../util/deepLinkParser';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees';

View File

@ -69,7 +69,6 @@ import {
addUsers,
removeOutlyingList,
removeRequestedMessageTranslation,
replaceScheduledMessages,
replaceSettings,
replaceThreadParam,
replaceUserStatuses,
@ -78,15 +77,20 @@ import {
updateChat,
updateChatFullInfo,
updateChatMessage,
updateChats,
updateListedIds,
updateMessageTranslation,
updateOutlyingLists,
updateQuickReplies,
updateQuickReplyMessages,
updateRequestedMessageTranslation,
updateScheduledMessages,
updateSponsoredMessage,
updateThreadInfo,
updateThreadUnreadFromForwardedMessage,
updateTopic,
updateUploadByMessageKey,
updateUsers,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
@ -1083,7 +1087,7 @@ addActionHandler('loadScheduledHistory', async (global, actions, payload): Promi
const ids = Object.keys(byId).map(Number).sort((a, b) => b - a);
global = getGlobal();
global = replaceScheduledMessages(global, chat.id, byId);
global = updateScheduledMessages(global, chat.id, byId);
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
if (chat?.isForum) {
const scheduledPerThread: Record<ThreadId, number[]> = {};
@ -1902,6 +1906,31 @@ addActionHandler('loadOutboxReadDate', async (global, actions, payload): Promise
}
});
addActionHandler('loadQuickReplies', async (global): Promise<void> => {
const result = await callApi('fetchQuickReplies');
if (!result) return;
global = getGlobal();
global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = updateQuickReplyMessages(global, buildCollectionByKey(result.messages, 'id'));
global = updateQuickReplies(global, result.quickReplies);
setGlobal(global);
});
addActionHandler('sendQuickReply', (global, actions, payload): ActionReturnType => {
const { chatId, quickReplyId } = payload;
const chat = selectChat(global, chatId);
if (!chat) return global;
callApi('sendQuickReply', {
chat,
shortcutId: quickReplyId,
});
return global;
});
addActionHandler('copyMessageLink', async (global, actions, payload): Promise<void> => {
const {
chatId, messageId, shouldIncludeThread, shouldIncludeGrouped, tabId = getCurrentTabId(),

View File

@ -682,6 +682,22 @@ addActionHandler('loadPeerColors', async (global): Promise<void> => {
setGlobal(global);
});
addActionHandler('loadTimezones', async (global): Promise<void> => {
const hash = global.timezones?.hash;
const result = await callApi('fetchTimezones', hash);
if (!result) return;
global = getGlobal();
global = {
...global,
timezones: {
byId: buildCollectionByKey(result.timezones, 'id'),
hash: result.hash,
},
};
setGlobal(global);
});
addActionHandler('loadGlobalPrivacySettings', async (global): Promise<void> => {
const globalSettings = await callApi('fetchGlobalPrivacySettings');
if (!globalSettings) {

View File

@ -11,7 +11,9 @@ import { MAIN_THREAD_ID } from '../../../api/types';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { areDeepEqual } from '../../../util/areDeepEqual';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { omit, pickTruthy, unique } from '../../../util/iteratees';
import {
buildCollectionByKey, omit, pickTruthy, unique,
} from '../../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../../util/messageKey';
import { notifyAboutMessage } from '../../../util/notifications';
import { onTickEnd } from '../../../util/schedulers';
@ -27,6 +29,8 @@ import {
clearMessageTranslation,
deleteChatMessages,
deleteChatScheduledMessages,
deleteQuickReply,
deleteQuickReplyMessages,
deleteTopic,
removeChatFromChatLists,
replaceThreadParam,
@ -35,6 +39,8 @@ import {
updateChatMessage,
updateListedIds,
updateMessageTranslations,
updateQuickReplies,
updateQuickReplyMessage,
updateScheduledMessage,
updateThreadInfo,
updateThreadInfos,
@ -259,6 +265,39 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break;
}
case 'updateQuickReplyMessage': {
const { id, message } = update;
global = updateQuickReplyMessage(global, id, message);
setGlobal(global);
break;
}
case 'deleteQuickReplyMessages': {
const { messageIds } = update;
global = deleteQuickReplyMessages(global, messageIds);
setGlobal(global);
break;
}
case 'updateQuickReplies': {
const { quickReplies } = update;
const byId = buildCollectionByKey(quickReplies, 'id');
global = updateQuickReplies(global, byId);
setGlobal(global);
break;
}
case 'deleteQuickReply': {
global = deleteQuickReply(global, update.quickReplyId);
setGlobal(global);
break;
}
case 'updateMessageSendSucceeded': {
const { chatId, localId, message } = update;
@ -769,7 +808,11 @@ function updateReactions<T extends GlobalState>(
}
function updateWithLocalMedia(
global: RequiredGlobalState, chatId: string, id: number, messageUpdate: Partial<ApiMessage>, isScheduled = false,
global: RequiredGlobalState,
chatId: string,
id: number,
messageUpdate: Partial<ApiMessage>,
isScheduled = false,
) {
const currentMessage = isScheduled
? selectScheduledMessage(global, chatId, id)

View File

@ -223,6 +223,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.reactions) {
cached.reactions = initialState.reactions;
}
if (!cached.quickReplies) {
cached.quickReplies = initialState.quickReplies;
}
}
function updateCache(force?: boolean) {
@ -280,6 +284,7 @@ export function serializeGlobal<T extends GlobalState>(global: T) {
'recentlyFoundChatIds',
'peerColors',
'savedReactionTags',
'timezones',
]),
lastIsChatInfoShown: !getIsMobile() ? global.lastIsChatInfoShown : undefined,
customEmojis: reduceCustomEmojis(global),

View File

@ -16,7 +16,7 @@ import {
ANONYMOUS_USER_ID,
ARCHIVED_FOLDER_ID, CHANNEL_ID_LENGTH, GENERAL_TOPIC_ID, REPLIES_USER_ID, TME_LINK_PREFIX,
} from '../../config';
import { formatDateToString, formatTime } from '../../util/dateFormat';
import { formatDateToString, formatTime } from '../../util/date/dateFormat';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..';
import { getMainUsername, getUserFirstOrLastName } from './users';

View File

@ -2,7 +2,7 @@ import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types';
import type { LangFn } from '../../hooks/useLang';
import { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { formatFullDate, formatTime } from '../../util/dateFormat';
import { formatFullDate, formatTime } from '../../util/date/dateFormat';
import { orderBy } from '../../util/iteratees';
import { formatPhoneNumber } from '../../util/phoneNumber';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';

View File

@ -138,6 +138,11 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
byChatId: {},
},
quickReplies: {
byId: {},
messagesById: {},
},
chatFolders: {
byId: {},
invites: {},

View File

@ -1,4 +1,6 @@
import type { ApiMessage, ApiSponsoredMessage, ApiThreadInfo } from '../../api/types';
import type {
ApiMessage, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
} from '../../api/types';
import type { FocusDirection, ThreadId } from '../../types';
import type {
GlobalState, MessageList, MessageListType, TabArgs, TabThread, Thread,
@ -27,7 +29,9 @@ import {
selectMessageIdsByGroupId,
selectOutlyingLists,
selectPinnedIds,
selectQuickReplyMessage,
selectScheduledIds,
selectScheduledMessage,
selectTabState,
selectThreadIdFromMessage,
selectThreadInfo,
@ -231,8 +235,7 @@ export function updateChatMessage<T extends GlobalState>(
export function updateScheduledMessage<T extends GlobalState>(
global: T, chatId: string, messageId: number, messageUpdate: Partial<ApiMessage>,
): T {
const byId = selectChatScheduledMessages(global, chatId) || {};
const message = byId[messageId];
const message = selectScheduledMessage(global, chatId, messageId)!;
const updatedMessage = {
...message,
...messageUpdate,
@ -242,12 +245,43 @@ export function updateScheduledMessage<T extends GlobalState>(
return global;
}
return replaceScheduledMessages(global, chatId, {
...byId,
return updateScheduledMessages(global, chatId, {
[messageId]: updatedMessage,
});
}
export function updateQuickReplyMessage<T extends GlobalState>(
global: T, messageId: number, messageUpdate: Partial<ApiMessage>,
): T {
const message = selectQuickReplyMessage(global, messageId);
const updatedMessage = {
...message,
...messageUpdate,
};
if (!updatedMessage.id) {
return global;
}
return updateQuickReplyMessages(global, {
[messageId]: updatedMessage,
});
}
export function deleteQuickReplyMessages<T extends GlobalState>(
global: T, messageIds: number[],
): T {
const byId = global.quickReplies.messagesById;
const newById = omit(byId, messageIds);
return {
...global,
quickReplies: {
...global.quickReplies,
messagesById: newById,
},
};
}
export function deleteChatMessages<T extends GlobalState>(
global: T,
chatId: string,
@ -385,7 +419,7 @@ export function deleteChatScheduledMessages<T extends GlobalState>(
});
}
global = replaceScheduledMessages(global, chatId, newById);
global = updateScheduledMessages(global, chatId, newById);
return global;
}
@ -544,16 +578,8 @@ export function updateThreadInfos<T extends GlobalState>(
return global;
}
export function replaceScheduledMessages<T extends GlobalState>(
export function updateScheduledMessages<T extends GlobalState>(
global: T, chatId: string, newById: Record<number, ApiMessage>,
): T {
return updateScheduledMessages(global, chatId, {
byId: newById,
});
}
function updateScheduledMessages<T extends GlobalState>(
global: T, chatId: string, update: Partial<{ byId: Record<number, ApiMessage> }>,
): T {
const current = global.scheduledMessages.byChatId[chatId] || { byId: {}, hash: 0 };
@ -564,13 +590,28 @@ function updateScheduledMessages<T extends GlobalState>(
...global.scheduledMessages.byChatId,
[chatId]: {
...current,
...update,
...newById,
},
},
},
};
}
export function updateQuickReplyMessages<T extends GlobalState>(
global: T, update: Record<number, ApiMessage>,
): T {
return {
...global,
quickReplies: {
...global.quickReplies,
messagesById: {
...global.quickReplies.messagesById,
...update,
},
},
};
}
export function updateFocusedMessage<T extends GlobalState>({
global,
chatId,
@ -818,3 +859,32 @@ export function updateUploadByMessageKey<T extends GlobalState>(
},
};
}
export function updateQuickReplies<T extends GlobalState>(
global: T,
quickRepliesUpdate: Record<number, ApiQuickReply>,
) {
return {
...global,
quickReplies: {
...global.quickReplies,
byId: {
...global.quickReplies.byId,
...quickRepliesUpdate,
},
},
};
}
export function deleteQuickReply<T extends GlobalState>(
global: T,
quickReplyId: number,
) {
return {
...global,
quickReplies: {
...global.quickReplies,
byId: omit(global.quickReplies.byId, [quickReplyId]),
},
};
}

View File

@ -342,6 +342,10 @@ export function selectScheduledMessage<T extends GlobalState>(global: T, chatId:
return chatMessages ? chatMessages[messageId] : undefined;
}
export function selectQuickReplyMessage<T extends GlobalState>(global: T, messageId: number) {
return global.quickReplies.messagesById[messageId];
}
export function selectEditingMessage<T extends GlobalState>(
global: T, chatId: string, threadId: ThreadId, messageListType: MessageListType,
) {

View File

@ -45,6 +45,7 @@ import type {
ApiPhoto,
ApiPostStatistics,
ApiPremiumPromo,
ApiQuickReply,
ApiReaction,
ApiReactionKey,
ApiReceipt,
@ -60,6 +61,7 @@ import type {
ApiStickerSetInfo,
ApiThemeParameters,
ApiThreadInfo,
ApiTimezone,
ApiTranscription,
ApiTypeStoryView,
ApiTypingStatus,
@ -701,6 +703,10 @@ export type GlobalState = {
config?: ApiConfig;
appConfig?: ApiAppConfig;
peerColors?: ApiPeerColors;
timezones?: {
byId: Record<string, ApiTimezone>;
hash: number;
};
hasWebAuthTokenFailed?: boolean;
hasWebAuthTokenPasswordRequired?: true;
isCacheApiSupported?: boolean;
@ -862,6 +868,11 @@ export type GlobalState = {
}>;
};
quickReplies: {
messagesById: Record<number, ApiMessage>;
byId: Record<number, ApiQuickReply>;
};
chatFolders: {
orderedIds?: number[];
byId: Record<number, ApiChatFolder>;
@ -2044,6 +2055,11 @@ export interface ActionPayloads {
chatId: string;
messageId: number;
};
loadQuickReplies: undefined;
sendQuickReply: {
chatId: string;
quickReplyId: number;
};
animateUnreadReaction: {
messageIds: number[];
} & WithTabId;
@ -2859,6 +2875,7 @@ export interface ActionPayloads {
hash: number;
} | undefined;
loadPeerColors: undefined;
loadTimezones: undefined;
requestNextSettingsScreen: {
screen?: SettingsScreens;
foldersAction?: ReducerAction<FoldersActions>;

Some files were not shown because too many files have changed in this diff Show More