Support displaying business profiles (#4407)
This commit is contained in:
parent
75f6b47692
commit
bd3afbca75
@ -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) {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
}: {
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -222,6 +222,12 @@ export interface ApiPeerColors {
|
||||
generalHash?: number;
|
||||
}
|
||||
|
||||
export interface ApiTimezone {
|
||||
id: string;
|
||||
name: string;
|
||||
utcOffset: number;
|
||||
}
|
||||
|
||||
export interface GramJsEmojiInteraction {
|
||||
v: number;
|
||||
a: {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
1
src/assets/font-icons/clock.svg
Normal file
1
src/assets/font-icons/clock.svg
Normal 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 |
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
78
src/components/common/BusinessHours.module.scss
Normal file
78
src/components/common/BusinessHours.module.scss
Normal 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);
|
||||
}
|
||||
198
src/components/common/BusinessHours.tsx
Normal file
198
src/components/common/BusinessHours.tsx
Normal 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);
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -248,6 +248,7 @@ const SettingsFoldersMain: FC<OwnProps & StateProps> = ({
|
||||
inactive
|
||||
multiline
|
||||
isStatic
|
||||
allowSelection
|
||||
>
|
||||
<span className="title">
|
||||
{folder.title}
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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);
|
||||
@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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;
|
||||
@ -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);
|
||||
58
src/components/middle/composer/ChatCommand.tsx
Normal file
58
src/components/middle/composer/ChatCommand.tsx
Normal 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);
|
||||
18
src/components/middle/composer/ChatCommandTooltip.async.tsx
Normal file
18
src/components/middle/composer/ChatCommandTooltip.async.tsx
Normal 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;
|
||||
@ -1,4 +1,4 @@
|
||||
.BotCommandTooltip {
|
||||
.root {
|
||||
width: calc(100% - 4rem);
|
||||
max-width: 26rem;
|
||||
flex-direction: column;
|
||||
169
src/components/middle/composer/ChatCommandTooltip.tsx
Normal file
169
src/components/middle/composer/ChatCommandTooltip.tsx
Normal 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));
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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)[];
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -92,6 +92,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.with-color-transition {
|
||||
.ListItem-button {
|
||||
transition: background-color 150ms ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.title, .subtitle {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;`}
|
||||
/>
|
||||
))}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -138,6 +138,11 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
byChatId: {},
|
||||
},
|
||||
|
||||
quickReplies: {
|
||||
byId: {},
|
||||
messagesById: {},
|
||||
},
|
||||
|
||||
chatFolders: {
|
||||
byId: {},
|
||||
invites: {},
|
||||
|
||||
@ -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]),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
) {
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user