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 && }
{canSelect && }
{canReport && }
- {(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && (
-
- )}
{canDelete && }
{hasCustomEmoji && (
<>
-
+
{!customEmojiSets && (
<>
@@ -462,6 +435,50 @@ const MessageContextMenu: FC = ({
{isSponsoredMessage && onSponsoredHide && (
)}
+ {(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && (
+ <>
+
+
+ >
+ )}
+ {!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 (
+ <>
+
+
+ >
+ );
+}
+
+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;