Settings: Private message permissions (#4245)

This commit is contained in:
Alexander Zinchuk 2024-02-23 14:06:36 +01:00
parent 3d4ba7c47b
commit 7569689fb2
20 changed files with 264 additions and 36 deletions

View File

@ -615,17 +615,24 @@ export async function fetchGlobalPrivacySettings() {
return {
shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers),
shouldHideReadMarks: Boolean(result.hideReadMarks),
shouldNewNonContactPeersRequirePremium: Boolean(result.newNoncontactPeersRequirePremium),
};
}
export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonContact, shouldHideReadMarks }: {
shouldArchiveAndMuteNewNonContact: boolean;
shouldHideReadMarks: boolean;
export async function updateGlobalPrivacySettings({
shouldArchiveAndMuteNewNonContact,
shouldHideReadMarks,
shouldNewNonContactPeersRequirePremium,
}: {
shouldArchiveAndMuteNewNonContact?: boolean;
shouldHideReadMarks?: boolean;
shouldNewNonContactPeersRequirePremium?: boolean;
}) {
const result = await invokeRequest(new GramJs.account.SetGlobalPrivacySettings({
settings: new GramJs.GlobalPrivacySettings({
...(shouldArchiveAndMuteNewNonContact && { archiveAndMuteNewNoncontactPeers: true }),
...(shouldHideReadMarks && { hideReadMarks: true }),
...(shouldNewNonContactPeersRequirePremium && { newNoncontactPeersRequirePremium: true }),
}),
}));
@ -636,6 +643,7 @@ export async function updateGlobalPrivacySettings({ shouldArchiveAndMuteNewNonCo
return {
shouldArchiveAndMuteNewNonContact: Boolean(result.archiveAndMuteNewNoncontactPeers),
shouldHideReadMarks: Boolean(result.hideReadMarks),
shouldNewNonContactPeersRequirePremium: Boolean(result.newNoncontactPeersRequirePremium),
};
}

View File

@ -194,6 +194,7 @@ function LeftColumn({
case SettingsScreens.PrivacyForwarding:
case SettingsScreens.PrivacyGroupChats:
case SettingsScreens.PrivacyVoiceMessages:
case SettingsScreens.PrivacyMessages:
case SettingsScreens.PrivacyBlockedUsers:
case SettingsScreens.ActiveWebsites:
case SettingsScreens.TwoFaDisabled:

View File

@ -0,0 +1,33 @@
import React, { memo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import PremiumIcon from '../../common/PremiumIcon';
import ListItem from '../../ui/ListItem';
function PremiumStatusItem() {
const { openPremiumModal } = getActions();
const lang = useLang();
const handleOpenPremiumModal = useLastCallback(() => openPremiumModal());
return (
<div className="settings-item">
<ListItem
leftElement={<PremiumIcon className="icon" withGradient big />}
onClick={handleOpenPremiumModal}
>
{lang('PrivacyLastSeenPremium')}
</ListItem>
<p
className="settings-item-description-larger premium-info"
dir={lang.isRtl ? 'rtl' : undefined}
>
{lang('lng_messages_privacy_premium_about')}
</p>
</div>
);
}
export default memo(PremiumStatusItem);

View File

@ -0,0 +1,12 @@
.contacts_and_premium_option-title {
cursor: pointer;
}
.lock-icon {
font-size: 1.25rem;
position: absolute;
left: 1.0625rem;
top: 50%;
transform: translateY(-50%);
color: var(--color-gray);
}

View File

@ -0,0 +1,29 @@
import React, { memo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import useLang from '../../../hooks/useLang';
import Icon from '../../common/Icon';
import styles from './PrivacyLockedOption.module.scss';
type OwnProps = {
label: string;
};
function PrivacyLockedOption({ label }: OwnProps) {
const lang = useLang();
const { showNotification } = getActions();
return (
<div
className={styles.contactsAndPremiumOptionTitle}
onClick={() => showNotification({ message: lang('OptionPremiumRequiredMessage') })}
>
<span>{label}</span>
<Icon name="lock-badge" className={styles.lockIcon} />
</div>
);
}
export default memo(PrivacyLockedOption);

View File

@ -0,0 +1,80 @@
import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import { selectIsCurrentUserPremium, selectNewNoncontactPeersRequirePremium } from '../../../global/selectors';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import RadioGroup from '../../ui/RadioGroup';
import PremiumStatusItem from './PremiumStatusItem';
import PrivacyLockedOption from './PrivacyLockedOption';
type OwnProps = {
isActive?: boolean;
onReset: VoidFunction;
};
type StateProps = {
shouldNewNonContactPeersRequirePremium?: boolean;
isCurrentUserPremium?: boolean;
};
function PrivacyMessages({
isActive, onReset, shouldNewNonContactPeersRequirePremium, isCurrentUserPremium,
}: OwnProps & StateProps) {
const { updateGlobalPrivacySettings } = getActions();
const lang = useLang();
const options = useMemo(() => {
return [
{ value: 'everybody', label: lang('P2PEverybody') },
{
value: 'contacts_and_premium',
label: isCurrentUserPremium ? (
lang('PrivacyMessagesContactsAndPremium')
) : (
<PrivacyLockedOption label={lang('PrivacyMessagesContactsAndPremium')} />
),
hidden: !isCurrentUserPremium,
},
];
}, [lang, isCurrentUserPremium]);
const handleChange = useLastCallback((privacy: string) => {
updateGlobalPrivacySettings({ shouldNewNonContactPeersRequirePremium: privacy === 'contacts_and_premium' });
});
useHistoryBack({
isActive,
onBack: onReset,
});
return (
<>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('PrivacyMessagesTitle')}
</h4>
<RadioGroup
name="privacy-messages"
options={options}
onChange={handleChange}
selected={shouldNewNonContactPeersRequirePremium ? 'contacts_and_premium' : 'everybody'}
/>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('Privacy.Messages.SectionFooter')}
</p>
</div>
{!isCurrentUserPremium && <PremiumStatusItem />}
</>
);
}
export default memo(withGlobal<OwnProps>((global): StateProps => {
return {
shouldNewNonContactPeersRequirePremium: selectNewNoncontactPeersRequirePremium(global),
isCurrentUserPremium: selectIsCurrentUserPremium(global),
};
})(PrivacyMessages));

View File

@ -180,6 +180,10 @@
margin-top: 2rem;
margin-bottom: 0.75rem;
&.premium-info {
margin-top: 1rem;
}
&[dir="rtl"] {
text-align: right;
}

View File

@ -14,6 +14,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import Transition from '../../ui/Transition';
import SettingsFolders from './folders/SettingsFolders';
import SettingsPasscode from './passcode/SettingsPasscode';
import PrivacyMessages from './PrivacyMessages';
import SettingsActiveSessions from './SettingsActiveSessions';
import SettingsActiveWebsites from './SettingsActiveWebsites';
import SettingsCustomEmoji from './SettingsCustomEmoji';
@ -372,6 +373,14 @@ const Settings: FC<OwnProps> = ({
/>
);
case SettingsScreens.PrivacyMessages:
return (
<PrivacyMessages
isActive={isScreenActive}
onReset={handleReset}
/>
);
case SettingsScreens.Folders:
case SettingsScreens.FoldersCreateFolder:
case SettingsScreens.FoldersEditFolder:

View File

@ -119,6 +119,8 @@ const SettingsHeader: FC<OwnProps> = ({
return <h3>{lang('PrivacyForwards')}</h3>;
case SettingsScreens.PrivacyVoiceMessages:
return <h3>{lang('PrivacyVoiceMessages')}</h3>;
case SettingsScreens.PrivacyMessages:
return <h3>{lang('PrivacyMessages')}</h3>;
case SettingsScreens.PrivacyGroupChats:
return <h3>{lang('AutodownloadGroupChats')}</h3>;
case SettingsScreens.PrivacyPhoneCall:

View File

@ -10,6 +10,7 @@ import { selectCanSetPasscode, selectIsCurrentUserPremium } from '../../../globa
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import PremiumIcon from '../../common/PremiumIcon';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -30,6 +31,7 @@ type StateProps = {
canChangeSensitive?: boolean;
canDisplayAutoarchiveSetting: boolean;
shouldArchiveAndMuteNewNonContact?: boolean;
shouldNewNonContactPeersRequirePremium?: boolean;
canDisplayChatInTitle?: boolean;
privacyPhoneNumber?: ApiPrivacySettings;
privacyLastSeen?: ApiPrivacySettings;
@ -52,6 +54,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
canChangeSensitive,
canDisplayAutoarchiveSetting,
shouldArchiveAndMuteNewNonContact,
shouldNewNonContactPeersRequirePremium,
canDisplayChatInTitle,
canSetPasscode,
privacyPhoneNumber,
@ -73,7 +76,6 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
loadGlobalPrivacySettings,
updateGlobalPrivacySettings,
loadWebAuthorizations,
showNotification,
setSettingOption,
} = getActions();
@ -103,16 +105,6 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
});
}, [updateGlobalPrivacySettings]);
const handleVoiceMessagesClick = useCallback(() => {
if (isCurrentUserPremium) {
onScreenSelect(SettingsScreens.PrivacyVoiceMessages);
} else {
showNotification({
message: lang('PrivacyVoiceMessagesPremiumOnly'),
});
}
}, [isCurrentUserPremium, lang, onScreenSelect, showNotification]);
const handleChatInTitleChange = useCallback((isChecked: boolean) => {
setSettingOption({
canDisplayChatInTitle: isChecked,
@ -298,11 +290,11 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
</ListItem>
<ListItem
narrow
disabled={!isCurrentUserPremium}
allowDisabledClick
rightElement={!isCurrentUserPremium && <i className="icon icon-lock-badge settings-icon-locked" />}
rightElement={isCurrentUserPremium && <PremiumIcon big withGradient />}
className="no-icon"
onClick={handleVoiceMessagesClick}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.PrivacyVoiceMessages)}
>
<div className="multiline-menu-item">
<span className="title">{lang('PrivacyVoiceMessagesTitle')}</span>
@ -311,6 +303,22 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
</span>
</div>
</ListItem>
<ListItem
narrow
rightElement={isCurrentUserPremium && <PremiumIcon big withGradient />}
className="no-icon"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => onScreenSelect(SettingsScreens.PrivacyMessages)}
>
<div className="multiline-menu-item">
<span className="title">{lang('PrivacyMessagesTitle')}</span>
<span className="subtitle" dir="auto">
{shouldNewNonContactPeersRequirePremium
? lang('PrivacyMessagesContactsAndPremium')
: lang('P2PEverybody')}
</span>
</div>
</ListItem>
</div>
{canDisplayAutoarchiveSetting && (
@ -362,7 +370,7 @@ export default memo(withGlobal<OwnProps>(
settings: {
byKey: {
hasPassword, isSensitiveEnabled, canChangeSensitive, shouldArchiveAndMuteNewNonContact,
canDisplayChatInTitle,
canDisplayChatInTitle, shouldNewNonContactPeersRequirePremium,
},
privacy,
},
@ -383,6 +391,7 @@ export default memo(withGlobal<OwnProps>(
canDisplayAutoarchiveSetting: Boolean(appConfig?.canDisplayAutoarchiveSetting),
shouldArchiveAndMuteNewNonContact,
canChangeSensitive,
shouldNewNonContactPeersRequirePremium,
privacyPhoneNumber: privacy.phoneNumber,
privacyLastSeen: privacy.lastSeen,
privacyProfilePhoto: privacy.profilePhoto,

View File

@ -1,3 +0,0 @@
:global(.settings-item-description-larger).premiumInfo {
margin-top: 1rem;
}

View File

@ -4,7 +4,6 @@ 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';
@ -14,8 +13,6 @@ 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;
};
@ -59,10 +56,7 @@ const SettingsPrivacyLastSeen = ({
{isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')}
</ListItem>
<p
className={buildClassName(
'settings-item-description-larger',
styles.premiumInfo,
)}
className="settings-item-description-larger premium-info"
dir={lang.isRtl ? 'rtl' : undefined}
>
{isCurrentUserPremium

View File

@ -6,7 +6,7 @@ import type { ApiPhoto } from '../../../api/types';
import type { ApiPrivacySettings } from '../../../types';
import { SettingsScreens } from '../../../types';
import { selectUserFullInfo } from '../../../global/selectors';
import { selectIsCurrentUserPremium, selectUserFullInfo } from '../../../global/selectors';
import { getPrivacyKey } from './helpers/privacy';
import useHistoryBack from '../../../hooks/useHistoryBack';
@ -15,6 +15,8 @@ import useLastCallback from '../../../hooks/useLastCallback';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import PremiumStatusItem from './PremiumStatusItem';
import PrivacyLockedOption from './PrivacyLockedOption';
import SettingsPrivacyLastSeen from './SettingsPrivacyLastSeen';
import SettingsPrivacyPublicProfilePhoto from './SettingsPrivacyPublicProfilePhoto';
@ -31,6 +33,7 @@ type StateProps = {
currentUserFallbackPhoto?: ApiPhoto;
primaryPrivacy?: ApiPrivacySettings;
secondaryPrivacy?: ApiPrivacySettings;
isPremiumRequired?: boolean;
};
const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
@ -41,6 +44,7 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
currentUserId,
hasCurrentUserFullInfo,
currentUserFallbackPhoto,
isPremiumRequired,
onScreenSelect,
onReset,
}) => {
@ -67,6 +71,7 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
screen={screen}
privacy={primaryPrivacy}
onScreenSelect={onScreenSelect}
isPremiumRequired={isPremiumRequired}
/>
{screen === SettingsScreens.PrivacyProfilePhoto && primaryPrivacy?.visibility !== 'everybody' && (
<SettingsPrivacyPublicProfilePhoto
@ -93,9 +98,11 @@ function PrivacySubsection({
screen,
privacy,
onScreenSelect,
isPremiumRequired,
}: {
screen: SettingsScreens;
privacy?: ApiPrivacySettings;
isPremiumRequired?: boolean;
onScreenSelect: (screen: SettingsScreens) => void;
}) {
const { setPrivacyVisibility } = getActions();
@ -105,13 +112,30 @@ function PrivacySubsection({
const hasNobody = screen !== SettingsScreens.PrivacyAddByPhone;
const options = [
{ value: 'everybody', label: lang('P2PEverybody') },
{ value: 'contacts', label: lang('P2PContacts') },
{
value: 'contacts',
label: isPremiumRequired ? (
<PrivacyLockedOption label={lang('P2PContacts')} />
) : (
lang('P2PContacts')
),
hidden: isPremiumRequired,
},
];
if (hasNobody) {
options.push({ value: 'nobody', label: lang('P2PNobody') });
options.push({
value: 'nobody',
label: isPremiumRequired ? (
<PrivacyLockedOption label={lang('P2PNobody')} />
) : (
lang('P2PNobody')
),
hidden: isPremiumRequired,
});
}
return options;
}, [lang, screen]);
}, [lang, screen, isPremiumRequired]);
const primaryExceptionLists = useMemo(() => {
if (screen === SettingsScreens.PrivacyAddByPhone) {
@ -136,6 +160,8 @@ function PrivacySubsection({
case SettingsScreens.PrivacyAddByPhone: {
return privacy?.visibility === 'everybody' ? lang('PrivacyPhoneInfo') : lang('PrivacyPhoneInfo3');
}
case SettingsScreens.PrivacyVoiceMessages:
return lang('PrivacyVoiceMessagesInfo');
default:
return undefined;
}
@ -257,7 +283,7 @@ function PrivacySubsection({
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>{descriptionText}</p>
)}
</div>
{(primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
{!isPremiumRequired && (primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
<div className="settings-item">
<h4 className="settings-item-header mb-4" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('PrivacyExceptions')}
@ -294,6 +320,7 @@ function PrivacySubsection({
)}
</div>
)}
{isPremiumRequired && <PremiumStatusItem />}
</>
);
}
@ -361,6 +388,7 @@ export default memo(withGlobal<OwnProps>(
currentUserId: currentUserId!,
hasCurrentUserFullInfo: Boolean(currentUserFullInfo),
currentUserFallbackPhoto: currentUserFullInfo?.fallbackPhoto,
isPremiumRequired: screen === SettingsScreens.PrivacyVoiceMessages && !selectIsCurrentUserPremium(global),
};
},
)(SettingsPrivacyVisibility));

View File

@ -20,6 +20,7 @@ type OwnProps = {
disabled?: boolean;
hidden?: boolean;
isLoading?: boolean;
className?: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
};
@ -33,18 +34,20 @@ const Radio: FC<OwnProps> = ({
disabled,
hidden,
isLoading,
className,
onChange,
}) => {
const lang = useLang();
const className = buildClassName(
const fullClassName = buildClassName(
'Radio',
className,
disabled && 'disabled',
hidden && 'hidden-widget',
isLoading && 'loading',
);
return (
<label className={className} dir={lang.isRtl ? 'rtl' : undefined}>
<label className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
<input
type="radio"
name={name}

View File

@ -9,6 +9,7 @@ export type IRadioOption = {
subLabel?: string;
value: string;
hidden?: boolean;
className?: string;
};
type OwnProps = {
@ -47,6 +48,7 @@ const RadioGroup: FC<OwnProps> = ({
hidden={option.hidden}
disabled={disabled}
isLoading={loadingOption ? loadingOption === option.value : undefined}
className={option.className}
onChange={handleChange}
/>
))}

View File

@ -674,6 +674,8 @@ addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload)
const shouldArchiveAndMuteNewNonContact = payload.shouldArchiveAndMuteNewNonContact
?? Boolean(global.settings.byKey.shouldArchiveAndMuteNewNonContact);
const shouldHideReadMarks = payload.shouldHideReadMarks ?? Boolean(global.settings.byKey.shouldHideReadMarks);
const shouldNewNonContactPeersRequirePremium = payload.shouldNewNonContactPeersRequirePremium
?? Boolean(global.settings.byKey.shouldNewNonContactPeersRequirePremium);
global = replaceSettings(global, { shouldArchiveAndMuteNewNonContact, shouldHideReadMarks });
setGlobal(global);
@ -681,6 +683,7 @@ addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload)
const result = await callApi('updateGlobalPrivacySettings', {
shouldArchiveAndMuteNewNonContact,
shouldHideReadMarks,
shouldNewNonContactPeersRequirePremium,
});
global = getGlobal();
@ -689,6 +692,9 @@ addActionHandler('updateGlobalPrivacySettings', async (global, actions, payload)
? !shouldArchiveAndMuteNewNonContact
: result.shouldArchiveAndMuteNewNonContact,
shouldHideReadMarks: !result ? !shouldHideReadMarks : result.shouldHideReadMarks,
shouldNewNonContactPeersRequirePremium: !result
? !shouldNewNonContactPeersRequirePremium
: result.shouldNewNonContactPeersRequirePremium,
});
setGlobal(global);
});

View File

@ -242,6 +242,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
wasTimeFormatSetManually: false,
isConnectionStatusMinimized: true,
shouldArchiveAndMuteNewNonContact: false,
shouldNewNonContactPeersRequirePremium: false,
shouldHideReadMarks: false,
canTranslate: false,
canTranslateChats: true,

View File

@ -20,6 +20,10 @@ export function selectTranslationLanguage<T extends GlobalState>(global: T) {
return global.settings.byKey.translationLanguage || selectLanguageCode(global);
}
export function selectNewNoncontactPeersRequirePremium<T extends GlobalState>(global: T) {
return global.settings.byKey.shouldNewNonContactPeersRequirePremium;
}
export function selectShouldHideReadMarks<T extends GlobalState>(global: T) {
return global.settings.byKey.shouldHideReadMarks;
}

View File

@ -2873,7 +2873,11 @@ export interface ActionPayloads {
} & WithTabId;
closeShareChatFolderModal: undefined | WithTabId;
loadGlobalPrivacySettings: undefined;
updateGlobalPrivacySettings: { shouldArchiveAndMuteNewNonContact?: boolean; shouldHideReadMarks?: boolean };
updateGlobalPrivacySettings: {
shouldArchiveAndMuteNewNonContact?: boolean;
shouldHideReadMarks?: boolean;
shouldNewNonContactPeersRequirePremium?: boolean;
};
// Premium
openPremiumModal: ({

View File

@ -101,6 +101,7 @@ export interface ISettings extends NotifySettings, Record<string, any> {
wasTimeFormatSetManually: boolean;
isConnectionStatusMinimized: boolean;
shouldArchiveAndMuteNewNonContact?: boolean;
shouldNewNonContactPeersRequirePremium?: boolean;
shouldHideReadMarks?: boolean;
canTranslate: boolean;
canTranslateChats: boolean;
@ -183,6 +184,7 @@ export enum SettingsScreens {
PrivacyPhoneP2P,
PrivacyForwarding,
PrivacyVoiceMessages,
PrivacyMessages,
PrivacyGroupChats,
PrivacyPhoneNumberAllowedContacts,
PrivacyPhoneNumberDeniedContacts,