432 lines
13 KiB
TypeScript
432 lines
13 KiB
TypeScript
import React, {
|
|
memo, useEffect, useMemo, useState,
|
|
} from '../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../global';
|
|
|
|
import type { ApiStory, ApiUser } from '../../api/types';
|
|
import type { ApiPrivacySettings, PrivacyVisibility } from '../../types';
|
|
import type { IconName } from '../../types/icons';
|
|
|
|
import { getSenderTitle, getUserFullName } from '../../global/helpers';
|
|
import { selectPeerStory, selectTabState } from '../../global/selectors';
|
|
import buildClassName from '../../util/buildClassName';
|
|
import stopEvent from '../../util/stopEvent';
|
|
|
|
import useFlag from '../../hooks/useFlag';
|
|
import useLastCallback from '../../hooks/useLastCallback';
|
|
import useOldLang from '../../hooks/useOldLang';
|
|
|
|
import Button from '../ui/Button';
|
|
import ListItem from '../ui/ListItem';
|
|
import Modal from '../ui/Modal';
|
|
import Switcher from '../ui/Switcher';
|
|
import Transition from '../ui/Transition';
|
|
import AllowDenyList from './privacy/AllowDenyList';
|
|
import CloseFriends from './privacy/CloseFriends';
|
|
|
|
import styles from './StorySettings.module.scss';
|
|
|
|
interface OwnProps {
|
|
isOpen?: boolean;
|
|
onClose?: NoneToVoidFunction;
|
|
}
|
|
|
|
interface StateProps {
|
|
story?: ApiStory;
|
|
visibility?: ApiPrivacySettings;
|
|
contactListIds?: string[];
|
|
usersById: Record<string, ApiUser>;
|
|
currentUserId: string;
|
|
}
|
|
|
|
type PrivacyAction = 'blockUserIds' | 'closeFriends' | 'blockContactUserIds' | 'allowUserIds';
|
|
|
|
interface PrivacyOption {
|
|
name: string;
|
|
value: PrivacyVisibility;
|
|
color: [string, string];
|
|
icon: IconName;
|
|
actions?: PrivacyAction;
|
|
}
|
|
|
|
const OPTIONS: PrivacyOption[] = [{
|
|
name: 'StoryPrivacyOptionEveryone',
|
|
value: 'everybody',
|
|
color: ['#50ABFF', '#007AFF'],
|
|
icon: 'channel-filled',
|
|
actions: 'blockUserIds',
|
|
}, {
|
|
name: 'StoryPrivacyOptionContacts',
|
|
value: 'contacts',
|
|
color: ['#C36EFF', '#8B60FA'],
|
|
icon: 'user-filled',
|
|
actions: 'blockContactUserIds',
|
|
}, {
|
|
name: 'StoryPrivacyOptionCloseFriends',
|
|
value: 'closeFriends',
|
|
color: ['#88D93A', '#30B73B'],
|
|
icon: 'favorite-filled',
|
|
actions: 'closeFriends',
|
|
}, {
|
|
name: 'StoryPrivacyOptionSelectedContacts',
|
|
value: 'nobody',
|
|
color: ['#FFB743', '#F69A36'],
|
|
icon: 'group-filled',
|
|
actions: 'allowUserIds',
|
|
}];
|
|
|
|
enum Screens {
|
|
privacy,
|
|
allowList,
|
|
closeFriends,
|
|
denyList,
|
|
}
|
|
|
|
function StorySettings({
|
|
isOpen,
|
|
story,
|
|
visibility,
|
|
contactListIds,
|
|
usersById,
|
|
currentUserId,
|
|
onClose,
|
|
}: OwnProps & StateProps) {
|
|
const { editStoryPrivacy, toggleStoryInProfile } = getActions();
|
|
|
|
const lang = useOldLang();
|
|
const [isOpenModal, openModal, closeModal] = useFlag(false);
|
|
const [privacy, setPrivacy] = useState<ApiPrivacySettings | undefined>(visibility);
|
|
const [isPinned, setIsPinned] = useState(story?.isInProfile);
|
|
const [activeKey, setActiveKey] = useState<Screens>(Screens.privacy);
|
|
const [editingBlockingCategory, setEditingBlockingCategory] = useState<PrivacyVisibility>('everybody');
|
|
const isBackButton = activeKey !== Screens.privacy;
|
|
|
|
const closeFriendIds = useMemo(() => {
|
|
return (contactListIds || []).filter((userId) => usersById[userId]?.isCloseFriend);
|
|
}, [contactListIds, usersById]);
|
|
|
|
const lockedIds = useMemo(() => {
|
|
if (activeKey === Screens.allowList
|
|
&& (!privacy?.allowUserIds?.length || privacy.allowUserIds[0] === currentUserId)
|
|
) {
|
|
return [currentUserId];
|
|
}
|
|
|
|
return undefined;
|
|
}, [activeKey, currentUserId, privacy?.allowUserIds]);
|
|
|
|
const selectedBlockedIds = useMemo(() => {
|
|
if (editingBlockingCategory !== privacy?.visibility) return [];
|
|
return privacy?.blockUserIds || [];
|
|
}, [editingBlockingCategory, privacy?.blockUserIds, privacy?.visibility]);
|
|
|
|
const handleAllowUserIdsChange = useLastCallback((newIds: string[]) => {
|
|
setPrivacy({
|
|
...privacy!,
|
|
allowUserIds: newIds?.length ? newIds?.filter((id) => id !== currentUserId) : [currentUserId],
|
|
});
|
|
});
|
|
|
|
const handleDenyUserIdsChange = useLastCallback((newIds: string[]) => {
|
|
setPrivacy({
|
|
...privacy!,
|
|
blockUserIds: newIds,
|
|
visibility: editingBlockingCategory,
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setActiveKey(Screens.privacy);
|
|
openModal();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
useEffect(() => {
|
|
setPrivacy(visibility);
|
|
}, [visibility]);
|
|
|
|
const handleCloseButtonClick = useLastCallback(() => {
|
|
if (activeKey === Screens.privacy) {
|
|
closeModal();
|
|
return;
|
|
}
|
|
|
|
setActiveKey(Screens.privacy);
|
|
});
|
|
|
|
function handleVisibilityChange(newVisibility: PrivacyVisibility) {
|
|
setPrivacy({
|
|
...privacy!,
|
|
visibility: newVisibility,
|
|
});
|
|
}
|
|
|
|
function handleActionClick(e: React.MouseEvent<HTMLDivElement>, action: PrivacyAction) {
|
|
stopEvent(e);
|
|
|
|
switch (action) {
|
|
case 'closeFriends':
|
|
setActiveKey(Screens.closeFriends);
|
|
break;
|
|
case 'allowUserIds':
|
|
setActiveKey(Screens.allowList);
|
|
break;
|
|
case 'blockUserIds':
|
|
setActiveKey(Screens.denyList);
|
|
setEditingBlockingCategory('everybody');
|
|
break;
|
|
case 'blockContactUserIds':
|
|
setActiveKey(Screens.denyList);
|
|
setEditingBlockingCategory('contacts');
|
|
break;
|
|
}
|
|
}
|
|
|
|
const handleIsPinnedToggle = useLastCallback(() => {
|
|
setIsPinned(!isPinned);
|
|
});
|
|
|
|
// console.warn(privacy?.visibility, story?.visibility, OPTIONS);
|
|
|
|
const handleSubmit = useLastCallback(() => {
|
|
editStoryPrivacy({
|
|
peerId: story!.peerId,
|
|
storyId: story!.id,
|
|
privacy: privacy!,
|
|
});
|
|
if (story!.isInProfile !== isPinned) {
|
|
toggleStoryInProfile({ peerId: story!.peerId, storyId: story!.id, isInProfile: isPinned });
|
|
}
|
|
closeModal();
|
|
});
|
|
|
|
function renderActionName(action: PrivacyAction) {
|
|
if (action === 'closeFriends') {
|
|
if (closeFriendIds.length === 0) {
|
|
return lang('StoryPrivacyOptionCloseFriendsDetail');
|
|
}
|
|
|
|
if (closeFriendIds.length === 1) {
|
|
return getSenderTitle(lang, usersById[closeFriendIds[0]]);
|
|
}
|
|
|
|
return lang('StoryPrivacyOptionPeople', closeFriendIds.length, 'i');
|
|
}
|
|
|
|
if ((action === 'blockUserIds' && privacy?.visibility === 'everybody')
|
|
|| (action === 'blockContactUserIds' && privacy?.visibility === 'contacts')) {
|
|
if (!privacy?.blockUserIds?.length) {
|
|
return lang('StoryPrivacyOptionContactsDetail');
|
|
}
|
|
|
|
if (privacy.blockUserIds.length === 1) {
|
|
return lang('StoryPrivacyOptionExcludePerson', getUserFullName(usersById[privacy.blockUserIds[0]]));
|
|
}
|
|
|
|
return lang('StoryPrivacyOptionExcludePeople', privacy.blockUserIds.length, 'i');
|
|
}
|
|
|
|
if (!privacy?.allowUserIds || privacy.allowUserIds.length === 0) {
|
|
return lang('StoryPrivacyOptionSelectedContactsDetail');
|
|
}
|
|
|
|
if (privacy.allowUserIds.length === 1) {
|
|
return getUserFullName(usersById[privacy.allowUserIds[0]]);
|
|
}
|
|
|
|
return lang('StoryPrivacyOptionPeople', privacy.allowUserIds.length, 'i');
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function renderHeaderContent() {
|
|
switch (activeKey) {
|
|
case Screens.privacy:
|
|
return <h3 className={styles.headerTitle}>{lang('StoryPrivacyAlertEditTitle')}</h3>;
|
|
case Screens.allowList:
|
|
return <h3 className={styles.headerTitle}>{lang('StoryPrivacyAlertSelectContactsTitle')}</h3>;
|
|
case Screens.closeFriends:
|
|
return <h3 className={styles.headerTitle}>{lang('CloseFriends')}</h3>;
|
|
case Screens.denyList:
|
|
return <h3 className={styles.headerTitle}>{lang('StoryPrivacyAlertExcludedContactsTitle')}</h3>;
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line consistent-return
|
|
function renderContent(isActive: boolean) {
|
|
switch (activeKey) {
|
|
case Screens.privacy:
|
|
return renderPrivacyList();
|
|
case Screens.closeFriends:
|
|
return (
|
|
<CloseFriends
|
|
key="close-friends"
|
|
isActive={isActive}
|
|
contactListIds={contactListIds}
|
|
currentUserId={currentUserId}
|
|
usersById={usersById}
|
|
onClose={handleCloseButtonClick}
|
|
/>
|
|
);
|
|
case Screens.denyList:
|
|
return (
|
|
<AllowDenyList
|
|
key="deny-list"
|
|
id="deny-list"
|
|
contactListIds={contactListIds}
|
|
currentUserId={currentUserId}
|
|
usersById={usersById}
|
|
selectedIds={selectedBlockedIds}
|
|
onSelect={handleDenyUserIdsChange}
|
|
/>
|
|
);
|
|
case Screens.allowList:
|
|
return (
|
|
<AllowDenyList
|
|
key="allow-list"
|
|
id="allow-list"
|
|
contactListIds={contactListIds}
|
|
lockedIds={lockedIds}
|
|
currentUserId={currentUserId}
|
|
usersById={usersById}
|
|
selectedIds={privacy?.allowUserIds}
|
|
onSelect={handleAllowUserIdsChange}
|
|
/>
|
|
);
|
|
}
|
|
}
|
|
|
|
function renderPrivacyList() {
|
|
const storyLifeTime = story ? convertSecondsToHours(story.expireDate - story.date) : 0;
|
|
|
|
return (
|
|
<>
|
|
<div className={styles.section}>
|
|
<h3 className={styles.title}>{lang('StoryPrivacyAlertSubtitleProfile')}</h3>
|
|
<div className={styles.list}>
|
|
{OPTIONS.map((option) => (
|
|
<label
|
|
key={option.value}
|
|
className={buildClassName(styles.option, option.value === privacy?.visibility && styles.checked)}
|
|
>
|
|
<input
|
|
type="radio"
|
|
name="story_privacy"
|
|
className={styles.input}
|
|
value={option.value}
|
|
checked={option.value === privacy?.visibility}
|
|
onChange={() => handleVisibilityChange(option.value)}
|
|
teactExperimentControlled
|
|
/>
|
|
<span
|
|
className={styles.icon}
|
|
style={`--color-from: ${option.color[0]}; --color-to: ${option.color[1]}`}
|
|
>
|
|
<i className={`icon icon-${option.icon}`} />
|
|
</span>
|
|
<div className={styles.optionContent}>
|
|
<span className={buildClassName(styles.option_name)}>{lang(option.name)}</span>
|
|
{option.actions && (
|
|
<div
|
|
tabIndex={0}
|
|
role="button"
|
|
className={styles.action}
|
|
aria-label={lang('Edit')}
|
|
onClick={(e) => { handleActionClick(e, option.actions!); }}
|
|
>
|
|
<span className={styles.actionInner}>{renderActionName(option.actions)}</span>
|
|
<i className="icon icon-next" aria-hidden />
|
|
</div>
|
|
)}
|
|
</div>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className={styles.section}>
|
|
<ListItem ripple onClick={handleIsPinnedToggle}>
|
|
<span>{lang('StoryKeep')}</span>
|
|
<Switcher
|
|
id="group-notifications"
|
|
label={lang('StoryKeep')}
|
|
checked={isPinned}
|
|
inactive
|
|
/>
|
|
</ListItem>
|
|
</div>
|
|
<div className={styles.footer}>
|
|
<div className={styles.info}>{lang('StoryKeepInfo', storyLifeTime)}</div>
|
|
<div className={styles.submit}>
|
|
<Button onClick={handleSubmit}>{lang('StoryPrivacyButtonSave')}</Button>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isOpenModal}
|
|
className={buildClassName(styles.modal, 'component-theme-dark')}
|
|
onClose={closeModal}
|
|
noBackdrop
|
|
onCloseAnimationEnd={onClose}
|
|
>
|
|
<div className={styles.header}>
|
|
<Button
|
|
className={buildClassName(styles.closeButton, 'close-button')}
|
|
round
|
|
color="translucent"
|
|
size="smaller"
|
|
onClick={handleCloseButtonClick}
|
|
ariaLabel={isBackButton ? lang('Common.Back') : lang('Common.Close')}
|
|
>
|
|
<div className={buildClassName('animated-close-icon', isBackButton && 'state-back')} />
|
|
</Button>
|
|
<Transition name="slideFade" activeKey={activeKey}>
|
|
{renderHeaderContent()}
|
|
</Transition>
|
|
</div>
|
|
<Transition
|
|
activeKey={activeKey}
|
|
name="slideFade"
|
|
slideClassName="ChatOrUserPicker_slide"
|
|
className={styles.content}
|
|
>
|
|
{renderContent}
|
|
</Transition>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
|
const {
|
|
storyViewer: {
|
|
storyId, peerId,
|
|
},
|
|
} = selectTabState(global);
|
|
const story = (peerId && storyId)
|
|
? selectPeerStory(global, peerId, storyId)
|
|
: undefined;
|
|
|
|
return {
|
|
story: story && 'content' in story ? story as ApiStory : undefined,
|
|
visibility: story && 'visibility' in story ? story.visibility : undefined,
|
|
contactListIds: global.contactList?.userIds,
|
|
usersById: global.users.byId,
|
|
currentUserId: global.currentUserId!,
|
|
};
|
|
})(StorySettings));
|
|
|
|
function convertSecondsToHours(seconds: number): number {
|
|
const secondsInHour = 3600;
|
|
const minutesInHour = 60;
|
|
|
|
const hours = Math.floor(seconds / secondsInHour);
|
|
const remainingSeconds = seconds % secondsInHour;
|
|
const remainingMinutes = Math.floor(remainingSeconds / minutesInHour);
|
|
|
|
// If remaining minutes are greater than or equal to 30, round up the hours
|
|
return remainingMinutes >= 30 ? hours + 1 : hours;
|
|
}
|