From 64799a88185cc387e844819503de6036dfdc018d Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 23 Feb 2024 14:05:43 +0100 Subject: [PATCH] MessageContextMenu: Read time in private chats (#4218) --- src/api/gramjs/apiBuilders/appConfig.ts | 2 + src/api/gramjs/apiBuilders/users.ts | 6 +- src/api/gramjs/methods/index.ts | 1 + src/api/gramjs/methods/messages.ts | 14 +++ src/api/gramjs/methods/settings.ts | 6 +- src/api/types/messages.ts | 1 + src/api/types/misc.ts | 1 + src/api/types/users.ts | 2 + src/assets/tgs/ReadTime.tgs | Bin 0 -> 9552 bytes src/bundles/extra.ts | 1 + .../common/ReadDateModal.module.scss | 43 +++++++ src/components/common/ReadDateModal.tsx | 106 ++++++++++++++++++ src/components/common/ReadTimeModal.async.tsx | 18 +++ .../common/helpers/animatedAssets.ts | 2 + .../SettingsPrivacyLastSeen.module.scss | 3 + .../left/settings/SettingsPrivacyLastSeen.tsx | 84 ++++++++++++++ .../settings/SettingsPrivacyVisibility.tsx | 4 + src/components/middle/MiddleColumn.tsx | 7 +- .../middle/message/ContextMenuContainer.tsx | 35 +++++- src/components/middle/message/Giveaway.tsx | 3 +- .../middle/message/MessageContextMenu.tsx | 83 ++++++++------ .../message/ReadTimeMenuItem.module.scss | 44 ++++++++ .../middle/message/ReadTimeMenuItem.tsx | 62 ++++++++++ src/components/ui/MenuSeparator.module.scss | 13 ++- src/components/ui/MenuSeparator.tsx | 5 +- src/components/ui/Modal.tsx | 2 +- src/components/ui/Separator.module.scss | 33 ++++++ src/components/ui/Separator.tsx | 27 +++++ src/global/actions/api/messages.ts | 41 +++++++ src/global/actions/api/settings.ts | 17 ++- src/global/actions/ui/messages.ts | 16 +++ src/global/initialState.ts | 1 + src/global/selectors/settings.ts | 4 + src/global/types.ts | 16 ++- src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/styles/_variables.scss | 1 + src/styles/index.scss | 1 + src/styles/themes.json | 3 +- src/types/index.ts | 1 + 40 files changed, 657 insertions(+), 54 deletions(-) create mode 100644 src/assets/tgs/ReadTime.tgs create mode 100644 src/components/common/ReadDateModal.module.scss create mode 100644 src/components/common/ReadDateModal.tsx create mode 100644 src/components/common/ReadTimeModal.async.tsx create mode 100644 src/components/left/settings/SettingsPrivacyLastSeen.module.scss create mode 100644 src/components/left/settings/SettingsPrivacyLastSeen.tsx create mode 100644 src/components/middle/message/ReadTimeMenuItem.module.scss create mode 100644 src/components/middle/message/ReadTimeMenuItem.tsx create mode 100644 src/components/ui/Separator.module.scss create mode 100644 src/components/ui/Separator.tsx diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index ffc39247e..7e723b251 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -45,6 +45,7 @@ export interface GramJsAppConfig extends LimitsConfig { reactions_uniq_max: number; chat_read_mark_size_threshold: number; chat_read_mark_expire_period: number; + pm_read_date_expire_period: number; reactions_user_max_default: number; reactions_user_max_premium: number; autologin_domains: string[]; @@ -98,6 +99,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp emojiSounds: buildEmojiSounds(appConfig), seenByMaxChatMembers: appConfig.chat_read_mark_size_threshold, seenByExpiresAt: appConfig.chat_read_mark_expire_period, + readDateExpiresAt: appConfig.pm_read_date_expire_period, autologinDomains: appConfig.autologin_domains || [], urlAuthDomains: appConfig.url_auth_domains || [], maxUniqueReactions: appConfig.reactions_uniq_max, diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index e4ca27ea2..e42696f67 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -108,11 +108,11 @@ export function buildApiUserStatus(mtpStatus?: GramJs.TypeUserStatus): ApiUserSt } else if (mtpStatus instanceof GramJs.UserStatusOffline) { return { type: 'userStatusOffline', wasOnline: mtpStatus.wasOnline }; } else if (mtpStatus instanceof GramJs.UserStatusRecently) { - return { type: 'userStatusRecently' }; + return { type: 'userStatusRecently', isReadDateRestrictedByMe: mtpStatus.byMe }; } else if (mtpStatus instanceof GramJs.UserStatusLastWeek) { - return { type: 'userStatusLastWeek' }; + return { type: 'userStatusLastWeek', isReadDateRestrictedByMe: mtpStatus.byMe }; } else { - return { type: 'userStatusLastMonth' }; + return { type: 'userStatusLastMonth', isReadDateRestrictedByMe: mtpStatus.byMe }; } } diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 9206476a4..f3584a061 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -35,6 +35,7 @@ export { reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, + fetchOutboxReadDate, deleteSavedHistory, } from './messages'; diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index c5c04a936..d9d055efd 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1868,3 +1868,17 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU handleGramJsUpdate(update); } + +export async function fetchOutboxReadDate({ chat, messageId }: { chat: ApiChat; messageId: number }) { + const { id, accessHash } = chat; + const peer = buildInputPeer(id, accessHash); + + const result = await invokeRequest(new GramJs.messages.GetOutboxReadDate({ + peer: peer as GramJs.TypeInputPeer, + msgId: messageId, + }), { shouldThrow: true }); + + if (!result) return undefined; + + return { date: result.date }; +} diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index 6931335eb..d69d35fb9 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -614,15 +614,18 @@ export async function fetchGlobalPrivacySettings() { return { shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers), + shouldHideReadMarks: Boolean(result.hideReadMarks), }; } -export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonContact }: { +export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonContact, shouldHideReadMarks }: { shouldArchiveAndMuteNewNonContact: boolean; + shouldHideReadMarks: boolean; }) { const result = await invokeRequest(new GramJs.account.SetGlobalPrivacySettings({ settings: new GramJs.GlobalPrivacySettings({ ...(shouldArchiveAndMuteNewNonContact && { archiveAndMuteNewNoncontactPeers: true }), + ...(shouldHideReadMarks && { hideReadMarks: true }), }), })); @@ -632,6 +635,7 @@ export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonCo return { shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers), + shouldHideReadMarks: Boolean(result.hideReadMarks), }; } diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 3e2950e23..893e0c445 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -536,6 +536,7 @@ export interface ApiMessage { }; reactions?: ApiReactions; hasComments?: boolean; + readDate?: number; savedPeerId?: string; } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index aa6f22940..931f20647 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -178,6 +178,7 @@ export interface ApiAppConfig { emojiSounds: Record; seenByMaxChatMembers: number; seenByExpiresAt: number; + readDateExpiresAt: number; autologinDomains: string[]; urlAuthDomains: string[]; premiumInvoiceSlug: string; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index d820d9678..2d96f80f3 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -66,6 +66,8 @@ export interface ApiUserStatus { ); wasOnline?: number; expires?: number; + isReadDateRestrictedByMe?: boolean; + isReadDateRestricted?: boolean; } export interface ApiUsername { diff --git a/src/assets/tgs/ReadTime.tgs b/src/assets/tgs/ReadTime.tgs new file mode 100644 index 0000000000000000000000000000000000000000..ceabee86d03f846d32df8ac993a380467a45f626 GIT binary patch literal 9552 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 Z`fG@q{1M}-r>lP`9Xo7EL9Afk{uju=cYgo? literal 0 HcmV?d00001 diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 52652ded5..32682d19b 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -30,6 +30,7 @@ export { default as PinMessageModal } from '../components/common/PinMessageModal export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllMessagesModal'; export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar'; export { default as SeenByModal } from '../components/common/SeenByModal'; +export { default as ReadTimeModal } from '../components/common/ReadDateModal'; export { default as ReactorListModal } from '../components/middle/ReactorListModal'; export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation'; export { default as ChatLanguageModal } from '../components/middle/ChatLanguageModal'; diff --git a/src/components/common/ReadDateModal.module.scss b/src/components/common/ReadDateModal.module.scss new file mode 100644 index 000000000..aa86b73d5 --- /dev/null +++ b/src/components/common/ReadDateModal.module.scss @@ -0,0 +1,43 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.icon { + border-radius: 50%; + background-color: var(--color-primary); + margin-bottom: 0.25rem; +} + +.header { + margin: 0.5rem; + font-size: 1.25rem; +} + +.desc { + font-size: 0.9375rem; + text-align: center; + + @media (min-width: 600px) { + margin-left: 0.75rem; + margin-right: 0.75rem; + } +} + +.separator { + margin-top: 1.25rem; + margin-bottom: 0.25rem; + width: 80%; +} + +.button { + text-transform: none; + border-radius: var(--border-radius-default-tiny); +} + +.closeButton { + position: absolute; + top: 0.5rem; + right: 0.5rem; +} diff --git a/src/components/common/ReadDateModal.tsx b/src/components/common/ReadDateModal.tsx new file mode 100644 index 000000000..429cfcb09 --- /dev/null +++ b/src/components/common/ReadDateModal.tsx @@ -0,0 +1,106 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import type { ApiUser } from '../../api/types'; + +import { ANIMATION_END_DELAY } from '../../config'; +import { getUserFirstOrLastName } from '../../global/helpers'; +import { selectTabState, selectUser } from '../../global/selectors'; +import { LOCAL_TGS_URLS } from './helpers/animatedAssets'; +import renderText from './helpers/renderText'; + +import useLang from '../../hooks/useLang'; + +import Button from '../ui/Button'; +import Modal, { ANIMATION_DURATION } from '../ui/Modal'; +import Separator from '../ui/Separator'; +import AnimatedIconWithPreview from './AnimatedIconWithPreview'; +import Icon from './Icon'; + +import styles from './ReadDateModal.module.scss'; + +export type OwnProps = { + isOpen: boolean; +}; + +type StateProps = { + user?: ApiUser; +}; + +const CLOSE_ANIMATION_DURATION = ANIMATION_DURATION + ANIMATION_END_DELAY; + +const ReadDateModal = ({ isOpen, user }: OwnProps & StateProps) => { + const lang = useLang(); + const { + updateGlobalPrivacySettings, openPremiumModal, closeGetReadDateModal, showNotification, + } = getActions(); + const userName = getUserFirstOrLastName(user); + + const handleShowReadTime = () => { + updateGlobalPrivacySettings({ shouldHideReadMarks: false }); + closeGetReadDateModal(); + + setTimeout(() => { + showNotification({ message: lang('PremiumReadSet') }); + }, CLOSE_ANIMATION_DURATION); + }; + + const handleOpenPremium = () => { + closeGetReadDateModal(); + + setTimeout(() => { + openPremiumModal(); + }, CLOSE_ANIMATION_DURATION); + }; + + return ( + +
+ + +

{lang('PremiumReadHeader1')}

+

{renderText(lang('PremiumReadText1', userName), ['simple_markdown'])}

+ + {lang('PremiumOr')} +

{lang('PremiumReadHeader2')}

+

{renderText(lang('PremiumReadText2', userName), ['simple_markdown'])}

+ {/* eslint-disable-next-line react/jsx-no-bind */} + +
+
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { chatId } = selectTabState(global).readDateModal || {}; + const user = chatId ? selectUser(global, chatId) : undefined; + + return { user }; + }, +)(ReadDateModal)); diff --git a/src/components/common/ReadTimeModal.async.tsx b/src/components/common/ReadTimeModal.async.tsx new file mode 100644 index 000000000..a1410967d --- /dev/null +++ b/src/components/common/ReadTimeModal.async.tsx @@ -0,0 +1,18 @@ +import type { FC } from '../../lib/teact/teact'; +import React from '../../lib/teact/teact'; + +import type { OwnProps } from './ReadDateModal'; + +import { Bundles } from '../../util/moduleLoader'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const ReadTimeModalAsync: FC = (props) => { + const { isOpen } = props; + const ReadTimeModal = useModuleLoader(Bundles.Extra, 'ReadTimeModal', !isOpen); + + // eslint-disable-next-line react/jsx-props-no-spreading + return ReadTimeModal ? : undefined; +}; + +export default ReadTimeModalAsync; diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 54350bfba..1079ead2d 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -15,6 +15,7 @@ import MonkeyClose from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyClose.t import MonkeyIdle from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyIdle.tgs'; import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs'; import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; +import ReadTime from '../../../assets/tgs/ReadTime.tgs'; import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; import Experimental from '../../../assets/tgs/settings/Experimental.tgs'; @@ -48,4 +49,5 @@ export const LOCAL_TGS_URLS = { Experimental, PartyPopper, Flame, + ReadTime, }; diff --git a/src/components/left/settings/SettingsPrivacyLastSeen.module.scss b/src/components/left/settings/SettingsPrivacyLastSeen.module.scss new file mode 100644 index 000000000..898838f0b --- /dev/null +++ b/src/components/left/settings/SettingsPrivacyLastSeen.module.scss @@ -0,0 +1,3 @@ +:global(.settings-item-description-larger).premiumInfo { + margin-top: 1rem; +} diff --git a/src/components/left/settings/SettingsPrivacyLastSeen.tsx b/src/components/left/settings/SettingsPrivacyLastSeen.tsx new file mode 100644 index 000000000..96964dbeb --- /dev/null +++ b/src/components/left/settings/SettingsPrivacyLastSeen.tsx @@ -0,0 +1,84 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { PrivacyVisibility } from '../../../types'; + +import { selectIsCurrentUserPremium, selectShouldHideReadMarks } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import renderText from '../../common/helpers/renderText'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import PremiumIcon from '../../common/PremiumIcon'; +import Checkbox from '../../ui/Checkbox'; +import ListItem from '../../ui/ListItem'; + +import styles from './SettingsPrivacyLastSeen.module.scss'; + +type OwnProps = { + visibility?: PrivacyVisibility; +}; + +type StateProps = { + isCurrentUserPremium: boolean; + shouldHideReadMarks: boolean; +}; + +const SettingsPrivacyLastSeen = ({ + isCurrentUserPremium, shouldHideReadMarks, visibility, +}: OwnProps & StateProps) => { + const { updateGlobalPrivacySettings, openPremiumModal } = getActions(); + const lang = useLang(); + const canShowHideReadTime = visibility === 'nobody' || visibility === 'contacts'; + + const handleChangeShouldHideReadMarks = useLastCallback( + (isEnabled) => updateGlobalPrivacySettings({ shouldHideReadMarks: isEnabled }), + ); + + return ( + <> + {canShowHideReadTime && ( +
+ +

+ {renderText(lang('HideReadTimeInfo'), ['br'])} +

+
+ )} +
+ } + // eslint-disable-next-line react/jsx-no-bind + onClick={() => openPremiumModal()} + > + {isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')} + +

