MessageContextMenu: Read time in private chats (#4218)
This commit is contained in:
parent
6146cf3a1b
commit
64799a8818
@ -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,
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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 };
|
||||
}
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -536,6 +536,7 @@ export interface ApiMessage {
|
||||
};
|
||||
reactions?: ApiReactions;
|
||||
hasComments?: boolean;
|
||||
readDate?: number;
|
||||
savedPeerId?: string;
|
||||
}
|
||||
|
||||
|
||||
@ -178,6 +178,7 @@ export interface ApiAppConfig {
|
||||
emojiSounds: Record<string, string>;
|
||||
seenByMaxChatMembers: number;
|
||||
seenByExpiresAt: number;
|
||||
readDateExpiresAt: number;
|
||||
autologinDomains: string[];
|
||||
urlAuthDomains: string[];
|
||||
premiumInvoiceSlug: string;
|
||||
|
||||
@ -66,6 +66,8 @@ export interface ApiUserStatus {
|
||||
);
|
||||
wasOnline?: number;
|
||||
expires?: number;
|
||||
isReadDateRestrictedByMe?: boolean;
|
||||
isReadDateRestricted?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiUsername {
|
||||
|
||||
BIN
src/assets/tgs/ReadTime.tgs
Normal file
BIN
src/assets/tgs/ReadTime.tgs
Normal file
Binary file not shown.
@ -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';
|
||||
|
||||
43
src/components/common/ReadDateModal.module.scss
Normal file
43
src/components/common/ReadDateModal.module.scss
Normal file
@ -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;
|
||||
}
|
||||
106
src/components/common/ReadDateModal.tsx
Normal file
106
src/components/common/ReadDateModal.tsx
Normal file
@ -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 (
|
||||
<Modal isSlim isOpen={isOpen} onClose={closeGetReadDateModal}>
|
||||
<div className={styles.container} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<Button
|
||||
className={styles.closeButton}
|
||||
color="translucent"
|
||||
round
|
||||
size="smaller"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => closeGetReadDateModal()}
|
||||
ariaLabel="Close"
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={LOCAL_TGS_URLS.ReadTime}
|
||||
size={84}
|
||||
className={styles.icon}
|
||||
nonInteractive
|
||||
noLoop
|
||||
/>
|
||||
<h2 className={styles.header}>{lang('PremiumReadHeader1')}</h2>
|
||||
<p className={styles.desc}>{renderText(lang('PremiumReadText1', userName), ['simple_markdown'])}</p>
|
||||
<Button
|
||||
size="smaller"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={handleShowReadTime}
|
||||
className={styles.button}
|
||||
>
|
||||
{lang('PremiumReadButton1')}
|
||||
</Button>
|
||||
<Separator className={styles.separator}>{lang('PremiumOr')}</Separator>
|
||||
<h2 className={styles.header}>{lang('PremiumReadHeader2')}</h2>
|
||||
<p className={styles.desc}>{renderText(lang('PremiumReadText2', userName), ['simple_markdown'])}</p>
|
||||
{/* eslint-disable-next-line react/jsx-no-bind */}
|
||||
<Button withPremiumGradient size="smaller" onClick={handleOpenPremium} className={styles.button}>
|
||||
{lang('PremiumLastSeenButton2')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const { chatId } = selectTabState(global).readDateModal || {};
|
||||
const user = chatId ? selectUser(global, chatId) : undefined;
|
||||
|
||||
return { user };
|
||||
},
|
||||
)(ReadDateModal));
|
||||
18
src/components/common/ReadTimeModal.async.tsx
Normal file
18
src/components/common/ReadTimeModal.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 './ReadDateModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const ReadTimeModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const ReadTimeModal = useModuleLoader(Bundles.Extra, 'ReadTimeModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return ReadTimeModal ? <ReadTimeModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default ReadTimeModalAsync;
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
:global(.settings-item-description-larger).premiumInfo {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
84
src/components/left/settings/SettingsPrivacyLastSeen.tsx
Normal file
84
src/components/left/settings/SettingsPrivacyLastSeen.tsx
Normal file
@ -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 && (
|
||||
<div className="settings-item">
|
||||
<Checkbox
|
||||
label={lang('HideReadTime')}
|
||||
checked={shouldHideReadMarks}
|
||||
onCheck={handleChangeShouldHideReadMarks}
|
||||
/>
|
||||
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{renderText(lang('HideReadTimeInfo'), ['br'])}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-item">
|
||||
<ListItem
|
||||
leftElement={<PremiumIcon className="icon" withGradient big />}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openPremiumModal()}
|
||||
>
|
||||
{isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')}
|
||||
</ListItem>
|
||||
<p
|
||||
className={buildClassName(
|
||||
'settings-item-description-larger',
|
||||
styles.premiumInfo,
|
||||
)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{isCurrentUserPremium
|
||||
? lang('PrivacyLastSeenPremiumInfoForPremium')
|
||||
: lang('PrivacyLastSeenPremiumInfo')}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
return {
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
shouldHideReadMarks: Boolean(selectShouldHideReadMarks(global)),
|
||||
};
|
||||
},
|
||||
)(SettingsPrivacyLastSeen));
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
currentUserFallbackPhoto={currentUserFallbackPhoto}
|
||||
/>
|
||||
)}
|
||||
{screen === SettingsScreens.PrivacyLastSeen && (
|
||||
<SettingsPrivacyLastSeen visibility={primaryPrivacy?.visibility} />
|
||||
)}
|
||||
{secondaryScreen && (
|
||||
<PrivacySubsection
|
||||
screen={secondaryScreen}
|
||||
|
||||
@ -78,6 +78,7 @@ import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
import usePinnedMessage from './hooks/usePinnedMessage';
|
||||
|
||||
import Composer from '../common/Composer';
|
||||
import ReadTimeModal from '../common/ReadTimeModal.async';
|
||||
import SeenByModal from '../common/SeenByModal.async';
|
||||
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
|
||||
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
||||
@ -128,6 +129,7 @@ type StateProps = {
|
||||
hasCurrentTextSearch?: boolean;
|
||||
isSelectModeActive?: boolean;
|
||||
isSeenByModalOpen: boolean;
|
||||
isReadDateModalOpen: boolean;
|
||||
isReactorListModalOpen: boolean;
|
||||
isGiftPremiumModalOpen?: boolean;
|
||||
isChatLanguageModalOpen?: boolean;
|
||||
@ -186,6 +188,7 @@ function MiddleColumn({
|
||||
hasCurrentTextSearch,
|
||||
isSelectModeActive,
|
||||
isSeenByModalOpen,
|
||||
isReadDateModalOpen,
|
||||
isReactorListModalOpen,
|
||||
isGiftPremiumModalOpen,
|
||||
isChatLanguageModalOpen,
|
||||
@ -676,6 +679,7 @@ function MiddleColumn({
|
||||
canPost={renderingCanPost}
|
||||
/>
|
||||
<SeenByModal isOpen={isSeenByModalOpen} />
|
||||
<ReadTimeModal isOpen={isReadDateModalOpen} />
|
||||
<ReactorListModal isOpen={isReactorListModalOpen} />
|
||||
{IS_TRANSLATION_SUPPORTED && <ChatLanguageModal isOpen={isChatLanguageModalOpen} />}
|
||||
</div>
|
||||
@ -723,7 +727,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
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<OwnProps>(
|
||||
hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)),
|
||||
isSelectModeActive: selectIsInSelectMode(global),
|
||||
isSeenByModalOpen: Boolean(seenByModal),
|
||||
isReadDateModalOpen: Boolean(readDateModal),
|
||||
isReactorListModalOpen: Boolean(reactorModal),
|
||||
isGiftPremiumModalOpen: giftPremiumModal?.isOpen,
|
||||
isChatLanguageModalOpen: Boolean(chatLanguageModal),
|
||||
|
||||
@ -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<OwnProps & StateProps> = ({
|
||||
canRevote,
|
||||
canClosePoll,
|
||||
canPlayAnimatedEmojis,
|
||||
canLoadReadDate,
|
||||
shouldRenderShowWhen,
|
||||
activeDownloads,
|
||||
noReplies,
|
||||
canShowSeenBy,
|
||||
@ -200,6 +206,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
showOriginalMessage,
|
||||
openChatLanguageModal,
|
||||
openMessageReactionPicker,
|
||||
loadOutboxReadDate,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
@ -221,6 +228,12 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
}, [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<OwnProps & StateProps> = ({
|
||||
canShowOriginal={canShowOriginal}
|
||||
canSelectLanguage={canSelectLanguage}
|
||||
canPlayAnimatedEmojis={canPlayAnimatedEmojis}
|
||||
shouldRenderShowWhen={shouldRenderShowWhen}
|
||||
canLoadReadDate={canLoadReadDate}
|
||||
hasCustomEmoji={hasCustomEmoji}
|
||||
customEmojiSets={customEmojiSets}
|
||||
isDownloading={isDownloading}
|
||||
@ -623,7 +638,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
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<OwnProps>(
|
||||
} = (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<OwnProps>(
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const canShowSeenBy = Boolean(!isLocal
|
||||
&& chat
|
||||
&& !isMessageUnread
|
||||
&& seenByMaxChatMembers
|
||||
&& seenByExpiresAt
|
||||
&& isChatGroup(chat)
|
||||
@ -706,6 +737,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
canClosePoll: !isScheduled && canClosePoll,
|
||||
activeDownloads,
|
||||
canShowSeenBy,
|
||||
canLoadReadDate,
|
||||
shouldRenderShowWhen,
|
||||
enabledReactions: chat?.isForbidden ? undefined : chatFullInfo?.enabledReactions,
|
||||
maxUniqueReactions,
|
||||
isPrivate,
|
||||
|
||||
@ -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'],
|
||||
)}
|
||||
</p>
|
||||
<div className={styles.separator}>{lang('BoostingGiveawayMsgWithDivider')}</div>
|
||||
<Separator>{lang('BoostingGiveawayMsgWithDivider')}</Separator>
|
||||
</>
|
||||
)}
|
||||
<p className={styles.description}>
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
customEmojiSets,
|
||||
canPlayAnimatedEmojis,
|
||||
noTransition,
|
||||
shouldRenderShowWhen,
|
||||
canLoadReadDate,
|
||||
onReply,
|
||||
onOpenThread,
|
||||
onEdit,
|
||||
@ -401,42 +406,10 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
{canForward && <MenuItem icon="forward" onClick={onForward}>{lang('Forward')}</MenuItem>}
|
||||
{canSelect && <MenuItem icon="select" onClick={onSelect}>{lang('Common.Select')}</MenuItem>}
|
||||
{canReport && <MenuItem icon="flag" onClick={onReport}>{lang('lng_context_report_msg')}</MenuItem>}
|
||||
{(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && (
|
||||
<MenuItem
|
||||
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
||||
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||
disabled={!canShowReactionsCount && !seenByDatesCount}
|
||||
>
|
||||
<span className="MessageContextMenu--seen-by-label-wrapper">
|
||||
<span className="MessageContextMenu--seen-by-label" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{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')
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<AvatarList className="avatars" size="micro" peers={seenByRecentPeers} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{canDelete && <MenuItem destructive icon="delete" onClick={onDelete}>{lang('Delete')}</MenuItem>}
|
||||
{hasCustomEmoji && (
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<MenuSeparator size="thick" />
|
||||
{!customEmojiSets && (
|
||||
<>
|
||||
<Skeleton inline className="menu-loading-row" />
|
||||
@ -462,6 +435,50 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
{isSponsoredMessage && onSponsoredHide && (
|
||||
<MenuItem icon="stop" onClick={onSponsoredHide}>{lang('HideAd')}</MenuItem>
|
||||
)}
|
||||
{(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && (
|
||||
<>
|
||||
<MenuSeparator size={hasCustomEmoji ? 'thin' : 'thick'} />
|
||||
<MenuItem
|
||||
icon={canShowReactionsCount ? 'heart-outline' : 'group'}
|
||||
onClick={canShowReactionsCount ? onShowReactors : onShowSeenBy}
|
||||
disabled={!canShowReactionsCount && !seenByDatesCount}
|
||||
>
|
||||
<span className="MessageContextMenu--seen-by-label-wrapper">
|
||||
<span className="MessageContextMenu--seen-by-label" dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{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')
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<AvatarList className="avatars" size="micro" peers={seenByRecentPeers} />
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen) && (
|
||||
<ReadTimeMenuItem
|
||||
canLoadReadDate={canLoadReadDate}
|
||||
shouldRenderShowWhen={shouldRenderShowWhen}
|
||||
message={message}
|
||||
menuSeparatorSize={hasCustomEmoji ? 'thin' : 'thick'}
|
||||
closeContextMenu={onClose}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
44
src/components/middle/message/ReadTimeMenuItem.module.scss
Normal file
44
src/components/middle/message/ReadTimeMenuItem.module.scss
Normal file
@ -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;
|
||||
}
|
||||
62
src/components/middle/message/ReadTimeMenuItem.tsx
Normal file
62
src/components/middle/message/ReadTimeMenuItem.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<MenuSeparator size={menuSeparatorSize} />
|
||||
<MenuItem icon="message-read" className={styles.item}>
|
||||
<Transition name="fade" activeKey={shouldRenderSkeleton ? 1 : 2} className={styles.transition}>
|
||||
{shouldRenderSkeleton ? <Skeleton className={styles.skeleton} /> : (
|
||||
<>
|
||||
{Boolean(readDate) && lang('PmReadAt', formatDateAtTime(lang, readDate * 1000))}
|
||||
{!readDate && shouldRenderShowWhen && (
|
||||
<div>
|
||||
{lang('PmRead')}
|
||||
<span className={styles.get} onClick={handleOpenModal}>
|
||||
{lang('PmReadShowWhen')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Transition>
|
||||
</MenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ReadTimeMenuItem);
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -7,11 +7,12 @@ import styles from './MenuSeparator.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
size?: 'thin' | 'thick';
|
||||
};
|
||||
|
||||
const MenuSeparator: FC<OwnProps> = ({ className }) => {
|
||||
const MenuSeparator: FC<OwnProps> = ({ className, size = 'thin' }) => {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)} />
|
||||
<div className={buildClassName(styles.root, styles[size], className)} />
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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[];
|
||||
|
||||
33
src/components/ui/Separator.module.scss
Normal file
33
src/components/ui/Separator.module.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/components/ui/Separator.tsx
Normal file
27
src/components/ui/Separator.tsx
Normal file
@ -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 (
|
||||
<div
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
className={buildClassName(styles.separator, className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Separator;
|
||||
@ -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<v
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadOutboxReadDate', async (global, actions, payload): Promise<void> => {
|
||||
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;
|
||||
|
||||
|
||||
@ -666,24 +666,29 @@ addActionHandler('loadGlobalPrivacySettings', async (global): Promise<void> => {
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = replaceSettings(global, {
|
||||
shouldArchiveAndMuteNewNonContact: globalSettings.shouldArchiveAndMuteNewNonContact,
|
||||
});
|
||||
global = replaceSettings(global, { ...globalSettings });
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload): Promise<void> => {
|
||||
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);
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -237,6 +237,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
wasTimeFormatSetManually: false,
|
||||
isConnectionStatusMinimized: true,
|
||||
shouldArchiveAndMuteNewNonContact: false,
|
||||
shouldHideReadMarks: false,
|
||||
canTranslate: false,
|
||||
canTranslateChats: true,
|
||||
doNotTranslate: [],
|
||||
|
||||
@ -19,3 +19,7 @@ export function selectCanSetPasscode<T extends GlobalState>(global: T) {
|
||||
export function selectTranslationLanguage<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey.translationLanguage || selectLanguageCode(global);
|
||||
}
|
||||
|
||||
export function selectShouldHideReadMarks<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey.shouldHideReadMarks;
|
||||
}
|
||||
|
||||
@ -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: ({
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -168,6 +168,7 @@
|
||||
"messages.getBotApp",
|
||||
"messages.requestAppWebView",
|
||||
"messages.togglePeerTranslations",
|
||||
"messages.getOutboxReadDate",
|
||||
"updates.getState",
|
||||
"updates.getDifference",
|
||||
"updates.getChannelDifference",
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
@ -101,6 +101,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
|
||||
wasTimeFormatSetManually: boolean;
|
||||
isConnectionStatusMinimized: boolean;
|
||||
shouldArchiveAndMuteNewNonContact?: boolean;
|
||||
shouldHideReadMarks?: boolean;
|
||||
canTranslate: boolean;
|
||||
canTranslateChats: boolean;
|
||||
translationLanguage?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user