+ {isCurrentUserPremium + ? lang('PrivacyLastSeenPremiumInfoForPremium') + : lang('PrivacyLastSeenPremiumInfo')} +

+
+ + ); +}; + +export default memo(withGlobal( + (global): StateProps => { + return { + isCurrentUserPremium: selectIsCurrentUserPremium(global), + shouldHideReadMarks: Boolean(selectShouldHideReadMarks(global)), + }; + }, +)(SettingsPrivacyLastSeen)); diff --git a/src/components/left/settings/SettingsPrivacyVisibility.tsx b/src/components/left/settings/SettingsPrivacyVisibility.tsx index dee894146..16556552a 100644 --- a/src/components/left/settings/SettingsPrivacyVisibility.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibility.tsx @@ -15,6 +15,7 @@ import useLastCallback from '../../../hooks/useLastCallback'; import ListItem from '../../ui/ListItem'; import RadioGroup from '../../ui/RadioGroup'; +import SettingsPrivacyLastSeen from './SettingsPrivacyLastSeen'; import SettingsPrivacyPublicProfilePhoto from './SettingsPrivacyPublicProfilePhoto'; type OwnProps = { @@ -74,6 +75,9 @@ const SettingsPrivacyVisibility: FC = ({ currentUserFallbackPhoto={currentUserFallbackPhoto} /> )} + {screen === SettingsScreens.PrivacyLastSeen && ( + + )} {secondaryScreen && ( + {IS_TRANSLATION_SUPPORTED && } @@ -723,7 +727,7 @@ export default memo(withGlobal( const { messageLists, isLeftColumnShown, activeEmojiInteractions, seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations, - chatLanguageModal, + chatLanguageModal, readDateModal, } = selectTabState(global); const currentMessageList = selectCurrentMessageList(global); const { leftColumnWidth } = global; @@ -739,6 +743,7 @@ export default memo(withGlobal( hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)), isSelectModeActive: selectIsInSelectMode(global), isSeenByModalOpen: Boolean(seenByModal), + isReadDateModalOpen: Boolean(readDateModal), isReactorListModalOpen: Boolean(reactorModal), isGiftPremiumModalOpen: giftPremiumModal?.isOpen, isChatLanguageModalOpen: Boolean(chatLanguageModal), diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index 8f09e5fac..6370fb2fe 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -33,6 +33,7 @@ import { selectCurrentMessageList, selectIsCurrentUserPremium, selectIsMessageProtected, + selectIsMessageUnread, selectIsPremiumPurchaseBlocked, selectIsReactionPickerOpen, selectMessageCustomEmojiSets, @@ -41,6 +42,7 @@ import { selectRequestedChatTranslationLanguage, selectRequestedMessageTranslationLanguage, selectStickerSet, + selectUserStatus, } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import { copyTextToClipboard } from '../../../util/clipboard'; @@ -107,6 +109,8 @@ type StateProps = { canSaveGif?: boolean; canRevote?: boolean; canClosePoll?: boolean; + canLoadReadDate?: boolean; + shouldRenderShowWhen?: boolean; activeDownloads?: TabState['activeDownloads']['byChatId'][number]; canShowSeenBy?: boolean; enabledReactions?: ApiChatReactions; @@ -159,6 +163,8 @@ const ContextMenuContainer: FC = ({ canRevote, canClosePoll, canPlayAnimatedEmojis, + canLoadReadDate, + shouldRenderShowWhen, activeDownloads, noReplies, canShowSeenBy, @@ -200,6 +206,7 @@ const ContextMenuContainer: FC = ({ showOriginalMessage, openChatLanguageModal, openMessageReactionPicker, + loadOutboxReadDate, } = getActions(); const lang = useLang(); @@ -221,6 +228,12 @@ const ContextMenuContainer: FC = ({ } }, [loadSeenBy, isOpen, message.chatId, message.id, canShowSeenBy]); + useEffect(() => { + if (canLoadReadDate && isOpen) { + loadOutboxReadDate({ chatId: message.chatId, messageId: message.id }); + } + }, [canLoadReadDate, isOpen, message.chatId, message.id, message.readDate]); + useEffect(() => { if (canShowReactionsCount && isOpen) { loadReactors({ chatId: message.chatId, messageId: message.id }); @@ -554,6 +567,8 @@ const ContextMenuContainer: FC = ({ canShowOriginal={canShowOriginal} canSelectLanguage={canSelectLanguage} canPlayAnimatedEmojis={canPlayAnimatedEmojis} + shouldRenderShowWhen={shouldRenderShowWhen} + canLoadReadDate={canLoadReadDate} hasCustomEmoji={hasCustomEmoji} customEmojiSets={customEmojiSets} isDownloading={isDownloading} @@ -623,7 +638,9 @@ export default memo(withGlobal( const { threadId } = selectCurrentMessageList(global) || {}; const activeDownloads = selectActiveDownloads(global, message.chatId); const chat = selectChat(global, message.chatId); - const { seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions } = global.appConfig || {}; + const { + seenByExpiresAt, seenByMaxChatMembers, maxUniqueReactions, readDateExpiresAt, + } = global.appConfig || {}; const { noOptions, canReply, @@ -645,7 +662,20 @@ export default memo(withGlobal( } = (threadId && selectAllowedMessageActions(global, message, threadId)) || {}; const isPrivate = chat && isUserId(chat.id); + const userStatus = isPrivate ? selectUserStatus(global, chat.id) : undefined; const isOwn = isOwnMessage(message); + const isMessageUnread = selectIsMessageUnread(global, message); + const canLoadReadDate = Boolean( + isPrivate + && isOwn + && !isMessageUnread + && readDateExpiresAt + && message.date > Date.now() / 1000 - readDateExpiresAt + && !userStatus?.isReadDateRestricted, + ); + const shouldRenderShowWhen = Boolean( + canLoadReadDate && isPrivate && selectUserStatus(global, chat.id)?.isReadDateRestrictedByMe, + ); const isPinned = messageListType === 'pinned'; const isScheduled = messageListType === 'scheduled'; const isChannel = chat && isChatChannel(chat); @@ -653,6 +683,7 @@ export default memo(withGlobal( const hasTtl = hasMessageTtl(message); const canShowSeenBy = Boolean(!isLocal && chat + && !isMessageUnread && seenByMaxChatMembers && seenByExpiresAt && isChatGroup(chat) @@ -706,6 +737,8 @@ export default memo(withGlobal( canClosePoll: !isScheduled && canClosePoll, activeDownloads, canShowSeenBy, + canLoadReadDate, + shouldRenderShowWhen, enabledReactions: chat?.isForbidden ? undefined : chatFullInfo?.enabledReactions, maxUniqueReactions, isPrivate, diff --git a/src/components/middle/message/Giveaway.tsx b/src/components/middle/message/Giveaway.tsx index 17e560c7d..8495385a7 100644 --- a/src/components/middle/message/Giveaway.tsx +++ b/src/components/middle/message/Giveaway.tsx @@ -32,6 +32,7 @@ import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker'; import PickerSelectedItem from '../../common/PickerSelectedItem'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; +import Separator from '../../ui/Separator'; import styles from './Giveaway.module.scss'; @@ -124,7 +125,7 @@ const Giveaway = ({ ['simple_markdown'], )}

-
{lang('BoostingGiveawayMsgWithDivider')}
+ {lang('BoostingGiveawayMsgWithDivider')} )}

diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index c28e3698c..89022872c 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -37,6 +37,7 @@ import MenuItem from '../../ui/MenuItem'; import MenuSeparator from '../../ui/MenuSeparator'; import Skeleton from '../../ui/placeholder/Skeleton'; import ReactionSelector from './ReactionSelector'; +import ReadTimeMenuItem from './ReadTimeMenuItem'; import './MessageContextMenu.scss'; @@ -86,6 +87,8 @@ type OwnProps = { customEmojiSets?: ApiStickerSet[]; canPlayAnimatedEmojis?: boolean; noTransition?: boolean; + shouldRenderShowWhen?: boolean; + canLoadReadDate?: boolean; onReply?: NoneToVoidFunction; onOpenThread?: VoidFunction; onEdit?: NoneToVoidFunction; @@ -171,6 +174,8 @@ const MessageContextMenu: FC = ({ customEmojiSets, canPlayAnimatedEmojis, noTransition, + shouldRenderShowWhen, + canLoadReadDate, onReply, onOpenThread, onEdit, @@ -401,42 +406,10 @@ const MessageContextMenu: FC = ({ {canForward && {lang('Forward')}} {canSelect && {lang('Common.Select')}} {canReport && {lang('lng_context_report_msg')}} - {(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && ( - - - - {canShowReactionsCount && message.reactors?.count ? ( - canShowSeenBy && seenByDatesCount - ? lang( - 'Chat.OutgoingContextMixedReactionCount', - [message.reactors.count, seenByDatesCount], - ) - : lang('Chat.ContextReactionCount', message.reactors.count, 'i') - ) : ( - seenByDatesCount === 1 && seenByRecentPeers - ? renderText( - isUserId(seenByRecentPeers[0].id) - ? getUserFullName(seenByRecentPeers[0] as ApiUser)! - : (seenByRecentPeers[0] as ApiChat).title, - ) : ( - seenByDatesCount - ? lang('Conversation.ContextMenuSeen', seenByDatesCount, 'i') - : lang('Conversation.ContextMenuNoViews') - ) - )} - - - - - )} {canDelete && {lang('Delete')}} {hasCustomEmoji && ( <> - + {!customEmojiSets && ( <> @@ -462,6 +435,50 @@ const MessageContextMenu: FC = ({ {isSponsoredMessage && onSponsoredHide && ( {lang('HideAd')} )} + {(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && ( + <> + + + + + {canShowReactionsCount && message.reactors?.count ? ( + canShowSeenBy && seenByDatesCount + ? lang( + 'Chat.OutgoingContextMixedReactionCount', + [message.reactors.count, seenByDatesCount], + ) + : lang('Chat.ContextReactionCount', message.reactors.count, 'i') + ) : ( + seenByDatesCount === 1 && seenByRecentPeers + ? renderText( + isUserId(seenByRecentPeers[0].id) + ? getUserFullName(seenByRecentPeers[0] as ApiUser)! + : (seenByRecentPeers[0] as ApiChat).title, + ) : ( + seenByDatesCount + ? lang('Conversation.ContextMenuSeen', seenByDatesCount, 'i') + : lang('Conversation.ContextMenuNoViews') + ) + )} + + + + + + )} + {!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen) && ( + + )} ); diff --git a/src/components/middle/message/ReadTimeMenuItem.module.scss b/src/components/middle/message/ReadTimeMenuItem.module.scss new file mode 100644 index 000000000..c273c15b7 --- /dev/null +++ b/src/components/middle/message/ReadTimeMenuItem.module.scss @@ -0,0 +1,44 @@ +:global(.MenuItem).item { + margin-bottom: 0; + font-size: 0.8125rem; + cursor: var(--custom-cursor, default); + pointer-events: none; + --color-skeleton-background: #2121211a; + &:hover, + &:focus, + &:active { + background: none; + } + + :global(.icon) { + margin-left: 0; + margin-right: 0.25rem; + } + + &[dir="rtl"] { + :global(.icon) { + margin-left: 0.25rem; + margin-right: 0; + } + } +} + +.get { + cursor: var(--custom-cursor, pointer); + margin-left: 0.375rem; + border-radius: 0.5rem; + padding: 0.125rem 0.375rem; + background: var(--color-background-menu-separator); + pointer-events: all; +} + +.skeleton { + height: 0.5rem; + width: calc(100% - 2rem); + margin: 0.5rem 0; + border-radius: 0.25rem; +} + +.transition { + height: 1.5rem; +} diff --git a/src/components/middle/message/ReadTimeMenuItem.tsx b/src/components/middle/message/ReadTimeMenuItem.tsx new file mode 100644 index 000000000..4aa962fa4 --- /dev/null +++ b/src/components/middle/message/ReadTimeMenuItem.tsx @@ -0,0 +1,62 @@ +import React, { memo } from '../../../lib/teact/teact'; +import { getActions } from '../../../lib/teact/teactn'; + +import type { ApiMessage } from '../../../api/types'; + +import { formatDateAtTime } from '../../../util/dateFormat'; + +import useLang from '../../../hooks/useLang'; + +import MenuItem from '../../ui/MenuItem'; +import MenuSeparator from '../../ui/MenuSeparator'; +import Skeleton from '../../ui/placeholder/Skeleton'; +import Transition from '../../ui/Transition'; + +import styles from './ReadTimeMenuItem.module.scss'; + +type OwnProps = { + message: ApiMessage; + shouldRenderShowWhen?: boolean; + canLoadReadDate?: boolean; + menuSeparatorSize: 'thin' | 'thick'; + closeContextMenu: NoneToVoidFunction; +}; + +function ReadTimeMenuItem({ + message, shouldRenderShowWhen, canLoadReadDate, closeContextMenu, menuSeparatorSize, +}: OwnProps) { + const { openGetReadDateModal } = getActions(); + const lang = useLang(); + const { readDate } = message; + const shouldRenderSkeleton = canLoadReadDate && !readDate && !shouldRenderShowWhen; + + const handleOpenModal = () => { + closeContextMenu(); + openGetReadDateModal({ chatId: message.chatId, messageId: message.id }); + }; + + return ( + <> + + + + {shouldRenderSkeleton ? : ( + <> + {Boolean(readDate) && lang('PmReadAt', formatDateAtTime(lang, readDate * 1000))} + {!readDate && shouldRenderShowWhen && ( +

+ {lang('PmRead')} + + {lang('PmReadShowWhen')} + +
+ )} + + )} + + + + ); +} + +export default memo(ReadTimeMenuItem); diff --git a/src/components/ui/MenuSeparator.module.scss b/src/components/ui/MenuSeparator.module.scss index bc934d91c..d32c6d361 100644 --- a/src/components/ui/MenuSeparator.module.scss +++ b/src/components/ui/MenuSeparator.module.scss @@ -1,6 +1,13 @@ .root { margin: 0.25rem 0; - height: 1px; - border-radius: 1px; - background-color: var(--color-interactive-inactive); + border-radius: 0.0625rem; + background-color: var(--color-background-menu-separator); +} + +.thin { + height: 0.0625rem; +} + +.thick { + height: 0.375rem; } diff --git a/src/components/ui/MenuSeparator.tsx b/src/components/ui/MenuSeparator.tsx index b178b90f3..b8d069dd2 100644 --- a/src/components/ui/MenuSeparator.tsx +++ b/src/components/ui/MenuSeparator.tsx @@ -7,11 +7,12 @@ import styles from './MenuSeparator.module.scss'; type OwnProps = { className?: string; + size?: 'thin' | 'thick'; }; -const MenuSeparator: FC = ({ className }) => { +const MenuSeparator: FC = ({ className, size = 'thin' }) => { return ( -
+
); }; diff --git a/src/components/ui/Modal.tsx b/src/components/ui/Modal.tsx index e750c74ce..9a5943c47 100644 --- a/src/components/ui/Modal.tsx +++ b/src/components/ui/Modal.tsx @@ -21,7 +21,7 @@ import Portal from './Portal'; import './Modal.scss'; -const ANIMATION_DURATION = 200; +export const ANIMATION_DURATION = 200; type OwnProps = { title?: string | TextPart[]; diff --git a/src/components/ui/Separator.module.scss b/src/components/ui/Separator.module.scss new file mode 100644 index 000000000..f68f95086 --- /dev/null +++ b/src/components/ui/Separator.module.scss @@ -0,0 +1,33 @@ +.separator { + display: flex; + align-items: center; + text-align: center; + color: var(--color-text-secondary); + + &::before, + &::after { + content: ''; + flex: 1; + border-bottom: 0.0625rem solid var(--color-dividers); + } + + &:not(:empty)::before { + margin-right: 0.5rem; + } + + &:not(:empty)::after { + margin-left: 0.5rem; + } + + &[dir="rtl"] { + &:not(:empty)::before { + margin-left: 0.5rem; + margin-right: 0; + } + + &:not(:empty)::after { + margin-right: 0.5rem; + margin-left: 0; + } + } +} diff --git a/src/components/ui/Separator.tsx b/src/components/ui/Separator.tsx new file mode 100644 index 000000000..a7675591b --- /dev/null +++ b/src/components/ui/Separator.tsx @@ -0,0 +1,27 @@ +import React from '../../lib/teact/teact'; + +import buildClassName from '../../util/buildClassName'; + +import useLang from '../../hooks/useLang'; + +import styles from './Separator.module.scss'; + +type OwnProps = { + children: React.ReactNode; + className?: string; +}; + +function Separator({ children, className }: OwnProps) { + const lang = useLang(); + + return ( +
+ {children} +
+ ); +} + +export default Separator; diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index f1673e65a..6f25531d7 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -1,6 +1,7 @@ import type { ApiAttachment, ApiChat, + ApiError, ApiInputMessageReplyInfo, ApiInputReplyInfo, ApiInputStoryReplyInfo, @@ -66,6 +67,7 @@ import { replaceScheduledMessages, replaceSettings, replaceThreadParam, + replaceUserStatuses, safeReplacePinnedIds, safeReplaceViewportIds, updateChat, @@ -116,6 +118,7 @@ import { selectTranslationLanguage, selectUser, selectUserFullInfo, + selectUserStatus, selectViewportIds, } from '../../selectors'; import { deleteMessages } from '../apiUpdaters/messages'; @@ -1820,6 +1823,44 @@ addActionHandler('loadMessageViews', async (global, actions, payload): Promise => { + const { chatId, messageId } = payload; + + const chat = selectChat(global, chatId); + if (!chat) return; + + try { + const result = await callApi('fetchOutboxReadDate', { chat, messageId }); + if (result?.date) { + global = getGlobal(); + global = updateChatMessage(global, chatId, messageId, { readDate: result.date }); + setGlobal(global); + } + } catch (error) { + const { message } = error as ApiError; + + if (message === 'USER_PRIVACY_RESTRICTED' || message === 'YOUR_PRIVACY_RESTRICTED') { + global = getGlobal(); + + const user = selectUser(global, chatId); + if (!user) return; + const userStatus = selectUserStatus(global, chatId); + if (!userStatus) return; + + const updateStatus = message === 'USER_PRIVACY_RESTRICTED' + ? { isReadDateRestricted: true } + : { isReadDateRestrictedByMe: true }; + + global = replaceUserStatuses(global, { + [chatId]: { ...userStatus, ...updateStatus }, + }); + // Need to reset `readDate` to `undefined` after click on "Show my Read Time" button + global = updateChatMessage(global, chatId, messageId, { readDate: undefined }); + setGlobal(global); + } + } +}); + function countSortedIds(ids: number[], from: number, to: number) { let count = 0; diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 89ad56248..5f4d93024 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -666,24 +666,29 @@ addActionHandler('loadGlobalPrivacySettings', async (global): Promise => { } global = getGlobal(); - global = replaceSettings(global, { - shouldArchiveAndMuteNewNonContact: globalSettings.shouldArchiveAndMuteNewNonContact, - }); + global = replaceSettings(global, { ...globalSettings }); setGlobal(global); }); addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload): Promise => { - const { shouldArchiveAndMuteNewNonContact } = payload; - global = replaceSettings(global, { shouldArchiveAndMuteNewNonContact }); + const shouldArchiveAndMuteNewNonContact = payload.shouldArchiveAndMuteNewNonContact + ?? Boolean(global.settings.byKey.shouldArchiveAndMuteNewNonContact); + const shouldHideReadMarks = payload.shouldHideReadMarks ?? Boolean(global.settings.byKey.shouldHideReadMarks); + + global = replaceSettings(global, { shouldArchiveAndMuteNewNonContact, shouldHideReadMarks }); setGlobal(global); - const result = await callApi('updateGlobalPrivacySettings', { shouldArchiveAndMuteNewNonContact }); + const result = await callApi('updateGlobalPrivacySettings', { + shouldArchiveAndMuteNewNonContact, + shouldHideReadMarks, + }); global = getGlobal(); global = replaceSettings(global, { shouldArchiveAndMuteNewNonContact: !result ? !shouldArchiveAndMuteNewNonContact : result.shouldArchiveAndMuteNewNonContact, + shouldHideReadMarks: !result ? !shouldHideReadMarks : result.shouldHideReadMarks, }); setGlobal(global); }); diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index c32fb04cb..3144d7f0d 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -781,6 +781,22 @@ addActionHandler('closeSeenByModal', (global, actions, payload): ActionReturnTyp }, tabId); }); +addActionHandler('openGetReadDateModal', (global, actions, payload): ActionReturnType => { + const { chatId, messageId, tabId = getCurrentTabId() } = payload; + + return updateTabState(global, { + readDateModal: { chatId, messageId }, + }, tabId); +}); + +addActionHandler('closeGetReadDateModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + readDateModal: undefined, + }, tabId); +}); + addActionHandler('openChatLanguageModal', (global, actions, payload): ActionReturnType => { const { chatId, messageId, tabId = getCurrentTabId() } = payload; diff --git a/src/global/initialState.ts b/src/global/initialState.ts index fb9722283..9e878aac1 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -237,6 +237,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { wasTimeFormatSetManually: false, isConnectionStatusMinimized: true, shouldArchiveAndMuteNewNonContact: false, + shouldHideReadMarks: false, canTranslate: false, canTranslateChats: true, doNotTranslate: [], diff --git a/src/global/selectors/settings.ts b/src/global/selectors/settings.ts index 0ea135cd4..d93b6c7b9 100644 --- a/src/global/selectors/settings.ts +++ b/src/global/selectors/settings.ts @@ -19,3 +19,7 @@ export function selectCanSetPasscode(global: T) { export function selectTranslationLanguage(global: T) { return global.settings.byKey.translationLanguage || selectLanguageCode(global); } + +export function selectShouldHideReadMarks(global: T) { + return global.settings.byKey.shouldHideReadMarks; +} diff --git a/src/global/types.ts b/src/global/types.ts index 67ae69797..ecfaa4988 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -283,6 +283,11 @@ export type TabState = { messageId: number; }; + readDateModal?: { + chatId: string; + messageId: number; + }; + reactorModal?: { chatId: string; messageId: number; @@ -1607,6 +1612,11 @@ export interface ActionPayloads { messageId: number; } & WithTabId; closeSeenByModal: WithTabId | undefined; + openGetReadDateModal: { + chatId: string; + messageId: number; + } & WithTabId; + closeGetReadDateModal: WithTabId | undefined; closeReactorListModal: WithTabId | undefined; openReactorListModal: { chatId: string; @@ -2009,6 +2019,10 @@ export interface ActionPayloads { ids: number[]; shouldIncrement?: boolean; }; + loadOutboxReadDate: { + chatId: string; + messageId: number; + }; animateUnreadReaction: { messageIds: number[]; } & WithTabId; @@ -2833,7 +2847,7 @@ export interface ActionPayloads { } & WithTabId; closeShareChatFolderModal: undefined | WithTabId; loadGlobalPrivacySettings: undefined; - updateGlobalPrivacySettings: { shouldArchiveAndMuteNewNonContact: boolean }; + updateGlobalPrivacySettings: { shouldArchiveAndMuteNewNonContact?: boolean; shouldHideReadMarks?: boolean }; // Premium openPremiumModal: ({ diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 2a117d367..6909bf9b0 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1412,6 +1412,7 @@ messages.getSavedHistory#3d9a414d peer:InputPeer offset_id:int offset_date:int a messages.deleteSavedHistory#6e98102b flags:# peer:InputPeer max_id:int min_date:flags.2?int max_date:flags.3?int = messages.AffectedHistory; messages.getPinnedSavedDialogs#d63d94e0 = messages.SavedDialogs; messages.toggleSavedDialogPin#ac81bbde flags:# pinned:flags.0?true peer:InputDialogPeer = Bool; +messages.getOutboxReadDate#8c4bfe5d peer:InputPeer msg_id:int = OutboxReadDate; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index f8fbe5a2c..fbc1c1340 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -168,6 +168,7 @@ "messages.getBotApp", "messages.requestAppWebView", "messages.togglePeerTranslations", + "messages.getOutboxReadDate", "updates.getState", "updates.getDifference", "updates.getChannelDifference", diff --git a/src/styles/_variables.scss b/src/styles/_variables.scss index cbc2f713b..81c0e70de 100644 --- a/src/styles/_variables.scss +++ b/src/styles/_variables.scss @@ -62,6 +62,7 @@ $color-message-story-mention-to: #74bcff; --color-background-compact-menu: #FFFFFFBB; --color-background-compact-menu-reactions: #FFFFFFEB; --color-background-compact-menu-hover: #000000B2; + --color-background-menu-separator: #0000001a; --color-background-selected: #f4f4f5; --color-background-secondary: #f4f4f5; --color-background-secondary-accent: #e4e4e5; diff --git a/src/styles/index.scss b/src/styles/index.scss index b7317472a..4488b4032 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -324,6 +324,7 @@ body:not(.is-ios) { --color-background-compact-menu: rgb(33, 33, 33, 0.867); --color-background-compact-menu-reactions: rgb(33, 33, 33, 0.867); --color-background-compact-menu-hover: rgb(0, 0, 0, 0.4); + --color-background-menu-separator: rgba(255, 255, 255, 0.102); --color-background-secondary: rgb(15, 15, 15); --color-background-secondary-accent: rgb(16, 15, 16); --color-background-own: rgb(118, 106, 200); diff --git a/src/styles/themes.json b/src/styles/themes.json index 9ded169b9..1278b2a59 100644 --- a/src/styles/themes.json +++ b/src/styles/themes.json @@ -66,5 +66,6 @@ "--color-forum-unread-topic-hover": ["#e9e9e9", "#363636"], "--color-forum-hover-unread-topic-hover": ["#e2e2e2", "#3f3f3f"], "--color-chat-username": ["#3C7EB0", "#E9EEF4"], - "--color-borders-read-story": ["#C4C9CC", "#737373"] + "--color-borders-read-story": ["#C4C9CC", "#737373"], + "--color-background-menu-separator": ["#0000001a", "#ffffff1a"] } diff --git a/src/types/index.ts b/src/types/index.ts index 424357f58..d36e1bced 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -101,6 +101,7 @@ export interface ISettings extends NotifySettings, Record { wasTimeFormatSetManually: boolean; isConnectionStatusMinimized: boolean; shouldArchiveAndMuteNewNonContact?: boolean; + shouldHideReadMarks?: boolean; canTranslate: boolean; canTranslateChats: boolean; translationLanguage?: string;