Settings: Redesign (#6922)

Co-authored-by: Alexander Zinchuk <alx.zinchuk@gmail.com>
This commit is contained in:
Alexander Zinchuk 2026-06-01 01:15:35 +02:00
parent c4b1bad481
commit 60f3995e82
105 changed files with 2216 additions and 1978 deletions

View File

@ -22,6 +22,7 @@ type OwnProps = {
isDisabled?: boolean;
className?: string;
withShare?: boolean;
noTitle?: boolean;
onRevoke?: VoidFunction;
};
@ -31,6 +32,7 @@ const InviteLink: FC<OwnProps> = ({
isDisabled,
className,
withShare,
noTitle,
onRevoke,
}) => {
const lang = useLang();
@ -76,9 +78,11 @@ const InviteLink: FC<OwnProps> = ({
return (
<div className={className}>
<p className={styles.title}>
{oldLang(title || 'InviteLink.InviteLink')}
</p>
{!noTitle && (
<p className={styles.title}>
{oldLang(title || 'InviteLink.InviteLink')}
</p>
)}
<div className={styles.primaryLink}>
<input
className={buildClassName('form-control', styles.input)}

View File

@ -1,29 +1,5 @@
@use "../../styles/mixins";
.container {
margin-bottom: 0.625rem;
padding: 1.5rem 1.5rem 0;
@include mixins.side-panel-section;
}
.header {
position: relative;
font-size: 1rem;
color: var(--color-text-secondary);
&[dir="rtl"] {
text-align: right;
}
}
.description {
margin-bottom: 0;
padding-top: 0.5rem;
padding-bottom: 1.5rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
padding-inline: 1.5rem;
}
.sortableContainer {

View File

@ -14,6 +14,7 @@ import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import Island, { IslandDescription, IslandTitle } from '../gili/layout/Island';
import ConfirmDialog from '../ui/ConfirmDialog';
import Draggable from '../ui/Draggable';
import ListItem from '../ui/ListItem';
@ -147,10 +148,10 @@ const ManageUsernames: FC<OwnProps> = ({
return (
<>
<div className={styles.container}>
<h4 className={styles.header} dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_usernames_subtitle')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_usernames_subtitle')}
</IslandTitle>
<Island className={styles.container}>
<div className={styles.sortableContainer} style={`height: ${(usernames.length) * USERNAME_HEIGHT_PX}px`}>
{usernames.map((usernameData, i) => {
const isDragged = state.draggedIndex === i;
@ -201,10 +202,10 @@ const ManageUsernames: FC<OwnProps> = ({
);
})}
</div>
<p className={styles.description} dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_usernames_description')}
</p>
</div>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_usernames_description')}
</IslandDescription>
<ConfirmDialog
isOpen={Boolean(usernameForConfirm)}
onClose={closeConfirmUsernameDialog}

View File

@ -21,6 +21,7 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Island from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import InfiniteScroll from '../../ui/InfiniteScroll';
import InputText from '../../ui/InputText';
@ -93,6 +94,7 @@ type OwnProps<CategoryType extends string> = {
withPeerTypes?: boolean;
withPeerUsernames?: boolean;
withDefaultPadding?: boolean;
withIslands?: boolean;
onFilterChange?: (value: string) => void;
onDisabledClick?: (id: string, isSelected: boolean) => void;
onLoadMore?: () => void;
@ -125,6 +127,7 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
withPeerTypes,
withPeerUsernames,
withDefaultPadding,
withIslands,
onFilterChange,
onDisabledClick,
onLoadMore,
@ -250,6 +253,19 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
onFilterChange?.(value);
});
function renderSearchInput() {
return (
<InputText
id={searchInputId}
ref={inputRef}
value={filterValue}
onChange={handleFilterChange}
placeholder={filterPlaceholder || oldLang('SelectChat')}
noMargin={withIslands}
/>
);
}
const [viewportIds, getMore] = useInfiniteScroll(
onLoadMore, sortedItemIds, Boolean(filterValue),
);
@ -387,62 +403,78 @@ const PeerPicker = <CategoryType extends string = CustomPeerType>({
? sections.some((s) => s.ids.length > 0)
: Boolean(viewportIds?.length);
const SearchWrapper = withIslands ? Island : 'div';
const ListWrapper = withIslands ? Island : 'div';
function renderListWrapper(content: TeactNode) {
return withIslands
? <ListWrapper className={styles.islandList}>{content}</ListWrapper>
: content;
}
return (
<div className={buildClassName(styles.container, className)}>
{isSearchable && (
<div className={buildClassName(styles.header, 'custom-scroll')} dir={oldLang.isRtl ? 'rtl' : undefined}>
{selectedCategories?.map((category) => (
<PeerChip
className={styles.peerChip}
customPeer={categoriesByType[category]}
onClick={handleItemClick}
clickArg={category}
canClose
/>
))}
{lockedSelectedIds?.map((id, i) => (
<PeerChip
className={styles.peerChip}
peerId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
forceShowSelf={forceShowSelf}
onClick={handleItemClick}
clickArg={id}
/>
))}
{unlockedSelectedIds.map((id, i) => (
<PeerChip
className={styles.peerChip}
peerId={id}
isMinimized={
shouldMinimize && i + (lockedSelectedIds?.length || 0) < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
}
canClose
onClick={handleItemClick}
clickArg={id}
/>
))}
<InputText
id={searchInputId}
ref={inputRef}
value={filterValue}
onChange={handleFilterChange}
placeholder={filterPlaceholder || oldLang('SelectChat')}
/>
</div>
<>
{(!withIslands || selectedIds.length > 0 || (selectedCategories && selectedCategories.length > 0)) && (
<SearchWrapper
className={buildClassName(withIslands ? styles.islandHeader : styles.header, 'custom-scroll')}
dir={oldLang.isRtl ? 'rtl' : undefined}
>
{selectedCategories?.map((category) => (
<PeerChip
className={styles.peerChip}
customPeer={categoriesByType[category]}
onClick={handleItemClick}
clickArg={category}
canClose
/>
))}
{lockedSelectedIds?.map((id, i) => (
<PeerChip
className={styles.peerChip}
peerId={id}
isMinimized={shouldMinimize && i < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT}
forceShowSelf={forceShowSelf}
onClick={handleItemClick}
clickArg={id}
/>
))}
{unlockedSelectedIds.map((id, i) => (
<PeerChip
className={styles.peerChip}
peerId={id}
isMinimized={
shouldMinimize
&& i + (lockedSelectedIds?.length || 0) < selectedIds.length - ALWAYS_FULL_ITEMS_COUNT
}
canClose
onClick={handleItemClick}
clickArg={id}
/>
))}
{!withIslands && renderSearchInput()}
</SearchWrapper>
)}
{withIslands && (
<Island>{renderSearchInput()}</Island>
)}
</>
)}
{hasContent ? (
<InfiniteScroll
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
items={viewportIds}
itemSelector={`.${ITEM_CLASS_NAME}`}
beforeChildren={beforeChildren}
onLoadMore={getMore}
noScrollRestore={noScrollRestore}
>
{renderItems()}
</InfiniteScroll>
renderListWrapper(
<InfiniteScroll
className={buildClassName(styles.pickerList, withDefaultPadding && styles.padded, 'custom-scroll')}
items={viewportIds}
itemSelector={`.${ITEM_CLASS_NAME}`}
beforeChildren={beforeChildren}
onLoadMore={getMore}
noScrollRestore={noScrollRestore}
>
{renderItems()}
</InfiniteScroll>,
)
) : !isLoading && viewportIds && !viewportIds.length ? (
<p className={styles.noResults}>{notFoundText || 'Sorry, nothing found.'}</p>
) : (

View File

@ -32,6 +32,16 @@
}
}
.islandHeader {
overflow-y: auto;
display: flex;
flex-flow: row wrap;
flex-shrink: 0;
max-height: 16.5rem;
padding-bottom: 0;
}
.pickerCategoryTitle {
margin-bottom: 0.5rem;
padding-inline: 0.5rem;
@ -77,6 +87,16 @@
}
}
.islandList {
overflow: hidden;
display: flex;
flex: 1 1 auto;
flex-direction: column;
min-height: 0;
padding: 0;
}
.noResults {
display: flex;
align-items: center;

View File

@ -17,12 +17,28 @@
margin-top: 1rem;
}
.left, .bottom {
.left {
display: flex;
flex-direction: column;
line-height: 1.4;
}
.bottom {
overflow: hidden;
display: flex;
flex-direction: column;
max-height: 25rem;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: max-height 0.25s ease-in-out, opacity 0.25s ease-in-out;
}
.collapsed {
max-height: 0;
opacity: 0;
}
.status {
font-size: 0.875rem;
color: var(--color-error);
@ -108,31 +124,3 @@
animation-name: none;
}
}
@include mixins.on-active-vt('profileBusinessHoursExpand') {
&::view-transition-old(.expandArrow) {
animation-name: vt-expand-icon-spin;
}
&::view-transition-new(.expandArrow) {
display: none;
}
&::view-transition-old(.businessHours) {
display: none;
}
}
@include mixins.on-active-vt('profileBusinessHoursCollapse') {
&::view-transition-old(.expandArrow) {
animation-name: vt-collapse-icon-spin;
}
&::view-transition-new(.expandArrow) {
display: none;
}
&::view-transition-new(.businessHours) {
display: none;
}
}

View File

@ -7,8 +7,6 @@ import type { ApiBusinessWorkHours } from '../../../api/types';
import { selectTimezones } from '../../../global/selectors';
import {
VTT_PROFILE_BUSINESS_HOURS,
VTT_PROFILE_BUSINESS_HOURS_COLLAPSE,
VTT_PROFILE_BUSINESS_HOURS_EXPAND,
} from '../../../util/animations/viewTransitionTypes';
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../util/buildClassName';
@ -105,13 +103,9 @@ const BusinessHours = ({
const handleClick = useLastCallback(() => {
if (isExpanded) {
startViewTransition(VTT_PROFILE_BUSINESS_HOURS_COLLAPSE, () => {
collapse();
});
collapse();
} else {
startViewTransition(VTT_PROFILE_BUSINESS_HOURS_EXPAND, () => {
expand();
});
expand();
}
});
@ -151,36 +145,34 @@ const BusinessHours = ({
</div>
<Icon className={styles.arrow} style={createVtnStyle('expandArrow', true)} name={isExpanded ? 'up' : 'down'} />
</div>
{isExpanded && (
<div className={styles.bottom}>
{Boolean(timezoneMinuteDifference) && (
<div
className={styles.offsetTrigger}
style={createVtnStyle('offsetTrigger')}
role="button"
tabIndex={0}
onMouseDown={!IS_TOUCH_ENV ? handleTriggerOffset : undefined}
onClick={IS_TOUCH_ENV ? handleTriggerOffset : undefined}
>
{oldLang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')}
</div>
)}
<dl className={styles.timetable}>
{DAYS.map((day) => (
<>
<dt className={buildClassName(styles.weekday, day === currentDay && styles.currentDay)}>
{formatWeekday(oldLang, day === 6 ? 0 : day + 1)}
</dt>
<dd className={styles.schedule}>
{workHours[day].map((segment) => (
<div>{segment}</div>
))}
</dd>
</>
))}
</dl>
</div>
)}
<div className={buildClassName(styles.bottom, !isExpanded && styles.collapsed)} aria-hidden={!isExpanded}>
{Boolean(timezoneMinuteDifference) && (
<div
className={styles.offsetTrigger}
style={createVtnStyle('offsetTrigger')}
role="button"
tabIndex={isExpanded ? 0 : -1}
onMouseDown={!IS_TOUCH_ENV ? handleTriggerOffset : undefined}
onClick={IS_TOUCH_ENV ? handleTriggerOffset : undefined}
>
{oldLang(isMyTime ? 'BusinessHoursProfileSwitchMy' : 'BusinessHoursProfileSwitchLocal')}
</div>
)}
<dl className={styles.timetable}>
{DAYS.map((day) => (
<>
<dt className={buildClassName(styles.weekday, day === currentDay && styles.currentDay)}>
{formatWeekday(oldLang, day === 6 ? 0 : day + 1)}
</dt>
<dd className={styles.schedule}>
{workHours[day].map((segment) => (
<div>{segment}</div>
))}
</dd>
</>
))}
</dl>
</div>
</ListItem>
);
};

View File

@ -76,7 +76,6 @@ type OwnProps = {
isOwnProfile?: boolean;
isSavedDialog?: boolean;
isInSettings?: boolean;
withIslands?: boolean;
className?: string;
style?: string;
};
@ -132,7 +131,6 @@ const ChatExtra = ({
className,
style,
isInSettings,
withIslands,
canViewSubscribers,
}: OwnProps & StateProps) => {
const {
@ -392,17 +390,15 @@ const ChatExtra = ({
);
}
const Wrapper = withIslands ? Island : 'div';
return (
<div className={buildClassName('ChatExtra', className)} style={style || createVtnStyle('chatExtra')}>
{user && userFullInfo?.isUnofficialSecurityRisk && (
<Wrapper className={withIslands ? styles.securityRiskIsland : undefined}>
<Island className={styles.securityRiskIsland}>
<div className={styles.unofficialSecurityRisk}>
<Icon className={buildClassName(styles.riskIcon, 'in-text-icon')} name="info-filled" />
{lang('UnofficialSecurityRisk', { peer: getPeerTitle(lang, user) })}
</div>
</Wrapper>
</Island>
)}
{personalChannel && (
<div className={styles.personalChannel} style={createVtnStyle('personalChannel')}>
@ -410,7 +406,7 @@ const ChatExtra = ({
<span className={styles.personalChannelSubscribers}>
{oldLang('Subscribers', personalChannel.membersCount, 'i')}
</span>
<Wrapper className={styles.personalChannelItem}>
<Island className={styles.personalChannelItem}>
<Chat
chatId={personalChannel.id}
orderDiff={0}
@ -419,10 +415,10 @@ const ChatExtra = ({
isPreview
previewMessageId={personalChannelMessageId}
/>
</Wrapper>
</Island>
</div>
)}
<Wrapper>
<Island>
{Boolean(formattedNumber?.length) && (
<ListItem
icon="phone"
@ -647,7 +643,7 @@ const ChatExtra = ({
{botVerification.description}
</div>
)}
</Wrapper>
</Island>
</div>
);
};

View File

@ -1,7 +1,7 @@
@use "../../../styles/mixins";
.root {
--profile-info-bg: var(--color-background);
--profile-info-bg: var(--color-background-secondary);
--rating-outline-color: #000000;
--rating-text-color: #000000;

View File

@ -57,3 +57,8 @@
margin-top: 1rem;
}
}
.outside {
padding-bottom: 0.5rem;
padding-inline: 0.5rem;
}

View File

@ -43,6 +43,24 @@ const IslandTitle = ({ className, children, ...otherProps }: OwnProps) => {
);
};
const IslandOutside = ({
className,
children,
...otherProps
}: OwnProps) => {
return (
<div
className={buildClassName(
styles.outside,
className,
)}
{...otherProps}
>
{children}
</div>
);
};
const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
return (
<div
@ -56,6 +74,7 @@ const IslandText = ({ className, children, ...otherProps }: OwnProps) => {
export default Island;
export {
IslandOutside,
IslandDescription,
IslandTitle,
IslandText,

View File

@ -12,6 +12,12 @@
background-color: var(--color-background);
transition: background-color 150ms;
&.secondary {
background-color: var(--color-background-secondary);
}
h3 {
user-select: none;

View File

@ -11,7 +11,8 @@ import type { ReducerAction } from '../../hooks/useReducer';
import { type AnimationLevel, LeftColumnContent, SettingsScreens } from '../../types';
import {
selectCurrentChat, selectIsCurrentUserFrozen, selectIsForumPanelOpen, selectTabState,
selectCurrentChat, selectIsCurrentUserFrozen, selectIsForumPanelOpen,
selectPeerHasProfileBackground, selectTabState,
} from '../../global/selectors';
import { selectSharedSettings } from '../../global/selectors/sharedState';
import {
@ -61,6 +62,7 @@ type StateProps = {
archiveSettings: GlobalState['archiveSettings'];
isArchivedStoryRibbonShown?: boolean;
isAccountFrozen?: boolean;
hasProfileBackground?: boolean;
};
enum ContentType {
@ -98,6 +100,7 @@ function LeftColumn({
archiveSettings,
isArchivedStoryRibbonShown,
isAccountFrozen,
hasProfileBackground,
isFoldersSidebarShown,
}: OwnProps & StateProps) {
const {
@ -514,6 +517,7 @@ function LeftColumn({
foldersDispatch={foldersDispatch}
animationLevel={animationLevel}
shouldSkipTransition={shouldSkipHistoryAnimations}
hasProfileBackground={hasProfileBackground}
onReset={handleReset}
/>
);
@ -627,6 +631,8 @@ export default memo(withGlobal<OwnProps>(
archiveSettings,
isArchivedStoryRibbonShown: isArchivedRibbonShown,
isAccountFrozen,
hasProfileBackground: currentUserId
? selectPeerHasProfileBackground(global, currentUserId) : undefined,
contentKey: leftColumn.contentKey,
settingsScreen: leftColumn.settingsScreen,
};

View File

@ -7,6 +7,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
import Island, { IslandDescription } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
type OwnProps = {
@ -19,20 +20,19 @@ function PremiumStatusItem({ premiumSection }: OwnProps) {
const handleOpenPremiumModal = useLastCallback(() => openPremiumModal({ initialSection: premiumSection }));
return (
<div className="settings-item">
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
onClick={handleOpenPremiumModal}
>
{lang('PrivacyLastSeenPremium')}
</ListItem>
<p
className="settings-item-description-larger premium-info"
dir={lang.isRtl ? 'rtl' : undefined}
>
<>
<Island>
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
onClick={handleOpenPremiumModal}
>
{lang('PrivacyLastSeenPremium')}
</ListItem>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('lng_messages_privacy_premium_about')}
</p>
</div>
</IslandDescription>
</>
);
}

View File

@ -23,6 +23,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import PremiumStatusItem from './PremiumStatusItem';
@ -159,28 +160,27 @@ function PrivacyMessages({
: oldLang('Users', noPaidReactionsForUsersCount, 'i');
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('RemoveFeeTitle')}
</h4>
<ListItem
narrow
icon="delete-user"
onClick={() => {
openSettingsScreen({ screen: SettingsScreens.PrivacyNoPaidMessages });
}}
>
<div className="multiline-item full-size">
<span className="title">{lang('ExceptionTitlePrivacyChargeForMessages')}</span>
<span className="subtitle">
{
itemSubtitle
}
</span>
</div>
</ListItem>
</div>
</IslandTitle>
<Island>
<ListItem
narrow
icon="delete-user"
onClick={() => {
openSettingsScreen({ screen: SettingsScreens.PrivacyNoPaidMessages });
}}
>
<div className="multiline-item full-size">
<span className="title">{lang('ExceptionTitlePrivacyChargeForMessages')}</span>
<span className="subtitle">
{itemSubtitle}
</span>
</div>
</ListItem>
</Island>
</>
);
}
@ -195,34 +195,34 @@ function PrivacyMessages({
}, [shouldChargeForMessages, lang]);
return (
<>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('PrivacyMessagesTitle')}
</h4>
<div className="settings-content custom-scroll">
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('PrivacyMessagesTitle')}
</IslandTitle>
<Island>
<RadioGroup
name="privacy-messages"
options={options}
onChange={handleChange}
selected={selectedValue}
/>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{privacyDescription}
</p>
</div>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{privacyDescription}
</IslandDescription>
{selectedValue === 'charge_for_messages' && (
<div className="settings-item fluid-container">
<Island>
<PaidMessagePrice
canChangeChargeForMessages={canChangeChargeForMessages}
chargeForMessages={chargeForMessages}
onChange={handleChargeForMessagesChange}
/>
</div>
</Island>
)}
{canChangeChargeForMessages && selectedValue === 'charge_for_messages' && renderSectionNoPaidMessagesForUsers()}
{!isCurrentUserPremium && selectedValue !== 'charge_for_messages'
&& <PremiumStatusItem premiumSection="message_privacy" />}
</>
</div>
);
}

View File

@ -3,6 +3,14 @@
#Settings {
height: 100%;
.chat-list {
padding-bottom: 0;
@media (max-width: 600px) {
padding-block: 0;
}
}
.with-notch::before {
top: var(--header-height);
}
@ -15,11 +23,11 @@
.left-header {
padding-right: 0.8125rem;
}
.self-profile .ProfileInfo {
margin: -0.5rem 0 0.75rem -0.5rem;
margin-inline-end: calc(min(var(--scrollbar-width) - 0.5rem, 0px));
&:has(~ .scrolled),
&:has(~ .settings-fab-wrapper .scrolled) {
background-color: var(--color-background);
}
}
}
@ -43,7 +51,14 @@
.settings-content {
overflow-y: scroll;
height: calc(100% - var(--header-height));
padding: 1rem;
padding-top: 0;
background-color: var(--color-background-secondary);
@include mixins.adapt-padding-to-scrollbar(1rem);
&.password-form .input-group.error label::first-letter {
text-transform: uppercase;
@ -82,7 +97,9 @@
flex-direction: column;
align-items: center;
padding: 0 1.5rem;
margin-bottom: 1rem;
padding-block: 0;
padding-inline: 1.5rem;
text-align: center;
@ -105,21 +122,6 @@
}
}
.settings-main-menu {
padding: 0.5rem;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
@include mixins.side-panel-section;
.ListItem.narrow:not(.multiline) {
margin-bottom: 0;
.ListItem-button {
min-height: 3.5rem;
}
}
}
.settings-range-value {
display: inline-flex;
align-items: center;
@ -137,8 +139,7 @@
}
.settings-unlock-button {
margin-top: 1rem;
margin-inline: 1rem;
margin-top: 0.5rem;
}
.fluid-container {
@ -246,39 +247,6 @@
}
}
&.blocked-list-item {
margin-bottom: 0.5rem;
.ListItem-button {
align-items: center;
padding: 0.5rem;
text-align: left;
}
.Avatar {
width: 3rem;
height: 3rem;
margin-right: 1rem;
}
.contact-info {
overflow: hidden;
}
.contact-name {
margin-bottom: 0.25rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
line-height: 1rem;
}
.contact-phone {
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-text-secondary);
}
}
&[dir="rtl"] {
.multiline-item .date {
float: left;
@ -314,18 +282,63 @@
}
}
.settings-language-transition {
height: auto;
margin-top: 1rem;
}
.settings-picker {
padding-block: 0;
}
.settings-picker-islands {
padding: 1rem;
background-color: var(--color-background-secondary);
}
.settings-input {
padding: 0.5rem 1rem 0 1rem;
padding: 0.125rem;
}
.settings-input > :last-child {
--input-group-margin: 0;
}
.settings-group {
padding: 1rem 1.5rem;
}
.ListItem.blocked-list-item {
.ListItem-button {
align-items: center;
padding: 0.5rem;
text-align: left;
}
.Avatar {
width: 3rem;
height: 3rem;
margin-right: 1rem;
}
.contact-info {
overflow: hidden;
}
.contact-name {
margin-bottom: 0.25rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
line-height: 1rem;
}
.contact-phone {
font-size: 0.875rem;
line-height: 1rem;
color: var(--color-text-secondary);
}
}
.settings-fab-wrapper {
position: relative;
height: calc(100% - var(--header-height));

View File

@ -155,6 +155,7 @@ export type OwnProps = {
foldersDispatch: FolderEditDispatch;
animationLevel: AnimationLevel;
shouldSkipTransition?: boolean;
hasProfileBackground?: boolean;
onReset: (forceReturnToChatList?: true | Event) => void;
};
@ -166,6 +167,7 @@ const Settings: FC<OwnProps> = ({
onReset,
animationLevel,
shouldSkipTransition,
hasProfileBackground,
}) => {
const { closeShareChatFolderModal, openSettingsScreen } = getActions();
@ -176,7 +178,8 @@ const Settings: FC<OwnProps> = ({
useScrollNotch({
containerRef,
selector: '.settings-content',
selector: '.Transition_slide-active .settings-content,'
+ ' .Transition_slide-active .settings-main-scroll',
}, [currentScreen]);
const handleReset = useLastCallback((forceReturnToChatList?: true | Event) => {
@ -513,6 +516,7 @@ const Settings: FC<OwnProps> = ({
currentScreen={currentScreen}
onReset={handleReset}
editedFolderId={foldersState.folderId}
hasProfileBackground={hasProfileBackground}
/>
{renderCurrentSectionContent(isScreenActive, activeKey)}
</>

View File

@ -8,6 +8,7 @@ import { selectIsCurrentUserPremium } from '../../../global/selectors';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import Switcher from '../../ui/Switcher';
@ -89,54 +90,56 @@ const SettingsAcceptedGift = ({
});
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('PrivacyAcceptedGiftTitle')}
</h4>
<ListItem onClick={handleLimitedEditionChange}>
<span>{lang('PrivacyGiftLimitedEdition')}</span>
<Switcher
id="limited_edition"
label={disallowedGifts?.shouldDisallowLimitedStarGifts ? lang('PrivacyDisableLimitedEditionStarGifts')
: lang('PrivacyEnableLimitedEditionStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowLimitedStarGifts}
/>
</ListItem>
<ListItem onClick={handleUnlimitedEditionChange}>
<span>{lang('PrivacyGiftUnlimited')}</span>
<Switcher
id="unlimited"
label={disallowedGifts?.shouldDisallowUnlimitedStarGifts ? lang('PrivacyDisableUnlimitedStarGifts')
: lang('PrivacyEnableUnlimitedStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowUnlimitedStarGifts}
/>
</ListItem>
<ListItem onClick={handleUniqueChange}>
<span>{lang('PrivacyGiftUnique')}</span>
<Switcher
id="unique"
label={disallowedGifts?.shouldDisallowUniqueStarGifts ? lang('PrivacyDisableUniqueStarGifts')
: lang('PrivacyEnableUniqueStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowUniqueStarGifts}
/>
</ListItem>
<ListItem onClick={handlePremiumSubscriptionChange}>
<span>{lang('PrivacyGiftPremiumSubscription')}</span>
<Switcher
id="premium_subscription"
label={disallowedGifts?.shouldDisallowPremiumGifts ? lang('PrivacyDisablePremiumGifts')
: lang('PrivacyEnablePremiumGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowPremiumGifts}
/>
</ListItem>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
</IslandTitle>
<Island>
<ListItem onClick={handleLimitedEditionChange}>
<span>{lang('PrivacyGiftLimitedEdition')}</span>
<Switcher
id="limited_edition"
label={disallowedGifts?.shouldDisallowLimitedStarGifts ? lang('PrivacyDisableLimitedEditionStarGifts')
: lang('PrivacyEnableLimitedEditionStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowLimitedStarGifts}
/>
</ListItem>
<ListItem onClick={handleUnlimitedEditionChange}>
<span>{lang('PrivacyGiftUnlimited')}</span>
<Switcher
id="unlimited"
label={disallowedGifts?.shouldDisallowUnlimitedStarGifts ? lang('PrivacyDisableUnlimitedStarGifts')
: lang('PrivacyEnableUnlimitedStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowUnlimitedStarGifts}
/>
</ListItem>
<ListItem onClick={handleUniqueChange}>
<span>{lang('PrivacyGiftUnique')}</span>
<Switcher
id="unique"
label={disallowedGifts?.shouldDisallowUniqueStarGifts ? lang('PrivacyDisableUniqueStarGifts')
: lang('PrivacyEnableUniqueStarGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowUniqueStarGifts}
/>
</ListItem>
<ListItem onClick={handlePremiumSubscriptionChange}>
<span>{lang('PrivacyGiftPremiumSubscription')}</span>
<Switcher
id="premium_subscription"
label={disallowedGifts?.shouldDisallowPremiumGifts ? lang('PrivacyDisablePremiumGifts')
: lang('PrivacyEnablePremiumGifts')}
disabled={!isCurrentUserPremium}
checked={!isCurrentUserPremium ? true : !disallowedGifts?.shouldDisallowPremiumGifts}
/>
</ListItem>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('PrivacyAcceptedGiftInfo')}
</p>
</div>
</IslandDescription>
</>
);
};

View File

@ -15,6 +15,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang';
import Island, { IslandTitle } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
@ -141,76 +142,79 @@ const SettingsActiveSessions: FC<OwnProps & StateProps> = ({
function renderCurrentSession(session: ApiSession) {
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('AuthSessionsCurrentSession')}
</h4>
<ListItem narrow inactive icon={`device-${getSessionIcon(session)}`} iconClassName="icon-device">
<div className="multiline-item full-size" dir="auto">
<span className="title" dir="auto">{session.deviceModel}</span>
<span className="subtitle black tight">
{session.appName}
{' '}
{session.appVersion}
,
{' '}
{session.platform}
{' '}
{session.systemVersion}
</span>
<span className="subtitle">
{session.ip}
{' '}
-
{' '}
{getLocation(session)}
</span>
</div>
</ListItem>
{hasOtherSessions && (
<ListItem
className="destructive mb-0 no-icon"
icon="stop"
ripple
narrow
onClick={openConfirmTerminateAllDialog}
>
{lang('TerminateAllSessions')}
</IslandTitle>
<Island>
<ListItem narrow inactive icon={`device-${getSessionIcon(session)}`} iconClassName="icon-device">
<div className="multiline-item full-size" dir="auto">
<span className="title" dir="auto">{session.deviceModel}</span>
<span className="subtitle black tight">
{session.appName}
{' '}
{session.appVersion}
,
{' '}
{session.platform}
{' '}
{session.systemVersion}
</span>
<span className="subtitle">
{session.ip}
{' '}
-
{' '}
{getLocation(session)}
</span>
</div>
</ListItem>
)}
</div>
{hasOtherSessions && (
<ListItem
className="destructive mb-0 no-icon"
icon="stop"
ripple
narrow
onClick={openConfirmTerminateAllDialog}
>
{lang('TerminateAllSessions')}
</ListItem>
)}
</Island>
</>
);
}
function renderOtherSessions(sessionHashes: string[]) {
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('OtherSessions')}
</h4>
{sessionHashes.map(renderSession)}
</div>
</IslandTitle>
<Island>
{sessionHashes.map(renderSession)}
</Island>
</>
);
}
function renderAutoTerminate() {
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('TerminateOldSessionHeader')}
</h4>
<p className="settings-item-description-larger">{lang('IfInactiveFor')}</p>
<RadioGroup
name="session_ttl"
options={AUTO_TERMINATE_OPTIONS}
selected={autoTerminateValue}
onChange={handleChangeSessionTtl}
/>
</div>
</IslandTitle>
<Island>
<p className="settings-item-description-larger">{lang('IfInactiveFor')}</p>
<RadioGroup
name="session_ttl"
options={AUTO_TERMINATE_OPTIONS}
selected={autoTerminateValue}
onChange={handleChangeSessionTtl}
/>
</Island>
</>
);
}

View File

@ -15,6 +15,7 @@ import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import FullNameTitle from '../../common/FullNameTitle';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import SettingsActiveWebsite from './SettingsActiveWebsite';
@ -80,13 +81,14 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
function renderSessions(sessionHashes: string[]) {
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('WebSessionsTitle')}
</h4>
{sessionHashes.map(renderSession)}
</div>
</IslandTitle>
<Island>
{sessionHashes.map(renderSession)}
</Island>
</>
);
}
@ -135,7 +137,7 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<Island>
<ListItem
className="destructive mb-0 no-icon"
icon="stop"
@ -145,10 +147,10 @@ const SettingsActiveWebsites: FC<OwnProps & StateProps> = ({
>
{lang('AuthSessions.LogOutApplications')}
</ListItem>
<p className={buildClassName('settings-item-description', styles.clearHelp)}>
{lang('ClearOtherWebSessionsHelp')}
</p>
</div>
</Island>
<IslandDescription className={styles.clearHelp}>
{lang('ClearOtherWebSessionsHelp')}
</IslandDescription>
{renderSessions(orderedHashes)}
<ConfirmDialog
isOpen={isConfirmTerminateAllDialogOpen}

View File

@ -16,6 +16,7 @@ import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'
import useOldLang from '../../../hooks/useOldLang';
import StickerSetCard from '../../common/StickerSetCard';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
type OwnProps = {
@ -67,27 +68,31 @@ const SettingsCustomEmoji: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content custom-scroll">
{customEmojiSets && (
<div className="settings-item">
<Checkbox
label={lang('SuggestAnimatedEmoji')}
checked={shouldSuggestCustomEmoji}
onCheck={handleSuggestCustomEmojiChange}
/>
<div className="mt-4" ref={stickerSettingsRef}>
{customEmojiSets.map((stickerSet: ApiStickerSet) => (
<StickerSetCard
key={stickerSet.id}
stickerSet={stickerSet}
observeIntersection={observeIntersectionForCovers}
onClick={handleStickerSetClick}
noPlay={!canPlayAnimatedEmojis}
/>
))}
</div>
<p className="settings-item-description mt-3" dir="auto">
<>
<Island>
<Checkbox
label={lang('SuggestAnimatedEmoji')}
checked={shouldSuggestCustomEmoji}
onCheck={handleSuggestCustomEmojiChange}
/>
</Island>
<Island>
<div ref={stickerSettingsRef}>
{customEmojiSets.map((stickerSet: ApiStickerSet) => (
<StickerSetCard
key={stickerSet.id}
stickerSet={stickerSet}
observeIntersection={observeIntersectionForCovers}
onClick={handleStickerSetClick}
noPlay={!canPlayAnimatedEmojis}
/>
))}
</div>
</Island>
<IslandDescription dir="auto">
{renderText(lang('EmojiBotInfo'), ['links'])}
</p>
</div>
</IslandDescription>
</>
)}
</div>
);

View File

@ -11,6 +11,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import Island, { IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
import RangeSlider from '../../ui/RangeSlider';
@ -84,7 +85,7 @@ const SettingsDataStorage = ({
const value = AUTODOWNLOAD_FILESIZE_MB_LIMITS.indexOf(autoLoadFileMaxSizeMb);
return (
<div className="pt-5">
<div>
<RangeSlider
label={lang('AutoDownloadMaxFileSize')}
min={0}
@ -106,37 +107,33 @@ const SettingsDataStorage = ({
canAutoLoadInChannels: boolean,
) {
return (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{title}</h4>
<Checkbox
label={lang('AutoDownloadSettingsContacts')}
checked={canAutoLoadFromContacts}
// TODO rewrite to support `useCallback`
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}FromContacts`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsPrivateChats')}
checked={canAutoLoadInPrivateChats}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InPrivateChats`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsGroupChats')}
checked={canAutoLoadInGroups}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InGroups`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsChannels')}
checked={canAutoLoadInChannels}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InChannels`]: isChecked })}
/>
{key === 'File' && renderContentSizeSlider()}
</div>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{title}</IslandTitle>
<Island>
<Checkbox
label={lang('AutoDownloadSettingsContacts')}
checked={canAutoLoadFromContacts}
// TODO rewrite to support `useCallback`
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}FromContacts`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsPrivateChats')}
checked={canAutoLoadInPrivateChats}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InPrivateChats`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsGroupChats')}
checked={canAutoLoadInGroups}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InGroups`]: isChecked })}
/>
<Checkbox
label={lang('AutoDownloadSettingsChannels')}
checked={canAutoLoadInChannels}
onCheck={(isChecked) => setSettingOption({ [`canAutoLoad${key}InChannels`]: isChecked })}
/>
{key === 'File' && renderContentSizeSlider()}
</Island>
</>
);
}
@ -166,7 +163,7 @@ const SettingsDataStorage = ({
canAutoLoadFileInGroups,
canAutoLoadFileInChannels,
)}
<div className="settings-item">
<Island>
<ListItem
onClick={handlePurge}
icon="delete"
@ -179,7 +176,7 @@ const SettingsDataStorage = ({
{lang('SettingsDataClearMediaCacheDescription')}
</span>
</ListItem>
</div>
</Island>
</div>
);
};

View File

@ -5,5 +5,6 @@
.item {
overflow: hidden;
min-height: 25rem;
flex: 1 1 auto;
padding-block: 0;
}

View File

@ -13,6 +13,8 @@ import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
import Island from '../../gili/layout/Island';
import InputText from '../../ui/InputText';
import styles from './SettingsDoNotTranslate.module.scss';
@ -79,7 +81,16 @@ const SettingsDoNotTranslate = ({
return (
<div className={buildClassName(styles.root, 'settings-content infinite-scroll')}>
<div className={buildClassName(styles.item)}>
<Island>
<InputText
id="lang-picker-search"
value={searchQuery}
onChange={(e) => setSearchQuery(e.currentTarget.value)}
placeholder={lang('Search')}
noMargin
/>
</Island>
<Island className={styles.item}>
<ItemPicker
className={styles.picker}
items={displayedOptionList}
@ -87,13 +98,11 @@ const SettingsDoNotTranslate = ({
onSelectedValuesChange={handleChange}
filterValue={searchQuery}
onFilterChange={setSearchQuery}
isSearchable
allowMultiple
withDefaultPadding
itemInputType="checkbox"
searchInputId="lang-picker-search"
/>
</div>
</Island>
</div>
);
};

View File

@ -26,6 +26,8 @@ import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import ManageUsernames from '../../common/ManageUsernames';
import SafeLink from '../../common/SafeLink';
import UsernameInput from '../../common/UsernameInput';
import Island, { IslandDescription, IslandOutside, IslandTitle } from '../../gili/layout/Island';
import Surface from '../../gili/layout/Surface';
import AvatarEditable from '../../ui/AvatarEditable';
import FloatingActionButton from '../../ui/FloatingActionButton';
import InputText from '../../ui/InputText';
@ -218,28 +220,30 @@ const SettingsEditProfile = ({
const purchaseInfoLink = `${TME_LINK_PREFIX}${PURCHASE_USERNAME}`;
return (
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
<IslandDescription dir={oldLang.isRtl ? 'rtl' : undefined}>
{(oldLang('lng_username_purchase_available'))
.replace('{link}', '%PURCHASE_LINK%')
.split('%')
.map((s) => {
return (s === 'PURCHASE_LINK' ? <SafeLink url={purchaseInfoLink} text={`@${PURCHASE_USERNAME}`} /> : s);
})}
</p>
</IslandDescription>
);
}
return (
<div className="settings-fab-wrapper">
<div className="settings-content no-border custom-scroll">
<div className="settings-item">
<Surface scrollable className="settings-content no-border">
<IslandOutside className="settings-content-header">
<AvatarEditable
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handlePhotoChange}
title={lang('AriaSettingsEditProfilePhoto')}
disabled={isLoading}
/>
</IslandOutside>
<Island>
<div className="settings-input">
<AvatarEditable
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handlePhotoChange}
title={lang('AriaSettingsEditProfilePhoto')}
disabled={isLoading}
/>
<InputText
value={firstName}
onChange={handleFirstNameChange}
@ -262,13 +266,12 @@ const SettingsEditProfile = ({
maxLengthIndicator={maxBioLength ? (maxBioLength - bio.length).toString() : undefined}
/>
</div>
</Island>
<IslandDescription dir={oldLang.isRtl ? 'rtl' : undefined}>
{renderText(oldLang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
</IslandDescription>
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
{renderText(oldLang('lng_settings_about_bio'), ['br', 'simple_markdown'])}
</p>
</div>
<div className="settings-item">
<Island>
<ListItem
icon="gift"
narrow
@ -279,21 +282,20 @@ const SettingsEditProfile = ({
>
<span className="flex-grow">{lang('SettingsBirthday')}</span>
</ListItem>
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
{lang('BirthdayPrivacySuggestion', {
link: (
<Link isPrimary onClick={handleBirthdayPrivacyClick}>
{lang('BirthdayPrivacySuggestionLink',
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
</Link>
),
}, { withNodes: true })}
</p>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={oldLang.isRtl ? 'rtl' : undefined}>{oldLang('Username')}</h4>
</Island>
<IslandDescription dir={oldLang.isRtl ? 'rtl' : undefined}>
{lang('BirthdayPrivacySuggestion', {
link: (
<Link isPrimary onClick={handleBirthdayPrivacyClick}>
{lang('BirthdayPrivacySuggestionLink',
undefined, { withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
</Link>
),
}, { withNodes: true })}
</IslandDescription>
<IslandTitle dir={oldLang.isRtl ? 'rtl' : undefined}>{oldLang('Username')}</IslandTitle>
<Island>
<div className="settings-input">
<UsernameInput
currentUsername={currentUsername}
@ -303,22 +305,21 @@ const SettingsEditProfile = ({
onChange={handleUsernameChange}
/>
</div>
{editUsernameError === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
{renderText(oldLang('UsernameHelp'), ['br', 'simple_markdown'])}
</p>
{editableUsername && (
<p className="settings-item-description" dir={oldLang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_username_link')}
<br />
<span className="username-link">
{TME_LINK_PREFIX}
{editableUsername}
</span>
</p>
)}
</div>
</Island>
{editUsernameError === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
<IslandDescription dir={oldLang.isRtl ? 'rtl' : undefined}>
{renderText(oldLang('UsernameHelp'), ['br', 'simple_markdown'])}
</IslandDescription>
{editableUsername && (
<IslandDescription dir={oldLang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_username_link')}
<br />
<span className="username-link">
{TME_LINK_PREFIX}
{editableUsername}
</span>
</IslandDescription>
)}
{shouldRenderUsernamesManage && (
<ManageUsernames
@ -326,7 +327,7 @@ const SettingsEditProfile = ({
onEditUsername={setEditableUsername}
/>
)}
</div>
</Surface>
<FloatingActionButton
isShown={isSaveButtonShown}

View File

@ -20,6 +20,7 @@ import useMultiaccountInfo from '../../../hooks/useMultiaccountInfo';
import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Island from '../../gili/layout/Island';
import { animateSnap } from '../../main/visualEffects/SnapEffectContainer';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -110,15 +111,15 @@ const SettingsExperimental = ({
/>
<p className="settings-item-description pt-3" dir="auto">{lang('lng_settings_experimental_about')}</p>
</div>
<div className="settings-item">
<Island>
<ListItem
href={newAccountUrl}
icon="add-user"
>
<div className="title">Login on Test Server</div>
</ListItem>
</div>
<div className="settings-item">
</Island>
<Island>
<ListItem
onClick={handleRequestConfetti}
icon="animations"
@ -141,45 +142,38 @@ const SettingsExperimental = ({
>
<div className="title">Vaporize this button</div>
</ListItem>
</div>
<div className="settings-item">
</Island>
<Island>
<Checkbox
label="Allow HTTP Transport"
checked={Boolean(shouldAllowHttpTransport)}
onCheck={() => setSharedSettingOption({ shouldAllowHttpTransport: !shouldAllowHttpTransport })}
/>
<Checkbox
label="Force HTTP Transport"
disabled={!shouldAllowHttpTransport}
checked={Boolean(shouldForceHttpTransport)}
onCheck={() => setSharedSettingOption({ shouldForceHttpTransport: !shouldForceHttpTransport })}
/>
</div>
<div className="settings-item">
</Island>
<Island>
<Checkbox
label={lang('DebugMenuEnableLogs')}
checked={Boolean(shouldCollectDebugLogs)}
onCheck={() => setSharedSettingOption({ shouldCollectDebugLogs: !shouldCollectDebugLogs })}
/>
<Checkbox
label="Enable exported senders debug"
checked={Boolean(shouldDebugExportedSenders)}
onCheck={() => setSharedSettingOption({ shouldDebugExportedSenders: !shouldDebugExportedSenders })}
/>
<ListItem
onClick={handleDownloadLog}
icon="bug"
>
<div className="title">Download log</div>
</ListItem>
</div>
</Island>
</div>
);
};

View File

@ -19,6 +19,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import Island, { IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import RangeSlider from '../../ui/RangeSlider';
@ -117,9 +118,8 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Settings')}</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('Settings')}</IslandTitle>
<Island>
<RangeSlider
label={lang('TextSize')}
min={12}
@ -127,52 +127,47 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
value={messageTextSize}
onChange={handleMessageTextSizeChange}
/>
<ListItem
icon="photo"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.GeneralChatBackground })}
>
{lang('ChatBackground')}
</ListItem>
</div>
</Island>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('Theme')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('Theme')}</IslandTitle>
<Island>
<RadioGroup
name="theme"
options={appearanceThemeOptions}
selected={shouldUseSystemTheme ? 'auto' : theme}
onChange={handleAppearanceThemeChange}
/>
</div>
</Island>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SettingsTimeFormat')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('SettingsTimeFormat')}</IslandTitle>
<Island>
<RadioGroup
name="timeformat"
options={timeFormatOptions}
selected={timeFormat}
onChange={handleTimeFormatChange}
/>
</div>
</Island>
{keyboardSendOptions && (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('SettingsKeyboard')}</h4>
<RadioGroup
name="keyboard-send-settings"
options={keyboardSendOptions}
onChange={handleMessageSendComboChange}
selected={messageSendKeyCombo}
/>
</div>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('SettingsKeyboard')}</IslandTitle>
<Island>
<RadioGroup
name="keyboard-send-settings"
options={keyboardSendOptions}
onChange={handleMessageSendComboChange}
selected={messageSendKeyCombo}
/>
</Island>
</>
)}
</div>
);

View File

@ -1,13 +1,29 @@
@use "../../../styles/mixins";
.SettingsGeneralBackground {
padding-bottom: 0 !important;
.settings-wallpapers {
overflow: clip;
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 0.0625rem;
@include mixins.side-panel-section;
margin: 1rem -1rem 0;
margin-inline: calc(min(var(--scrollbar-width) - 1rem, 0px));
border-top-left-radius: var(--border-radius-island);
border-top-right-radius: var(--border-radius-island);
> :first-child .media-inner,
> :first-child::after {
border-top-left-radius: var(--border-radius-island);
}
> :nth-child(3) .media-inner,
> :nth-child(3)::after {
border-top-right-radius: var(--border-radius-island);
}
}
.Loading {

View File

@ -19,6 +19,7 @@ import { openSystemFilesDialog } from '../../../util/systemFilesDialog';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useOldLang from '../../../hooks/useOldLang';
import Island from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
import Loading from '../../ui/Loading';
@ -124,7 +125,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
return (
<div className="SettingsGeneralBackground settings-content custom-scroll">
<div className="settings-item">
<Island>
<ListItem
icon="camera-add"
className="mb-0"
@ -151,7 +152,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
checked={Boolean(isBlurred)}
onChange={handleWallPaperBlurChange}
/>
</div>
</Island>
{loadedWallpapers ? (
<div className="settings-wallpapers">

View File

@ -1,6 +1,8 @@
@use "../../../styles/mixins";
.SettingsGeneralBackgroundColor {
padding-bottom: 0 !important;
&:not(.is-dragging) .handle {
transition: transform 300ms ease;
}
@ -71,12 +73,16 @@
}
.predefined-colors {
overflow: clip;
display: grid;
grid-auto-rows: 1fr;
grid-template-columns: repeat(3, 1fr);
gap: 0.0625rem;
@include mixins.side-panel-section;
margin: 1rem -1rem 0;
margin-inline: calc(min(var(--scrollbar-width) - 1rem, 0px));
border-top-left-radius: var(--border-radius-island);
border-top-right-radius: var(--border-radius-island);
}
.predefined-color {
@ -84,6 +90,14 @@
box-shadow: inset 0 0 0 0 var(--color-background);
transition: box-shadow 300ms ease;
&:nth-child(1) {
border-top-left-radius: var(--border-radius-island);
}
&:nth-child(3) {
border-top-right-radius: var(--border-radius-island);
}
&.active {
border: 0.125rem solid var(--color-primary);
box-shadow: inset 0 0 0 0.3125rem var(--color-background);

View File

@ -20,6 +20,7 @@ import { pick } from '../../../util/iteratees';
import useFlag from '../../../hooks/useFlag';
import useHistoryBack from '../../../hooks/useHistoryBack';
import Island from '../../gili/layout/Island';
import InputText from '../../ui/InputText';
import './SettingsGeneralBackgroundColor.scss';
@ -211,7 +212,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
return (
<div ref={containerRef} className={className}>
<div className="settings-item pt-3">
<Island>
<div ref={colorPickerRef} className="color-picker">
<canvas />
<div
@ -230,7 +231,7 @@ const SettingsGeneralBackground: FC<OwnProps & StateProps> = ({
<InputText value={hexInput} label="HEX" onChange={handleHexChange} />
<InputText value={rgbInput} label="RGB" onChange={handleRgbChange} />
</div>
</div>
</Island>
<div className="predefined-colors">
{PREDEFINED_COLORS.map((color) => (
<div

View File

@ -19,12 +19,14 @@ import MenuItem from '../../ui/MenuItem';
type OwnProps = {
currentScreen: SettingsScreens;
editedFolderId?: number;
hasProfileBackground?: boolean;
onReset: () => void;
};
const SettingsHeader: FC<OwnProps> = ({
currentScreen,
editedFolderId,
hasProfileBackground,
onReset,
}) => {
const {
@ -286,7 +288,9 @@ const SettingsHeader: FC<OwnProps> = ({
}
return (
<div className="left-header">
<div className={hasProfileBackground && currentScreen === SettingsScreens.Main
? 'left-header' : 'left-header secondary'}
>
<Button
round
size="smaller"

View File

@ -19,9 +19,11 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import ItemPicker, { type ItemPickerOption } from '../../common/pickers/ItemPicker';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
import Loading from '../../ui/Loading';
import Transition from '../../ui/Transition';
type OwnProps = {
isActive?: boolean;
@ -131,51 +133,56 @@ const SettingsLanguage: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content settings-language custom-scroll">
{IS_TRANSLATION_SUPPORTED && (
<div className="settings-item">
<Checkbox
label={lang('ShowTranslateButton')}
checked={canTranslate}
onCheck={handleShouldTranslateChange}
/>
<Checkbox
label={lang('ShowTranslateChatButton')}
checked={canTranslateChatsEnabled}
disabled={!isCurrentUserPremium}
rightIcon={!isCurrentUserPremium ? 'lock' : undefined}
onClickLabel={handleShouldTranslateChatsClick}
onCheck={handleShouldTranslateChatsChange}
/>
{(canTranslate || canTranslateChatsEnabled) && (
<ListItem
narrow
onClick={handleDoNotSelectOpen}
>
{lang('DoNotTranslate')}
<span className="settings-item__current-value">{doNotTranslateText}</span>
</ListItem>
)}
<p className="settings-item-description mb-0 mt-1">
<>
<Island>
<Checkbox
label={lang('ShowTranslateButton')}
checked={canTranslate}
onCheck={handleShouldTranslateChange}
/>
<Checkbox
label={lang('ShowTranslateChatButton')}
checked={canTranslateChatsEnabled}
disabled={!isCurrentUserPremium}
rightIcon={!isCurrentUserPremium ? 'lock' : undefined}
onClickLabel={handleShouldTranslateChatsClick}
onCheck={handleShouldTranslateChatsChange}
/>
{(canTranslate || canTranslateChatsEnabled) && (
<ListItem
narrow
onClick={handleDoNotSelectOpen}
>
{lang('DoNotTranslate')}
<span className="settings-item__current-value">{doNotTranslateText}</span>
</ListItem>
)}
</Island>
<IslandDescription>
{lang('lng_translate_settings_about')}
</p>
</div>
</IslandDescription>
</>
)}
<div className="settings-item settings-item-picker">
<h4 className="settings-item-header">
{lang('Localization.InterfaceLanguage')}
</h4>
<Transition activeKey={options ? 1 : 0} name="fade" className="settings-language-transition">
{options ? (
<ItemPicker
items={options}
selectedValue={selectedLanguage}
forceRenderAllItems
onSelectedValueChange={handleChange}
itemInputType="radio"
className="settings-picker"
/>
<>
<IslandTitle>{lang('Localization.InterfaceLanguage')}</IslandTitle>
<Island>
<ItemPicker
items={options}
selectedValue={selectedLanguage}
forceRenderAllItems
onSelectedValueChange={handleChange}
itemInputType="radio"
className="settings-picker"
/>
</Island>
</>
) : (
<Loading />
)}
</div>
</Transition>
</div>
);
};

View File

@ -0,0 +1,22 @@
@use "../../../styles/mixins";
.root {
overflow-y: scroll;
height: calc(100% - var(--header-height));
background-color: var(--color-background-secondary);
}
.menuSection,
.selfProfile {
padding: 1rem;
@include mixins.adapt-padding-to-scrollbar(1rem);
}
.selfProfile {
padding-bottom: 0;
:global(.ProfileInfo) {
margin: -1rem 0 0.75rem -1rem;
margin-inline-end: calc(min(var(--scrollbar-width) - 1rem, 0px));
}
}

View File

@ -11,6 +11,7 @@ import {
selectIsGiveawayGiftsPurchaseAvailable,
selectIsPremiumPurchaseBlocked,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { convertCurrencyFromBaseUnit } from '../../../util/formatCurrency';
import useFlag from '../../../hooks/useFlag';
@ -22,9 +23,12 @@ import Icon from '../../common/icons/Icon';
import StarIcon from '../../common/icons/StarIcon';
import ChatExtra from '../../common/profile/ChatExtra';
import ProfileInfo from '../../common/profile/ProfileInfo';
import Island from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import styles from './SettingsMain.module.scss';
type OwnProps = {
isActive?: boolean;
onReset: () => void;
@ -80,8 +84,8 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
});
return (
<div className="settings-content custom-scroll">
<div className="settings-main-menu self-profile">
<div className={buildClassName(styles.root, 'settings-main-scroll', 'custom-scroll')}>
<div className={styles.selfProfile}>
{currentUserId && (
<ProfileInfo
peerId={currentUserId}
@ -96,153 +100,141 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
/>
)}
</div>
<div className="settings-main-menu">
<ListItem
icon="settings"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.General })}
>
{lang('TelegramGeneralSettingsViewController')}
</ListItem>
<ListItem
icon="animations"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Performance })}
>
{lang('MenuAnimations')}
</ListItem>
<ListItem
icon="unmute"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Notifications })}
>
{lang('Notifications')}
</ListItem>
<ListItem
icon="data"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.DataStorage })}
>
{lang('DataSettings')}
</ListItem>
<ListItem
icon="lock"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Privacy })}
>
{lang('PrivacySettings')}
</ListItem>
<ListItem
icon="folder"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Folders })}
>
{lang('Filters')}
</ListItem>
<ListItem
icon="active-sessions"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.ActiveSessions })}
>
{lang('SessionsTitle')}
{sessionCount > 0 && (<span className="settings-item__current-value">{sessionCount}</span>)}
</ListItem>
<ListItem
icon="language"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Language })}
>
{lang('Language')}
<span className="settings-item__current-value">{lang.languageInfo.nativeName}</span>
</ListItem>
<ListItem
icon="stickers"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Stickers })}
>
{lang('MenuStickers')}
</ListItem>
</div>
<div className="settings-main-menu">
{canBuyPremium && (
<div className={styles.menuSection}>
<Island>
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
icon="settings"
narrow
onClick={() => openPremiumModal()}
onClick={() => openSettingsScreen({ screen: SettingsScreens.General })}
>
{lang('TelegramPremium')}
{lang('TelegramGeneralSettingsViewController')}
</ListItem>
)}
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="gold" size="big" />}
narrow
onClick={() => openStarsBalanceModal({})}
>
{lang('MenuStars')}
{Boolean(starsBalance) && (
<span className="settings-item__current-value">
{formatStarsAmount(lang, starsBalance)}
</span>
)}
</ListItem>
<ListItem
leftElement={<Icon className="icon ListItem-main-icon" name="toncoin" />}
narrow
onClick={() => openStarsBalanceModal({ currency: TON_CURRENCY_CODE })}
>
{lang('MenuTon')}
{Boolean(tonBalance) && (
<span className="settings-item__current-value">
{convertCurrencyFromBaseUnit(tonBalance.amount, tonBalance.currency)}
</span>
)}
</ListItem>
{isGiveawayAvailable && (
<ListItem
icon="gift"
icon="animations"
narrow
onClick={() => openGiftRecipientPicker()}
onClick={() => openSettingsScreen({ screen: SettingsScreens.Performance })}
>
{lang('MenuSendGift')}
{lang('MenuAnimations')}
</ListItem>
)}
</div>
<div className="settings-main-menu">
<ListItem
icon="ask-support"
narrow
onClick={openSupportDialog}
>
{lang('AskAQuestion')}
</ListItem>
<ListItem
icon="help"
narrow
onClick={() => openUrl({ url: FAQ_URL })}
>
{lang('MenuTelegramFaq')}
</ListItem>
<ListItem
icon="privacy-policy"
narrow
onClick={() => openUrl({ url: PRIVACY_URL })}
>
{lang('MenuPrivacyPolicy')}
</ListItem>
<ListItem
icon="unmute"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Notifications })}
>
{lang('Notifications')}
</ListItem>
<ListItem
icon="data"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.DataStorage })}
>
{lang('DataSettings')}
</ListItem>
<ListItem
icon="lock"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Privacy })}
>
{lang('PrivacySettings')}
</ListItem>
<ListItem
icon="folder"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Folders })}
>
{lang('Filters')}
</ListItem>
<ListItem
icon="active-sessions"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.ActiveSessions })}
>
{lang('SessionsTitle')}
{sessionCount > 0 && (<span className="settings-item__current-value">{sessionCount}</span>)}
</ListItem>
<ListItem
icon="language"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Language })}
>
{lang('Language')}
<span className="settings-item__current-value">{lang.languageInfo.nativeName}</span>
</ListItem>
<ListItem
icon="stickers"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.Stickers })}
>
{lang('MenuStickers')}
</ListItem>
</Island>
<Island>
{canBuyPremium && (
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
narrow
onClick={() => openPremiumModal()}
>
{lang('TelegramPremium')}
</ListItem>
)}
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="gold" size="big" />}
narrow
onClick={() => openStarsBalanceModal({})}
>
{lang('MenuStars')}
{Boolean(starsBalance) && (
<span className="settings-item__current-value">
{formatStarsAmount(lang, starsBalance)}
</span>
)}
</ListItem>
<ListItem
leftElement={<Icon className="icon ListItem-main-icon" name="toncoin" />}
narrow
onClick={() => openStarsBalanceModal({ currency: TON_CURRENCY_CODE })}
>
{lang('MenuTon')}
{Boolean(tonBalance) && (
<span className="settings-item__current-value">
{convertCurrencyFromBaseUnit(tonBalance.amount, tonBalance.currency)}
</span>
)}
</ListItem>
{isGiveawayAvailable && (
<ListItem
icon="gift"
narrow
onClick={() => openGiftRecipientPicker()}
>
{lang('MenuSendGift')}
</ListItem>
)}
</Island>
<Island>
<ListItem
icon="ask-support"
narrow
onClick={openSupportDialog}
>
{lang('AskAQuestion')}
</ListItem>
<ListItem
icon="help"
narrow
onClick={() => openUrl({ url: FAQ_URL })}
>
{lang('MenuTelegramFaq')}
</ListItem>
<ListItem
icon="privacy-policy"
narrow
onClick={() => openUrl({ url: PRIVACY_URL })}
>
{lang('MenuPrivacyPolicy')}
</ListItem>
</Island>
</div>
<ConfirmDialog
isOpen={isSupportDialogOpen}

View File

@ -13,6 +13,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import useRunDebounced from '../../../hooks/useRunDebounced';
import Island, { IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import RangeSlider from '../../ui/RangeSlider';
@ -140,10 +141,10 @@ const SettingsNotifications = ({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('NotificationsWeb')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('NotificationsWeb')}
</IslandTitle>
<Island>
<Checkbox
label={lang('NotificationsWeb')}
subLabel={lang(hasWebNotifications ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
@ -160,22 +161,20 @@ const SettingsNotifications = ({
checked={hasPushNotifications}
onChange={handlePushNotificationsChange}
/>
<div className="settings-item-slider">
<RangeSlider
label={lang('NotificationsSound')}
min={0}
max={10}
disabled={!areNotificationsSupported}
value={notificationSoundVolume}
onChange={handleVolumeChange}
/>
</div>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('AutodownloadPrivateChats')}
</h4>
<RangeSlider
label={lang('NotificationsSound')}
min={0}
max={10}
disabled={!areNotificationsSupported}
value={notificationSoundVolume}
onChange={handleVolumeChange}
/>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('AutodownloadPrivateChats')}
</IslandTitle>
<Island>
<Checkbox
label={lang('NotificationsForPrivateChats')}
subLabel={lang(!areUsersMuted ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
@ -190,11 +189,10 @@ const SettingsNotifications = ({
checked={Boolean(notifyDefaults?.users?.shouldShowPreviews)}
onChange={handlePrivateChatsPreviewChange}
/>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterGroups')}</h4>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterGroups')}</IslandTitle>
<Island>
<Checkbox
label={lang('NotificationsForGroups')}
subLabel={lang(!areGroupsMuted ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
@ -209,11 +207,10 @@ const SettingsNotifications = ({
checked={Boolean(notifyDefaults?.groups?.shouldShowPreviews)}
onChange={handleGroupsPreviewChange}
/>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterChannels')}</h4>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterChannels')}</IslandTitle>
<Island>
<Checkbox
label={lang('NotificationsForChannels')}
subLabel={lang(!areChannelsMuted ? 'UserInfoNotificationsEnabled' : 'UserInfoNotificationsDisabled')}
@ -228,11 +225,10 @@ const SettingsNotifications = ({
checked={Boolean(notifyDefaults?.channels?.shouldShowPreviews)}
onChange={handleChannelsPreviewChange}
/>
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{lang('PhoneOther')}</h4>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('PhoneOther')}</IslandTitle>
<Island>
<Checkbox
label={lang('ContactJoined')}
checked={hasContactJoinedNotifications}
@ -243,7 +239,7 @@ const SettingsNotifications = ({
checked={shouldNotifyAboutPinnedMessages}
onChange={handlePinnedMessagesNotificationChange}
/>
</div>
</Island>
</div>
);
};

View File

@ -22,6 +22,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import CustomEmoji from '../../common/CustomEmoji';
import Icon from '../../common/icons/Icon';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Link from '../../ui/Link';
@ -139,7 +140,7 @@ const SettingsPasskeys = ({
{lang('SettingsPasskeyInfo')}
</p>
</div>
<div className="settings-item">
<Island className="settings-item">
{passkeys?.map(renderPasskey)}
{canAddPasskey && (
<Button
@ -153,17 +154,17 @@ const SettingsPasskeys = ({
{lang('SettingsPasskeysCreate')}
</Button>
)}
<p className="settings-item-description mt-3" dir="auto">
{lang('SettingsPasskeysFooter', {
link: (
<Link isPrimary onClick={handleOpenPasskeyModal}>
{lang('SettingsPasskeysFooterLink', undefined,
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
</Link>
),
}, { withNodes: true })}
</p>
</div>
</Island>
<IslandDescription dir="auto">
{lang('SettingsPasskeysFooter', {
link: (
<Link isPrimary onClick={handleOpenPasskeyModal}>
{lang('SettingsPasskeysFooterLink', undefined,
{ withNodes: true, specialReplacement: NEXT_ARROW_REPLACEMENT })}
</Link>
),
}, { withNodes: true })}
</IslandDescription>
<ConfirmDialog
isOpen={Boolean(deleteModalId)}
title={lang('PasskeyDeleteTitle')}

View File

@ -6,6 +6,7 @@ import useLang from '../../../hooks/useLang';
import PasswordForm from '../../common/PasswordForm';
import PasswordMonkey from '../../common/PasswordMonkey';
import Island from '../../gili/layout/Island';
type OwnProps = {
error?: string;
@ -65,21 +66,23 @@ const SettingsPasswordForm: FC<OwnProps> = ({
<PasswordMonkey isBig isPasswordVisible={shouldShowPassword} />
</div>
<div className="settings-item settings-group">
<PasswordForm
error={validationError || error}
hint={hint}
placeholder={placeholder || lang('CurrentPasswordPlaceholder')}
shouldDisablePasswordManager={shouldDisablePasswordManager}
submitLabel={submitLabel}
onClearError={handleClearError}
isLoading={isLoading}
isPasswordVisible={shouldShowPassword}
shouldResetValue={isActive}
onChangePasswordVisibility={setShouldShowPassword}
onSubmit={handleSubmit}
/>
</div>
<Island>
<div className="settings-input">
<PasswordForm
error={validationError || error}
hint={hint}
placeholder={placeholder || lang('CurrentPasswordPlaceholder')}
shouldDisablePasswordManager={shouldDisablePasswordManager}
submitLabel={submitLabel}
onClearError={handleClearError}
isLoading={isLoading}
isPasswordVisible={shouldShowPassword}
shouldResetValue={isActive}
onChangePasswordVisibility={setShouldShowPassword}
onSubmit={handleSubmit}
/>
</div>
</Island>
</div>
);
};

View File

@ -22,6 +22,7 @@ import { IS_BACKDROP_BLUR_SUPPORTED, IS_SNAP_EFFECT_SUPPORTED } from '../../../u
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import RangeSlider from '../../ui/RangeSlider';
@ -168,24 +169,22 @@ function SettingsPerformance({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SettingsPerformanceSliderTitle')}
</h4>
<p className="settings-item-description" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SettingsPerformanceSliderSubtitle')}
</p>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SettingsPerformanceSliderTitle')}
</IslandTitle>
<Island>
<RangeSlider
options={animationLevelOptions}
value={animationLevelState === ANIMATION_LEVEL_CUSTOM ? ANIMATION_LEVEL_MED : animationLevelState}
onChange={handleAnimationLevelChange}
/>
</div>
<div className="settings-item-simple settings-item__with-shifted-dropdown">
<h3 className="settings-item-header" dir="auto">{lang('SettingsPerformanceGranularTitle')}</h3>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('SettingsPerformanceSliderSubtitle')}
</IslandDescription>
<IslandTitle dir="auto">{lang('SettingsPerformanceGranularTitle')}</IslandTitle>
<Island>
{PERFORMANCE_OPTIONS.map(([sectionName, options], index) => {
return (
<div
@ -223,7 +222,7 @@ function SettingsPerformance({
</div>
);
})}
</div>
</Island>
</div>
);
}

View File

@ -19,6 +19,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
import Island, { IslandTitle } from '../../gili/layout/Island';
import Button from '../../ui/Button';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -196,11 +197,10 @@ const SettingsPrivacy = ({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<Island>
<ListItem
icon="delete-user"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyBlockedUsers })}
>
{oldLang('BlockedUsers')}
@ -210,7 +210,6 @@ const SettingsPrivacy = ({
<ListItem
icon="lock"
narrow
onClick={() => openSettingsScreen({
screen: hasPasscode ? SettingsScreens.PasscodeEnabled : SettingsScreens.PasscodeDisabled,
})}
@ -226,7 +225,6 @@ const SettingsPrivacy = ({
<ListItem
icon="admin"
narrow
onClick={() => openSettingsScreen({
screen: hasPassword ? SettingsScreens.TwoFaEnabled : SettingsScreens.TwoFaDisabled,
})}
@ -257,22 +255,19 @@ const SettingsPrivacy = ({
<ListItem
icon="web"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.ActiveWebsites })}
>
{oldLang('PrivacySettings.WebSessions')}
<span className="settings-item__current-value">{webAuthCount}</span>
</ListItem>
)}
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{oldLang('PrivacyTitle')}</h4>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{oldLang('PrivacyTitle')}</IslandTitle>
<Island>
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyPhoneNumber })}
>
<div className="multiline-item">
@ -285,7 +280,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyLastSeen })}
>
<div className="multiline-item">
@ -298,7 +292,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyProfilePhoto })}
>
<div className="multiline-item">
@ -311,7 +304,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyBio })}
>
<div className="multiline-item">
@ -324,7 +316,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyBirthday })}
>
<div className="multiline-item">
@ -337,7 +328,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyGifts })}
>
<div className="multiline-item">
@ -350,7 +340,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyForwarding })}
>
<div className="multiline-item">
@ -363,7 +352,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyPhoneCall })}
>
<div className="multiline-item">
@ -378,7 +366,6 @@ const SettingsPrivacy = ({
allowDisabledClick
rightElement={isCurrentUserPremium && <StarIcon size="big" type="premium" />}
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyVoiceMessages })}
>
<div className="multiline-item">
@ -392,7 +379,6 @@ const SettingsPrivacy = ({
narrow
rightElement={isCurrentUserPremium && <StarIcon size="big" type="premium" />}
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyMessages })}
>
<div className="multiline-item">
@ -408,7 +394,6 @@ const SettingsPrivacy = ({
<ListItem
narrow
className="no-icon"
onClick={() => openSettingsScreen({ screen: SettingsScreens.PrivacyGroupChats })}
>
<div className="multiline-item">
@ -418,65 +403,68 @@ const SettingsPrivacy = ({
</span>
</div>
</ListItem>
</div>
</Island>
{canChangeSensitive && (
<div className="settings-item fluid-container">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_settings_sensitive_title')}
</h4>
<Checkbox
label={oldLang('lng_settings_sensitive_disable_filtering')}
subLabel={oldLang('lng_settings_sensitive_about')}
checked={Boolean(isSensitiveEnabled)}
disabled={!canChangeSensitive || (!isSensitiveEnabled && needAgeVideoVerification)}
onCheck={handleUpdateContentSettings}
/>
{!isSensitiveEnabled && needAgeVideoVerification && (
<Button
color="primary"
fluid
noForcedUpperCase
className="settings-unlock-button"
onClick={handleAgeVerification}
>
<span className="settings-unlock-button-title">
{lang('ButtonAgeVerification')}
</span>
</Button>
)}
</div>
</IslandTitle>
<Island>
<Checkbox
label={oldLang('lng_settings_sensitive_disable_filtering')}
subLabel={oldLang('lng_settings_sensitive_about')}
checked={Boolean(isSensitiveEnabled)}
disabled={!canChangeSensitive || (!isSensitiveEnabled && needAgeVideoVerification)}
onCheck={handleUpdateContentSettings}
/>
{!isSensitiveEnabled && needAgeVideoVerification && (
<Button
color="primary"
noForcedUpperCase
className="settings-unlock-button"
onClick={handleAgeVerification}
>
<span className="settings-unlock-button-title">
{lang('ButtonAgeVerification')}
</span>
</Button>
)}
</Island>
</>
)}
{canDisplayAutoarchiveSetting && (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('NewChatsFromNonContacts')}
</h4>
<Checkbox
label={oldLang('ArchiveAndMute')}
subLabel={oldLang('ArchiveAndMuteInfo')}
checked={Boolean(shouldArchiveAndMuteNewNonContact)}
onCheck={handleArchiveAndMuteChange}
/>
</div>
</IslandTitle>
<Island>
<Checkbox
label={oldLang('ArchiveAndMute')}
subLabel={oldLang('ArchiveAndMuteInfo')}
checked={Boolean(shouldArchiveAndMuteNewNonContact)}
onCheck={handleArchiveAndMuteChange}
/>
</Island>
</>
)}
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_settings_window_system')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('lng_settings_window_system')}
</IslandTitle>
<Island>
<Checkbox
label={oldLang('lng_settings_title_chat_name')}
checked={Boolean(canDisplayChatInTitle)}
onCheck={handleChatInTitleChange}
/>
</div>
</Island>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('DeleteMyAccount')}
</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('DeleteMyAccount')}
</IslandTitle>
<Island>
<ListItem
narrow
onClick={handleOpenDeleteAccountModal}
@ -486,7 +474,7 @@ const SettingsPrivacy = ({
{lang('Months', { count: dayOption }, { pluralValue: 1 })}
</span>
</ListItem>
</div>
</Island>
</div>
);
};

View File

@ -0,0 +1,15 @@
.surface {
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
.listIsland {
overflow: hidden;
flex: 0 1 auto;
min-height: 0;
margin-bottom: 1rem;
padding: 0;
}

View File

@ -16,11 +16,15 @@ import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import FullNameTitle from '../../common/FullNameTitle';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Surface from '../../gili/layout/Surface';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem';
import Loading from '../../ui/Loading';
import BlockUserModal from './BlockUserModal';
import styles from './SettingsPrivacyBlockedUsers.module.scss';
type OwnProps = {
isActive?: boolean;
onReset: () => void;
@ -118,25 +122,28 @@ const SettingsPrivacyBlockedUsers: FC<OwnProps & StateProps> = ({
return (
<div className="settings-fab-wrapper">
<div className="settings-content infinite-scroll">
<div className="settings-item no-border">
<p className="settings-item-description-larger mt-0 mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('BlockedUsersInfo')}
</p>
</div>
<Surface className={styles.surface}>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('BlockedUsersInfo')}
</IslandDescription>
<div className="chat-list custom-scroll">
{blockedIds?.length ? (
<div className="scroll-container settings-item">
{blockedIds.map((contactId, i) => renderContact(contactId, i, 0))}
</div>
) : blockedIds && !blockedIds.length ? (
<div className="no-results" dir="auto">{lang('NoBlocked')}</div>
) : (
<Loading key="loading" />
)}
</div>
</div>
<Island className={styles.listIsland}>
<div className="chat-list custom-scroll">
{blockedIds?.length ? (
<div
className="scroll-container"
style={`height: ${blockedIds.length * CHAT_HEIGHT_PX}px`}
>
{blockedIds.map((contactId, i) => renderContact(contactId, i, 0))}
</div>
) : blockedIds && !blockedIds.length ? (
<div className="no-results" dir="auto">{lang('NoBlocked')}</div>
) : (
<Loading key="loading" />
)}
</div>
</Island>
</Surface>
<FloatingActionButton
isShown

View File

@ -10,6 +10,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import StarIcon from '../../common/icons/StarIcon';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -42,33 +43,32 @@ const SettingsPrivacyLastSeen = ({
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}>
<>
<Island>
<Checkbox
label={lang('HideReadTime')}
checked={shouldHideReadMarks}
onCheck={handleChangeShouldHideReadMarks}
/>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{renderText(lang('HideReadTimeInfo'), ['br'])}
</p>
</div>
</IslandDescription>
</>
)}
<div className="settings-item">
<Island>
<ListItem
leftElement={<StarIcon className="icon ListItem-main-icon" type="premium" size="big" />}
onClick={handleOpenPremiumModal}
>
{isCurrentUserPremium ? lang('PrivacyLastSeenPremiumForPremium') : lang('PrivacyLastSeenPremium')}
</ListItem>
<p
className="settings-item-description-larger premium-info"
dir={lang.isRtl ? 'rtl' : undefined}
>
{isCurrentUserPremium
? lang('PrivacyLastSeenPremiumInfoForPremium')
: lang('PrivacyLastSeenPremiumInfo')}
</p>
</div>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{isCurrentUserPremium
? lang('PrivacyLastSeenPremiumInfoForPremium')
: lang('PrivacyLastSeenPremiumInfo')}
</IslandDescription>
</>
);
};

View File

@ -10,6 +10,7 @@ import useFlag from '../../../hooks/useFlag';
import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import Island, { IslandDescription } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import SelectAvatar from '../../ui/SelectAvatar';
@ -63,44 +64,46 @@ const SettingsPrivacyPublicProfilePhoto: FC<OwnProps> = ({
}, []);
return (
<div className="settings-item">
<ListItem
narrow
icon="camera-add"
onClick={handleOpenFileSelector}
>
<SelectAvatar
onChange={handleSelectFile}
inputRef={inputRef}
/>
{lang(currentUserFallbackPhoto
? 'Privacy.ProfilePhoto.UpdatePublicPhoto'
: 'Privacy.ProfilePhoto.SetPublicPhoto')}
</ListItem>
{currentUserFallbackPhoto && (
<>
<Island>
<ListItem
narrow
leftElement={<Avatar photo={currentUserFallbackPhoto} size="mini" className={styles.fallbackPhoto} />}
onClick={openDeleteFallbackPhotoModal}
destructive
icon="camera-add"
onClick={handleOpenFileSelector}
>
{lang(currentUserFallbackPhoto.isVideo
? 'Privacy.ProfilePhoto.RemovePublicVideo'
: 'Privacy.ProfilePhoto.RemovePublicPhoto')}
<ConfirmDialog
isOpen={isDeleteFallbackPhotoModalOpen}
onClose={closeDeleteFallbackPhotoModal}
text={lang('Privacy.ResetPhoto.Confirm')}
confirmLabel={lang('Delete')}
confirmHandler={handleConfirmDelete}
confirmIsDestructive
<SelectAvatar
onChange={handleSelectFile}
inputRef={inputRef}
/>
{lang(currentUserFallbackPhoto
? 'Privacy.ProfilePhoto.UpdatePublicPhoto'
: 'Privacy.ProfilePhoto.SetPublicPhoto')}
</ListItem>
)}
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{currentUserFallbackPhoto && (
<ListItem
narrow
leftElement={<Avatar photo={currentUserFallbackPhoto} size="mini" className={styles.fallbackPhoto} />}
onClick={openDeleteFallbackPhotoModal}
destructive
>
{lang(currentUserFallbackPhoto.isVideo
? 'Privacy.ProfilePhoto.RemovePublicVideo'
: 'Privacy.ProfilePhoto.RemovePublicPhoto')}
<ConfirmDialog
isOpen={isDeleteFallbackPhotoModalOpen}
onClose={closeDeleteFallbackPhotoModal}
text={lang('Privacy.ResetPhoto.Confirm')}
confirmLabel={lang('Delete')}
confirmHandler={handleConfirmDelete}
confirmIsDestructive
/>
</ListItem>
)}
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('Privacy.ProfilePhoto.PublicPhotoInfo')}
</p>
</div>
</IslandDescription>
</>
);
};

View File

@ -14,6 +14,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import Switcher from '../../ui/Switcher';
@ -95,25 +96,27 @@ const SettingsPrivacyVisibility: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content custom-scroll">
{screen === SettingsScreens.PrivacyGifts && (
<div className="settings-item">
<ListItem onClick={handleShowGiftIconInChats}>
<span>{lang('PrivacyDisplayGiftsButton')}</span>
<Switcher
id="gift"
disabled={!isCurrentUserPremium}
label={shouldDisplayGiftsButton ? lang('HideGiftsButton') : lang('DisplayGiftsButton')}
checked={shouldDisplayGiftsButton}
/>
</ListItem>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<Island>
<ListItem onClick={handleShowGiftIconInChats}>
<span>{lang('PrivacyDisplayGiftsButton')}</span>
<Switcher
id="gift"
disabled={!isCurrentUserPremium}
label={shouldDisplayGiftsButton ? lang('HideGiftsButton') : lang('DisplayGiftsButton')}
checked={shouldDisplayGiftsButton}
/>
</ListItem>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('PrivacyDisplayGiftIconInChats', {
icon: <Icon name="gift" className="gift-icon" />,
gift: lang('PrivacyDisplayGift'),
}, {
withNodes: true,
})}
</p>
</div>
</IslandDescription>
</>
)}
<PrivacySubsection
screen={screen}
@ -347,54 +350,54 @@ function PrivacySubsection({
return (
<>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>{headerText}</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{headerText}</IslandTitle>
<Island>
<RadioGroup
name={`visibility-${privacyKey}`}
options={visibilityOptions}
onChange={handleVisibilityChange}
selected={privacy?.visibility}
/>
{descriptionText && (
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>{descriptionText}</p>
)}
</div>
</Island>
{descriptionText && (
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>{descriptionText}</IslandDescription>
)}
{!isPremiumRequired && (primaryExceptionLists.shouldShowAllowed || primaryExceptionLists.shouldShowDenied) && (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('PrivacyExceptions')}
</h4>
{primaryExceptionLists.shouldShowAllowed && (
<ListItem
narrow
icon="add-user"
onClick={() => {
openSettingsScreen({ screen: allowedContactsScreen });
}}
>
<div className="multiline-item full-size">
<span className="title">{oldLang('AlwaysAllow')}</span>
<span className="subtitle">{allowedString}</span>
</div>
</ListItem>
)}
{primaryExceptionLists.shouldShowDenied && (
<ListItem
narrow
icon="delete-user"
onClick={() => {
openSettingsScreen({ screen: deniedContactsScreen });
}}
>
<div className="multiline-item full-size">
<span className="title">{oldLang('NeverAllow')}</span>
<span className="subtitle">{blockString}</span>
</div>
</ListItem>
)}
</div>
</IslandTitle>
<Island>
{primaryExceptionLists.shouldShowAllowed && (
<ListItem
narrow
icon="add-user"
onClick={() => {
openSettingsScreen({ screen: allowedContactsScreen });
}}
>
<div className="multiline-item full-size">
<span className="title">{oldLang('AlwaysAllow')}</span>
<span className="subtitle">{allowedString}</span>
</div>
</ListItem>
)}
{primaryExceptionLists.shouldShowDenied && (
<ListItem
narrow
icon="delete-user"
onClick={() => {
openSettingsScreen({ screen: deniedContactsScreen });
}}
>
<div className="multiline-item full-size">
<span className="title">{oldLang('NeverAllow')}</span>
<span className="subtitle">{blockString}</span>
</div>
</ListItem>
)}
</Island>
</>
)}
{isPremiumRequired && <PremiumStatusItem />}
</>

View File

@ -183,7 +183,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
}
return (
<div className="NewChat-inner step-1">
<div className="Picker settings-picker-islands">
<PeerPicker
categories={getCustomCategory()}
itemIds={displayedIds || []}
@ -201,6 +201,7 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
itemInputType="checkbox"
withDefaultPadding
withStatus
withIslands
/>
<FloatingActionButton

View File

@ -7,6 +7,8 @@ import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
import useHistoryBack from '../../../hooks/useHistoryBack';
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import Island from '../../gili/layout/Island';
import Surface from '../../gili/layout/Surface';
import RadioGroup from '../../ui/RadioGroup';
type OwnProps = {
@ -52,15 +54,17 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
}, [setDefaultReaction]);
return (
<div className="settings-content settings-item custom-scroll settings-quick-reaction">
<RadioGroup
name="quick-reaction-settings"
options={options}
selected={selectedReaction?.type === 'emoji' ? selectedReaction.emoticon : undefined}
onChange={handleChange}
withIcon
/>
</div>
<Surface scrollable className="settings-content settings-quick-reaction">
<Island>
<RadioGroup
name="quick-reaction-settings"
options={options}
selected={selectedReaction?.type === 'emoji' ? selectedReaction.emoticon : undefined}
onChange={handleChange}
withIcon
/>
</Island>
</Surface>
);
};

View File

@ -24,6 +24,7 @@ import useOldLang from '../../../hooks/useOldLang';
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import StickerSetCard from '../../common/StickerSetCard';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
@ -93,7 +94,7 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
return (
<div className="settings-content custom-scroll">
<div className="settings-item">
<Island>
<Checkbox
label={lang('SuggestStickers')}
checked={shouldSuggestStickers}
@ -101,7 +102,6 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
/>
<ListItem
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.CustomEmoji })}
icon="smile"
>
@ -112,7 +112,6 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
<ListItem
className="SettingsDefaultReaction"
narrow
onClick={() => openSettingsScreen({ screen: SettingsScreens.QuickReaction })}
>
<ReactionStaticEmoji
@ -124,40 +123,44 @@ const SettingsStickers: FC<OwnProps & StateProps> = ({
<div className="title">{lang('DoubleTapSetting')}</div>
</ListItem>
)}
</div>
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('InstalledStickers.DynamicPackOrder')}
</h4>
</Island>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('InstalledStickers.DynamicPackOrder')}
</IslandTitle>
<Island>
<Checkbox
label={lang('InstalledStickers.DynamicPackOrder')}
checked={shouldUpdateStickerSetOrder}
onCheck={handleSuggestStickerSetOrderChange}
/>
<p className="settings-item-description mt-3" dir="auto">
{lang('InstalledStickers.DynamicPackOrderInfo')}
</p>
</div>
</Island>
<IslandDescription dir="auto">
{lang('InstalledStickers.DynamicPackOrderInfo')}
</IslandDescription>
{stickerSets && (
<div className="settings-item">
<h4 className="settings-item-header" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('ChooseStickerMyStickerSets')}
</h4>
<div ref={stickerSettingsRef}>
{stickerSets.map((stickerSet: ApiStickerSet) => (
<StickerSetCard
key={stickerSet.id}
stickerSet={stickerSet}
observeIntersection={observeIntersectionForCovers}
onClick={handleStickerSetClick}
noPlay={!canPlayAnimatedEmojis}
/>
))}
</div>
<p className="settings-item-description mt-3" dir="auto">
</IslandTitle>
<Island>
<div ref={stickerSettingsRef}>
{stickerSets.map((stickerSet: ApiStickerSet) => (
<StickerSetCard
key={stickerSet.id}
stickerSet={stickerSet}
observeIntersection={observeIntersectionForCovers}
onClick={handleStickerSetClick}
noPlay={!canPlayAnimatedEmojis}
/>
))}
</div>
</Island>
<IslandDescription dir="auto">
{renderText(lang('StickersBotInfo'), ['links'])}
</p>
</div>
</IslandDescription>
</>
)}
</div>
);

View File

@ -123,7 +123,6 @@
gap: 0.75rem;
align-items: center;
margin-inline: 1rem;
padding: 0.125rem;
}
@ -224,11 +223,19 @@
background-color: var(--accent-color);
}
.settings-folders-header.settings-content-header {
padding-inline: 0;
}
.settings-folders-input-container {
position: relative;
display: flex;
align-items: center;
align-self: stretch;
.input-group {
margin: 0.5rem;
}
}
.settings-folders-input-with-icon .form-control {
@ -236,11 +243,11 @@
}
.settings-folders-icon-picker {
--custom-emoji-size: 2rem;
--custom-emoji-size: 1.5rem;
position: absolute;
inset-inline-end: 0.5rem;
font-size: 2rem;
inset-inline-end: 1.5rem;
font-size: 1.5rem;
color: var(--color-text-secondary);
}

View File

@ -137,7 +137,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
}
return (
<div className="Picker settings-folders-chat-list">
<div className="Picker settings-folders-chat-list settings-picker-islands">
<PeerPicker
categories={shouldHideChatTypes ? undefined : chatTypes}
itemIds={displayedIds}
@ -150,6 +150,7 @@ const SettingsFoldersChatFilters: FC<OwnProps & StateProps> = ({
isSearchable
withDefaultPadding
withPeerTypes
withIslands
allowMultiple
itemInputType="checkbox"
onSelectedIdsChange={handleSelectedIdsChange}

View File

@ -40,6 +40,8 @@ import FolderIcon from '../../../common/FolderIcon';
import GroupChatInfo from '../../../common/GroupChatInfo';
import Icon from '../../../common/icons/Icon';
import PrivateChatInfo from '../../../common/PrivateChatInfo';
import Island, { IslandDescription, IslandOutside, IslandTitle } from '../../../gili/layout/Island';
import Surface from '../../../gili/layout/Surface';
import FloatingActionButton from '../../../ui/FloatingActionButton';
import InputText from '../../../ui/InputText';
import ListItem from '../../../ui/ListItem';
@ -381,8 +383,8 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
return (
<div className="settings-fab-wrapper">
<div className="settings-content no-border custom-scroll">
<div className="settings-content-header">
<Surface scrollable className="settings-content no-border">
<IslandOutside className="settings-content-header settings-folders-header">
<AnimatedIconWithPreview
size={STICKER_SIZE_FOLDER_SETTINGS}
tgsUrl={LOCAL_TGS_URLS.FoldersNew}
@ -395,9 +397,9 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
{lang('FilterIncludeInfo')}
</p>
)}
<div className="settings-folders-input-container">
<Island className="settings-folders-input-container">
<InputText
className={buildClassName('mb-0', !isMobile && 'settings-folders-input-with-icon')}
className={buildClassName(!isMobile && 'settings-folders-input-with-icon')}
label={lang('FilterNameHint')}
value={titleText}
maxLength={folderTitleMaxLength}
@ -424,67 +426,69 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
/>
</div>
)}
</div>
</div>
</Island>
</IslandOutside>
{!isOnlyInvites && (
<div className="settings-item">
<>
{state.error && state.error === ERROR_NO_CHATS && (
<p className="settings-item-description color-danger mb-2" dir={lang.isRtl ? 'rtl' : undefined}>
<IslandDescription className="color-danger" dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang(state.error)}
</p>
</IslandDescription>
)}
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</h4>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterInclude')}</IslandTitle>
<Island>
<ListItem
className="settings-folders-list-item color-primary"
icon="add"
narrow
onClick={onAddIncludedChats}
>
{lang('FilterAddChats')}
</ListItem>
<ListItem
className="settings-folders-list-item color-primary"
icon="add"
narrow
onClick={onAddIncludedChats}
>
{lang('FilterAddChats')}
</ListItem>
{renderChats('included')}
</div>
{renderChats('included')}
</Island>
</>
)}
{!isOnlyInvites && !isEditingChatList && (
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</h4>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('FilterExclude')}</IslandTitle>
<Island>
<ListItem
className="settings-folders-list-item color-primary"
icon="add"
narrow
onClick={onAddExcludedChats}
>
{lang('FilterAddChats')}
</ListItem>
<ListItem
className="settings-folders-list-item color-primary"
icon="add"
narrow
onClick={onAddExcludedChats}
>
{lang('FilterAddChats')}
</ListItem>
{renderChats('excluded')}
</div>
{renderChats('excluded')}
</Island>
</>
)}
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3 color-picker-header" dir={lang.isRtl ? 'rtl' : undefined}>
<span className="color-picker-title-text">{lang('FilterColorTitle')}</span>
<div className={buildClassName(
'color-picker-title',
'color-picker-selected-color',
isCurrentUserPremium && state.folder.color !== undefined && state.folder.color !== -1
? getPeerColorClass(state.folder.color)
: 'color-picker-item-disabled',
)}
>
{renderTextWithEntities({
text: state.folder.title.text,
entities: state.folder.title.entities,
noCustomEmojiPlayback: state.folder.noTitleAnimations,
})}
</div>
</h4>
<IslandTitle className="color-picker-header" dir={lang.isRtl ? 'rtl' : undefined}>
<span className="color-picker-title-text">{lang('FilterColorTitle')}</span>
<div className={buildClassName(
'color-picker-title',
'color-picker-selected-color',
isCurrentUserPremium && state.folder.color !== undefined && state.folder.color !== -1
? getPeerColorClass(state.folder.color)
: 'color-picker-item-disabled',
)}
>
{renderTextWithEntities({
text: state.folder.title.text,
entities: state.folder.title.entities,
noCustomEmojiPlayback: state.folder.noTitleAnimations,
})}
</div>
</IslandTitle>
<Island>
<div className="color-picker custom-scroll-x">
{FOLDER_COLORS.map((color) => (
<button
@ -530,16 +534,15 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
)}
</button>
</div>
<p className="settings-item-description mb-0 mt-3" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterColorHint')}
</p>
</div>
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('FolderLinkScreen.Title')}
</h4>
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterColorHint')}
</IslandDescription>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{oldLang('FolderLinkScreen.Title')}
</IslandTitle>
<Island>
<ListItem
className="settings-folders-list-item color-primary"
icon="add"
@ -564,9 +567,8 @@ const SettingsFoldersEdit: FC<OwnProps & StateProps> = ({
</span>
</ListItem>
))}
</div>
</div>
</Island>
</Surface>
<FloatingActionButton
isShown={Boolean(state.isTouched)}

View File

@ -28,6 +28,8 @@ import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated';
import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview';
import Icon from '../../../common/icons/Icon';
import Island, { IslandOutside, IslandTitle } from '../../../gili/layout/Island';
import Surface from '../../../gili/layout/Surface';
import Button from '../../../ui/Button';
import Checkbox from '../../../ui/Checkbox';
import Draggable from '../../../ui/Draggable';
@ -225,8 +227,8 @@ const SettingsFoldersMain = ({
}, [foldersById, isPremium, maxFolders]);
return (
<div className="settings-content no-border custom-scroll">
<div className="settings-content-header">
<Surface scrollable className="settings-content no-border">
<IslandOutside className="settings-content-header">
<AnimatedIconWithPreview
size={STICKER_SIZE_FOLDER_SETTINGS}
tgsUrl={LOCAL_TGS_URLS.FoldersAll}
@ -250,11 +252,10 @@ const SettingsFoldersMain = ({
{lang('CreateNewFilter')}
</Button>
)}
</div>
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('Filters')}</h4>
</IslandOutside>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('Filters')}</IslandTitle>
<Island>
<div className="settings-sortable-container" style={`height: ${(folderIds?.length || 0) * FOLDER_HEIGHT_PX}px`}>
{userFolders?.length ? userFolders.map((folder, i) => {
const isBlocked = i > maxFolders - 1;
@ -365,48 +366,49 @@ const SettingsFoldersMain = ({
</p>
) : <Loading />}
</div>
</div>
</Island>
{(recommendedChatFolders && Boolean(recommendedChatFolders.length)) && (
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('FilterRecommended')}
</h4>
</IslandTitle>
<Island>
{recommendedChatFolders.map((folder) => (
<ListItem
key={folder.id}
narrow
onClick={() => handleCreateFolderFromRecommended(folder)}
>
<div className="settings-folders-recommended-item">
<div className="multiline-item">
<span className="title">
{renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
})}
</span>
<span className="subtitle">{folder.description}</span>
</div>
{recommendedChatFolders.map((folder) => (
<ListItem
narrow
onClick={() => handleCreateFolderFromRecommended(folder)}
>
<div className="settings-folders-recommended-item">
<div className="multiline-item">
<span className="title">
{renderTextWithEntities({
text: folder.title.text,
entities: folder.title.entities,
noCustomEmojiPlayback: folder.noTitleAnimations,
})}
</span>
<span className="subtitle">{folder.description}</span>
<Button
className="px-3"
color="primary"
size="tiny"
pill
fluid
isRtl={lang.isRtl}
>
{lang('Add')}
</Button>
</div>
<Button
className="px-3"
color="primary"
size="tiny"
pill
fluid
isRtl={lang.isRtl}
>
{lang('Add')}
</Button>
</div>
</ListItem>
))}
</div>
</ListItem>
))}
</Island>
</>
)}
<div className="settings-item pt-3">
<Island>
<div className="settings-item-relative">
<Checkbox
label={lang('ShowFolderTags')}
@ -422,26 +424,27 @@ const SettingsFoldersMain = ({
/>
{!isPremium && <Icon name="lock-badge" className="settings-folders-lock-icon" />}
</div>
</div>
</Island>
{!isMobile && (
<div className="settings-item pt-3">
<h4 className="settings-item-header mb-3" dir={lang.isRtl ? 'rtl' : undefined}>{lang('TabsPosition')}</h4>
<RadioGroup
name="tabsPosition"
options={[{
label: lang('TabsPositionLeft'),
value: FOLDERS_POSITION_LEFT,
}, {
label: lang('TabsPositionTop'),
value: FOLDERS_POSITION_TOP,
}]}
selected={foldersPosition}
onChange={handleFoldersPositionChange}
/>
</div>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang('TabsPosition')}</IslandTitle>
<Island>
<RadioGroup
name="tabsPosition"
options={[{
label: lang('TabsPositionLeft'),
value: FOLDERS_POSITION_LEFT,
}, {
label: lang('TabsPositionTop'),
value: FOLDERS_POSITION_TOP,
}]}
selected={foldersPosition}
onChange={handleFoldersPositionChange}
/>
</Island>
</>
)}
</div>
</Surface>
);
};

View File

@ -26,6 +26,7 @@ import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIcon from '../../../common/AnimatedIcon';
import LinkField from '../../../common/LinkField';
import PeerPicker from '../../../common/pickers/PeerPicker';
import Island, { IslandTitle } from '../../../gili/layout/Island';
import FloatingActionButton from '../../../ui/FloatingActionButton';
type OwnProps = {
@ -170,15 +171,18 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
)}
</div>
<LinkField
className="settings-item"
link={!url ? oldLang('Loading') : url}
withShare
onRevoke={handleRevoke}
isDisabled={!chatsCount || isTouched}
/>
<IslandTitle>{oldLang('InviteLink.InviteLink')}</IslandTitle>
<Island>
<LinkField
link={!url ? oldLang('Loading') : url}
withShare
noTitle
onRevoke={handleRevoke}
isDisabled={!chatsCount || isTouched}
/>
</Island>
<div className="settings-item settings-item-picker">
<Island>
<PeerPicker
itemIds={itemIds}
lockedUnselectedIds={lockedIds}
@ -189,7 +193,7 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
withStatus
itemInputType="checkbox"
/>
</div>
</Island>
<FloatingActionButton
isShown={isLoading || isTouched}

View File

@ -43,9 +43,7 @@ const SettingsPasscodeCongratulations: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item settings-group">
<Button onClick={fullReset}>{lang('Back')}</Button>
</div>
<Button onClick={fullReset}>{lang('Back')}</Button>
</div>
);
};

View File

@ -10,6 +10,7 @@ import useHistoryBack from '../../../../hooks/useHistoryBack';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview';
import Island from '../../../gili/layout/Island';
import ListItem from '../../../ui/ListItem';
import lockPreviewUrl from '../../../../assets/lock.png';
@ -42,7 +43,7 @@ const SettingsPasscodeEnabled: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item">
<Island>
<ListItem
icon="edit"
@ -57,7 +58,7 @@ const SettingsPasscodeEnabled: FC<OwnProps> = ({
>
{lang('Passcode.TurnOff')}
</ListItem>
</div>
</Island>
</div>
);
};

View File

@ -43,9 +43,7 @@ const SettingsPasscodeStart: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item settings-group">
<Button onClick={onStart}>{lang('EnablePasscode')}</Button>
</div>
<Button onClick={onStart}>{lang('EnablePasscode')}</Button>
</div>
);
};

View File

@ -48,9 +48,7 @@ const SettingsTwoFaCongratulations: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item settings-group">
<Button onClick={handleClick}>{lang('TwoStepVerificationPasswordReturnSettings')}</Button>
</div>
<Button onClick={handleClick}>{lang('TwoStepVerificationPasswordReturnSettings')}</Button>
</div>
);
};

View File

@ -14,6 +14,7 @@ import useHistoryBack from '../../../../hooks/useHistoryBack';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Island from '../../../gili/layout/Island';
import InputText from '../../../ui/InputText';
import Loading from '../../../ui/Loading';
@ -88,7 +89,7 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
)}
</div>
<div className="settings-item settings-group">
<Island>
<InputText
value={value}
ref={inputRef}
@ -98,7 +99,7 @@ const SettingsTwoFaEmailCode: FC<OwnProps & StateProps> = ({
onChange={handleInputChange}
/>
{isLoading && <Loading />}
</div>
</Island>
</div>
);
};

View File

@ -11,6 +11,7 @@ import useHistoryBack from '../../../../hooks/useHistoryBack';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../../common/AnimatedIconWithPreview';
import Island from '../../../gili/layout/Island';
import ListItem from '../../../ui/ListItem';
import lockPreviewUrl from '../../../../assets/lock.png';
@ -46,7 +47,7 @@ const SettingsTwoFaEnabled: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item">
<Island>
<ListItem
icon="edit"
@ -68,7 +69,7 @@ const SettingsTwoFaEnabled: FC<OwnProps> = ({
>
{lang('SetRecoveryEmail')}
</ListItem>
</div>
</Island>
</div>
);
};

View File

@ -16,6 +16,7 @@ import useHistoryBack from '../../../../hooks/useHistoryBack';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker';
import Island from '../../../gili/layout/Island';
import Button from '../../../ui/Button';
import InputText from '../../../ui/InputText';
import Modal from '../../../ui/Modal';
@ -107,8 +108,8 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
)}
</div>
<div className="settings-item settings-group">
<form action="" onSubmit={handleSubmit}>
<Island>
<form className="settings-input" action="" onSubmit={handleSubmit}>
<InputText
ref={inputRef}
value={value}
@ -161,7 +162,7 @@ const SettingsTwoFaSkippableForm: FC<OwnProps & StateProps> = ({
</div>
</Modal>
)}
</div>
</Island>
</div>
);
};

View File

@ -42,9 +42,7 @@ const SettingsTwoFaStart: FC<OwnProps> = ({
</p>
</div>
<div className="settings-item settings-group">
<Button onClick={onStart}>{lang('EditAdminTransferSetPassword')}</Button>
</div>
<Button onClick={onStart}>{lang('EditAdminTransferSetPassword')}</Button>
</div>
);
};

View File

@ -1,11 +1,17 @@
.AddChatMembers {
position: relative;
overflow: hidden;
height: 100%;
background: var(--color-background);
padding: 1rem;
background: var(--color-background-secondary);
&-inner {
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
}
}

View File

@ -131,6 +131,7 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
onFilterChange={handleFilterChange}
isSearchable
withDefaultPadding
withIslands
noScrollRestore={noPickerScrollRestore}
allowMultiple
withStatus

View File

@ -20,6 +20,7 @@ import useOldLang from '../../hooks/useOldLang';
import CustomEmojiPicker from '../common/CustomEmojiPicker';
import TopicIcon from '../common/TopicIcon';
import Island from '../gili/layout/Island';
import FloatingActionButton from '../ui/FloatingActionButton';
import InputText from '../ui/InputText';
import Transition from '../ui/Transition';
@ -114,7 +115,7 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
return (
<div className={styles.root}>
<div className={buildClassName(styles.content, 'custom-scroll')}>
<div className={buildClassName(styles.section, styles.top)}>
<Island className={styles.top}>
<span className={styles.heading}>{lang('CreateTopicTitle')}</span>
<Transition
name="zoomFade"
@ -138,8 +139,8 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
disabled={isLoading}
teactExperimentControlled
/>
</div>
<div className={buildClassName(styles.section, styles.bottom)}>
</Island>
<Island className={styles.bottom}>
<CustomEmojiPicker
idPrefix="create-topic-icons-set-"
isHidden={!isActive}
@ -149,7 +150,7 @@ const CreateTopic: FC<OwnProps & StateProps> = ({
pickerListClassName="fab-padding-bottom"
withDefaultTopicIcons
/>
</div>
</Island>
</div>
<FloatingActionButton
isShown={isTouched}

View File

@ -20,6 +20,7 @@ import useOldLang from '../../hooks/useOldLang';
import CustomEmojiPicker from '../common/CustomEmojiPicker';
import TopicIcon from '../common/TopicIcon';
import Island from '../gili/layout/Island';
import FloatingActionButton from '../ui/FloatingActionButton';
import InputText from '../ui/InputText';
import Loading from '../ui/Loading';
@ -136,7 +137,7 @@ const EditTopic: FC<OwnProps & StateProps> = ({
{!topic && <Loading />}
{topic && (
<>
<div className={buildClassName(styles.section, styles.top, isGeneral && styles.general)}>
<Island className={buildClassName(styles.top, isGeneral && styles.general)}>
<span className={styles.heading}>{lang(isGeneral ? 'CreateGeneralTopicTitle' : 'CreateTopicTitle')}</span>
<Transition
name="zoomFade"
@ -160,9 +161,9 @@ const EditTopic: FC<OwnProps & StateProps> = ({
disabled={isLoading}
teactExperimentControlled
/>
</div>
</Island>
{!isGeneral && (
<div className={buildClassName(styles.section, styles.bottom)}>
<Island className={styles.bottom}>
<CustomEmojiPicker
idPrefix="edit-topic-icons-set-"
isHidden={!isActive}
@ -172,7 +173,7 @@ const EditTopic: FC<OwnProps & StateProps> = ({
pickerListClassName="fab-padding-bottom"
withDefaultTopicIcons
/>
</div>
</Island>
)}
</>
)}

View File

@ -5,37 +5,35 @@
position: relative;
height: 100%;
background-color: var(--color-background);
background-color: var(--color-background-secondary);
}
.content {
overflow: auto;
display: flex;
flex-direction: column;
height: 100%;
}
.section {
display: flex;
flex-direction: column;
justify-content: center;
@include mixins.side-panel-section;
padding: 1rem;
}
.general {
border-bottom: 0;
box-shadow: none;
}
.top {
display: flex;
flex-direction: column;
padding: 1rem 1.5rem;
}
.bottom {
display: flex;
flex-direction: column;
flex-grow: 1;
min-height: 30rem;
margin-bottom: 0;
padding-bottom: 0;
}
.iconWrapper {

View File

@ -7,7 +7,7 @@
height: 100%;
:global(.FloatingActionButton) {
z-index: 1;
z-index: 3;
@include mixins.with-vt-type('rightColumn');

View File

@ -1267,7 +1267,6 @@ const Profile = ({
chatOrUserId={profileId}
isSavedDialog={isSavedDialog}
isOwnProfile={isOwnProfile}
withIslands
className={styles.chatExtraBlock}
style={createVtnStyle('chatExtraBlock', true)}
/>

View File

@ -20,6 +20,8 @@
}
> .Transition {
--slide-background-color: var(--color-background-secondary);
overflow: hidden;
height: calc(100% - var(--header-height));
}
@ -33,7 +35,9 @@
}
}
.Management .section > .ChatInfo {
.Management .section > .ChatInfo,
.Management > .custom-scroll > .ChatInfo,
.Management > .panel-content > .ChatInfo {
margin: 1rem 0;
padding: 0 1.5rem;
text-align: center;

View File

@ -1,11 +1,11 @@
import type { FC } from '@teact';
import { memo, useEffect, useRef, useState } from '@teact';
import { memo, useEffect, useLayoutEffect, useRef, useState } from '@teact';
import { getActions, withGlobal } from '../../global';
import type { AnimationLevel, ThreadId } from '../../types';
import { ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent } from '../../types';
import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
import { MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
import { getIsSavedDialog } from '../../global/helpers';
import {
selectAreActiveChatsLoaded,
@ -20,10 +20,12 @@ import captureEscKeyListener from '../../util/captureEscKeyListener';
import { resolveTransitionName } from '../../util/resolveTransitionName.ts';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useFlag from '../../hooks/useFlag';
import useHistoryBack from '../../hooks/useHistoryBack';
import useLastCallback from '../../hooks/useLastCallback';
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
import useScrollNotch from '../../hooks/useScrollNotch.ts';
import useSyncEffect from '../../hooks/useSyncEffect';
import useWindowSize from '../../hooks/window/useWindowSize';
import Transition from '../ui/Transition';
@ -63,7 +65,6 @@ type StateProps = {
hasProfileBackground?: boolean;
};
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
const MAIN_SCREENS_COUNT = Object.keys(RightColumnContent).length / 2;
const MANAGEMENT_SCREENS_COUNT = Object.keys(ManagementScreens).length / 2;
@ -120,7 +121,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
const [isProfileExpanded, setIsProfileExpanded] = useState(false);
const [isProfileScrolled, setIsProfileScrolled] = useState(false);
const isScrolledDown = profileState !== ProfileState.Profile;
const isOpen = contentKey !== undefined;
@ -138,43 +138,57 @@ const RightColumn: FC<OwnProps & StateProps> = ({
const isCreatingTopic = contentKey === RightColumnContent.CreateTopic;
const isEditingTopic = contentKey === RightColumnContent.EditTopic;
const isOverlaying = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN;
const isContentScrolledRef = useRef(false);
const headerBackground: 'regular' | 'secondary' = (() => {
if (isSavedMessages) return 'secondary';
if (!isProfile) return 'regular';
const hasStaticHeader = isSavedMessages || isAddingChatMembers || isCreatingTopic || isEditingTopic
|| (isManagement && (
managementScreen === ManagementScreens.GroupMembers
|| managementScreen === ManagementScreens.ChatAdministrators
|| managementScreen === ManagementScreens.GroupAddAdmins
));
const getHeaderBackground = useLastCallback((): 'regular' | 'secondary' => {
const isScrolled = isContentScrolledRef.current;
if (hasStaticHeader) return 'secondary';
if (!isProfile) return isScrolled ? 'regular' : 'secondary';
if (isScrolledDown) return 'secondary';
if (!isProfileScrolled && !isProfileExpanded && !hasProfileBackground) return 'secondary';
if (!isScrolled && !isProfileExpanded && !hasProfileBackground) return 'secondary';
return 'regular';
})();
});
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isOpen);
const [headerBackground, setHeaderBackground] = useState<'regular' | 'secondary'>(getHeaderBackground);
const handleContentScrolled = useLastCallback((isScrolled: boolean) => {
isContentScrolledRef.current = isScrolled;
setHeaderBackground(getHeaderBackground());
});
const [isAnimating, startAnimating, stopAnimating] = useFlag();
useLayoutEffect(() => {
const elements = containerRef.current?.querySelectorAll<HTMLElement>(
':scope .custom-scroll, :scope .panel-content',
);
elements?.forEach((el) => {
el.scrollTop = 0;
});
isContentScrolledRef.current = false;
setHeaderBackground(getHeaderBackground());
}, [contentKey, managementScreen]);
useSyncEffect(() => {
setHeaderBackground(getHeaderBackground());
}, [isScrolledDown, isProfileExpanded, hasProfileBackground]);
const renderingContentKey = useCurrentOrPrev(contentKey, true, !isChatSelected) ?? -1;
useScrollNotch({
containerRef,
selector: ':scope .custom-scroll, :scope .panel-content',
shouldHideTopNotch: isSavedMessages || (isProfile && isScrolledDown),
shouldHideTopNotch: isAnimating || isSavedMessages || hasStaticHeader || (isProfile && isScrolledDown),
onScrolled: handleContentScrolled,
}, [contentKey, managementScreen, chatId, threadId]);
useEffect(() => {
if (!isProfile || isScrolledDown || isSavedMessages) {
setIsProfileScrolled(false);
return undefined;
}
const scrollEl = containerRef.current?.querySelector<HTMLElement>('.custom-scroll');
if (!scrollEl) return undefined;
const handleProfileScroll = () => {
setIsProfileScrolled(scrollEl.scrollTop > 1);
};
handleProfileScroll();
scrollEl.addEventListener('scroll', handleProfileScroll, { passive: true });
return () => scrollEl.removeEventListener('scroll', handleProfileScroll);
}, [chatId, threadId, isProfile, isScrolledDown, isSavedMessages]);
const close = useLastCallback((shouldScrollUp = true) => {
switch (contentKey) {
case RightColumnContent.AddingMembers:
@ -278,12 +292,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
useEffect(() => (isOpen && chatId ? captureEscKeyListener(close) : undefined), [isOpen, close, chatId]);
useEffect(() => {
setTimeout(() => {
setShouldSkipTransition(!isOpen);
}, ANIMATION_DURATION);
}, [isOpen]);
useEffect(() => {
if (nextManagementScreen) {
setManagementScreen(nextManagementScreen);
@ -429,10 +437,12 @@ const RightColumn: FC<OwnProps & StateProps> = ({
/>
<Transition
ref={containerRef}
name={resolveTransitionName('layers', animationLevel, shouldSkipTransition || shouldSkipHistoryAnimations)}
name={resolveTransitionName('layers', animationLevel, !isOpen || shouldSkipHistoryAnimations)}
renderCount={MAIN_SCREENS_COUNT + MANAGEMENT_SCREENS_COUNT}
activeKey={isManagement ? MAIN_SCREENS_COUNT + managementScreen : renderingContentKey}
shouldCleanup
onStart={startAnimating}
onStop={stopAnimating}
cleanupExceptionKey={
(renderingContentKey === RightColumnContent.MessageStatistics
|| renderingContentKey === RightColumnContent.StoryStatistics)

View File

@ -26,6 +26,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import Island, { IslandDescription } from '../../gili/layout/Island';
import AvatarEditable from '../../ui/AvatarEditable';
import FloatingActionButton from '../../ui/FloatingActionButton';
import InputText from '../../ui/InputText';
@ -183,7 +184,7 @@ const ManageBot: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="custom-scroll">
<div className="section">
<Island>
<AvatarEditable
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handlePhotoChange}
@ -206,8 +207,8 @@ const ManageBot: FC<OwnProps & StateProps> = ({
maxLength={maxBioLength}
maxLengthIndicator={maxBioLength ? (maxBioLength - bio.length).toString() : undefined}
/>
</div>
<div className="section">
</Island>
<Island>
<div className="dialog-buttons">
<ListItem icon="bot-commands-filled" ripple onClick={handleChangeEditIntro}>
<span>{lang('BotEditIntro')}</span>
@ -218,11 +219,11 @@ const ManageBot: FC<OwnProps & StateProps> = ({
<ListItem icon="bots" ripple onClick={handleChangeSettings}>
<span>{lang('BotChangeSettings')}</span>
</ListItem>
<div className="section-info section-info_push">
<IslandDescription>
{renderText(lang('BotManageInfo'), ['links'])}
</div>
</IslandDescription>
</div>
</div>
</Island>
</div>
<FloatingActionButton
isShown={isFieldTouched || isAvatarTouched}

View File

@ -21,6 +21,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import Island from '../../gili/layout/Island';
import AvatarEditable from '../../ui/AvatarEditable';
import ConfirmDialog from '../../ui/ConfirmDialog';
import FloatingActionButton from '../../ui/FloatingActionButton';
@ -225,12 +226,12 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section">
<AvatarEditable
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handleSetPhoto}
disabled={!canChangeInfo}
/>
<AvatarEditable
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handleSetPhoto}
disabled={!canChangeInfo}
/>
<Island>
<div className="settings-edit">
<InputText
id="channel-title"
@ -318,8 +319,8 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
/>
</ListItem>
)}
</div>
<div className="section">
</Island>
<Island>
<ListItem
icon="admin"
multiline
@ -344,12 +345,12 @@ const ManageChannel: FC<OwnProps & StateProps> = ({
<span className="title">{lang('ChannelBlockedUsers')}</span>
<span className="subtitle">{removedUsersCount}</span>
</ListItem>
</div>
<div className="section">
</Island>
<Island>
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
{chat.isCreator ? lang('ChannelDelete') : lang('LeaveChannel')}
</ListItem>
</div>
</Island>
</div>
<FloatingActionButton
isShown={isProfileFieldsTouched}

View File

@ -14,6 +14,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem';
@ -111,7 +112,7 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section">
<Island>
<ListItem
icon="recent"
multiline
@ -120,15 +121,14 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
<span className="title">{lang('EventLog')}</span>
<span className="subtitle">{lang(isChannel ? 'EventLogInfoDetailChannel' : 'EventLogInfoDetail')}</span>
</ListItem>
</div>
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
<p className="section-help" dir="auto">
{lang(isChannel
? 'Channel.Management.AddModeratorHelp'
: 'Group.Management.AddModeratorHelp')}
</p>
</Island>
<IslandDescription dir="auto">
{lang(isChannel
? 'Channel.Management.AddModeratorHelp'
: 'Group.Management.AddModeratorHelp')}
</IslandDescription>
<Island dir={lang.isRtl ? 'rtl' : undefined}>
{adminMembers.map((member) => (
<ListItem
key={member.userId}
@ -150,10 +150,10 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
ariaLabel={lang('Channel.Management.AddModerator')}
iconName="add-user-filled"
/>
</div>
</Island>
{canToggleSignatures && (
<div className="section">
<Island>
<div className="ListItem narrow">
<Checkbox
checked={areSignaturesEnabled}
@ -170,12 +170,12 @@ const ManageChatAdministrators: FC<OwnProps & StateProps> = ({
onChange={handleToggleProfiles}
/>
</div>
<p className="section-info section-info_push">
<IslandDescription>
{lang('ChannelSignProfilesInfo')}
</p>
</IslandDescription>
</>
)}
</div>
</Island>
)}
</div>
</div>

View File

@ -25,6 +25,7 @@ import LinkField from '../../common/LinkField';
import ManageUsernames from '../../common/ManageUsernames';
import SafeLink from '../../common/SafeLink';
import UsernameInput from '../../common/UsernameInput';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem';
@ -184,71 +185,77 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
const purchaseInfoLink = `${TME_LINK_PREFIX}${PURCHASE_USERNAME}`;
return (
<p className="section-info" dir="auto">
<IslandDescription dir="auto">
{(lang('lng_username_purchase_available'))
.replace('{link}', '%PURCHASE_LINK%')
.split('%')
.map((s) => {
return (s === 'PURCHASE_LINK' ? <SafeLink url={purchaseInfoLink} text={`@${PURCHASE_USERNAME}`} /> : s);
})}
</p>
</IslandDescription>
);
}
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
<h3 className="section-heading">{lang(`${langPrefix2}Type`)}</h3>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>{lang(`${langPrefix2}Type`)}</IslandTitle>
<Island dir={lang.isRtl ? 'rtl' : undefined}>
<RadioGroup
selected={privacyType}
name="channel-type"
options={options}
onChange={handleOptionChange}
/>
</div>
</Island>
{privacyType === 'private' ? (
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
{privateInviteLink ? (
<>
<LinkField link={privateInviteLink} className="invite-link" />
<p className="section-info" dir={lang.isRtl ? 'rtl' : undefined}>
{lang(`${langPrefix1}PrivateLinkHelp`)}
</p>
<ListItem icon="delete" ripple destructive onClick={openRevokeConfirmDialog}>
{lang('RevokeLink')}
</ListItem>
<ConfirmDialog
isOpen={isRevokeConfirmDialogOpen}
onClose={closeRevokeConfirmDialog}
text={lang('RevokeAlert')}
confirmLabel={lang('RevokeButton')}
confirmHandler={handleRevokePrivateLink}
confirmIsDestructive
/>
</>
) : (
<Loading />
)}
</div>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('InviteLink.InviteLink')}
</IslandTitle>
<Island dir={lang.isRtl ? 'rtl' : undefined}>
{privateInviteLink ? (
<>
<LinkField link={privateInviteLink} className="invite-link" noTitle />
<ListItem icon="delete" ripple destructive onClick={openRevokeConfirmDialog}>
{lang('RevokeLink')}
</ListItem>
<ConfirmDialog
isOpen={isRevokeConfirmDialogOpen}
onClose={closeRevokeConfirmDialog}
text={lang('RevokeAlert')}
confirmLabel={lang('RevokeButton')}
confirmHandler={handleRevokePrivateLink}
confirmIsDestructive
/>
</>
) : (
<Loading />
)}
</Island>
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang(`${langPrefix1}PrivateLinkHelp`)}
</IslandDescription>
</>
) : (
<div className="section no-border">
<div className="settings-input">
<UsernameInput
asLink
currentUsername={currentUsername}
isLoading={isLoading}
isUsernameAvailable={isUsernameAvailable}
checkedUsername={checkedUsername}
onChange={handleUsernameChange}
/>
</div>
<>
<Island>
<div className="settings-input">
<UsernameInput
asLink
currentUsername={currentUsername}
isLoading={isLoading}
isUsernameAvailable={isUsernameAvailable}
checkedUsername={checkedUsername}
onChange={handleUsernameChange}
/>
</div>
</Island>
{error === USERNAME_PURCHASE_ERROR && renderPurchaseLink()}
<p className="section-info" dir="auto">
<IslandDescription dir="auto">
{lang(`${langPrefix2}.Username.CreatePublicLinkHelp`)}
</p>
</div>
</IslandDescription>
</>
)}
{shouldRenderUsernamesManage && (
<ManageUsernames
@ -257,22 +264,22 @@ const ManageChatPrivacyType: FC<OwnProps & StateProps> = ({
onEditUsername={handleUsernameChange}
/>
)}
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
<h3 className="section-heading">
{lang(isChannel ? 'ChannelVisibility.Forwarding.ChannelTitle' : 'ChannelVisibility.Forwarding.GroupTitle')}
</h3>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang(isChannel ? 'ChannelVisibility.Forwarding.ChannelTitle' : 'ChannelVisibility.Forwarding.GroupTitle')}
</IslandTitle>
<Island dir={lang.isRtl ? 'rtl' : undefined}>
<RadioGroup
selected={isProtected ? 'protected' : 'allowed'}
name="forwarding-type"
options={forwardingOptions}
onChange={handleForwardingOptionChange}
/>
<p className="section-info section-info_push">
{isChannel
? lang('ChannelVisibility.Forwarding.ChannelInfo')
: lang('ChannelVisibility.Forwarding.GroupInfo')}
</p>
</div>
</Island>
<IslandDescription>
{isChannel
? lang('ChannelVisibility.Forwarding.ChannelInfo')
: lang('ChannelVisibility.Forwarding.GroupInfo')}
</IslandDescription>
</div>
<FloatingActionButton
isShown={canUpdate}

View File

@ -13,6 +13,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useOldLang from '../../../hooks/useOldLang';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription } from '../../gili/layout/Island';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem, { type MenuItemContextAction } from '../../ui/ListItem';
import RemoveGroupUserModal from './RemoveGroupUserModal';
@ -83,40 +84,41 @@ const ManageChatRemovedUsers: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section" dir={lang.isRtl ? 'rtl' : undefined}>
<p className="section-help">{lang(isChannel ? 'NoBlockedChannel2' : 'NoBlockedGroup2')}</p>
{removedMembers.map((member) => (
<ListItem
key={member.userId}
className="chat-item-clickable"
ripple
contextActions={getContextActions(member)}
>
<PrivateChatInfo
userId={member.userId}
status={getRemovedBy(member)}
forceShowSelf
/>
</ListItem>
))}
{canDeleteMembers && (
<FloatingActionButton
isShown
onClick={openRemoveUserModal}
ariaLabel={lang('Channel.EditAdmin.Permission.BanUsers')}
iconName="add-user-filled"
/>
)}
{chat && canDeleteMembers && (
<RemoveGroupUserModal
chat={chat}
isOpen={isRemoveUserModalOpen}
onClose={closeRemoveUserModal}
/>
)}
</div>
<IslandDescription>{lang(isChannel ? 'NoBlockedChannel2' : 'NoBlockedGroup2')}</IslandDescription>
{Boolean(removedMembers.length) && (
<Island dir={lang.isRtl ? 'rtl' : undefined}>
{removedMembers.map((member) => (
<ListItem
key={member.userId}
className="chat-item-clickable"
ripple
contextActions={getContextActions(member)}
>
<PrivateChatInfo
userId={member.userId}
status={getRemovedBy(member)}
forceShowSelf
/>
</ListItem>
))}
</Island>
)}
</div>
{canDeleteMembers && (
<FloatingActionButton
isShown
onClick={openRemoveUserModal}
ariaLabel={lang('Channel.EditAdmin.Permission.BanUsers')}
iconName="add-user-filled"
/>
)}
{chat && canDeleteMembers && (
<RemoveGroupUserModal
chat={chat}
isOpen={isRemoveUserModalOpen}
onClose={closeRemoveUserModal}
/>
)}
</div>
);
};

View File

@ -22,6 +22,7 @@ import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Avatar from '../../common/Avatar';
import GroupChatInfo from '../../common/GroupChatInfo';
import NothingFound from '../../common/NothingFound';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
@ -208,7 +209,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
function renderDiscussionGroups() {
return (
<div>
<p className="section-help" dir="auto">{lang('DiscussionChannelHelp')}</p>
<IslandDescription dir="auto">{lang('DiscussionChannelHelp')}</IslandDescription>
<div teactFastList>
<ListItem
@ -239,7 +240,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
<NothingFound key="nothing-found" teactOrderKey={0} text="No discussion groups found" />
)}
</div>
<p className="mt-4 mb-0 section-help" dir="auto">{lang('DiscussionChannelHelp2')}</p>
<IslandDescription className="mt-4 mb-0" dir="auto">{lang('DiscussionChannelHelp2')}</IslandDescription>
<ConfirmDialog
isOpen={isConfirmLinkGroupDialogOpen}
onClose={closeConfirmLinkGroupDialog}
@ -255,7 +256,7 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section">
<Island>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.DiscussionGroups}
size={STICKER_SIZE_DISCUSSION_GROUPS}
@ -263,30 +264,32 @@ const ManageDiscussion: FC<OwnProps & StateProps> = ({
/>
{linkedChat && renderLinkedGroup()}
{!linkedChat && renderDiscussionGroups()}
</div>
</Island>
{linkedChat && (
<div className="section">
<h3 className="section-heading">{lang('ChannelSettingsJoinTitle')}</h3>
<div className="ListItem narrow">
<Checkbox
checked={isJoinToSend}
onCheck={handleJoinToSendCheck}
label={lang('ChannelSettingsJoinToSend')}
/>
</div>
{isJoinToSend && (
<>
<IslandTitle>{lang('ChannelSettingsJoinTitle')}</IslandTitle>
<Island>
<div className="ListItem narrow">
<Checkbox
checked={isJoinRequest}
onCheck={handleJoinRequestCheck}
label={lang('ChannelSettingsJoinRequest')}
checked={isJoinToSend}
onCheck={handleJoinToSendCheck}
label={lang('ChannelSettingsJoinToSend')}
/>
</div>
)}
<p className="section-info section-info_push">
{isJoinToSend && (
<div className="ListItem narrow">
<Checkbox
checked={isJoinRequest}
onCheck={handleJoinRequestCheck}
label={lang('ChannelSettingsJoinRequest')}
/>
</div>
)}
</Island>
<IslandDescription>
{isJoinToSend ? lang('ChannelSettingsJoinRequestInfo') : lang('ChannelSettingsJoinToSendInfo')}
</p>
</div>
</IslandDescription>
</>
)}
</div>
</div>

View File

@ -28,6 +28,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useOldLang from '../../../hooks/useOldLang';
import Island, { IslandDescription } from '../../gili/layout/Island';
import AvatarEditable from '../../ui/AvatarEditable';
import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
@ -320,13 +321,13 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section">
<AvatarEditable
isForForum={isForumEnabled}
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handleSetPhoto}
disabled={!canChangeInfo}
/>
<AvatarEditable
isForForum={isForumEnabled}
currentAvatarBlobUrl={currentAvatarBlobUrl}
onChange={handleSetPhoto}
disabled={!canChangeInfo}
/>
<Island>
<div className="settings-edit">
<InputText
id="group-title"
@ -431,11 +432,11 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
inactive
/>
</ListItem>
<div className="section-info section-info_push">{lang('ForumToggleDescription')}</div>
<IslandDescription>{lang('ForumToggleDescription')}</IslandDescription>
</>
)}
</div>
<div className="section">
</Island>
<Island>
<ListItem icon="group" multiline onClick={handleClickMembers}>
<span className="title">{lang('GroupMembers')}</span>
<span className="subtitle">{formatInteger(chat.membersCount ?? 0)}</span>
@ -455,12 +456,12 @@ const ManageGroup: FC<OwnProps & StateProps> = ({
/>
</div>
)}
</div>
<div className="section">
</Island>
<Island>
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
{lang('DeleteMega')}
</ListItem>
</div>
</Island>
</div>
<FloatingActionButton
isShown={isProfileFieldsTouched}

View File

@ -18,6 +18,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import PasswordConfirmModal from '../../common/PasswordConfirmModal';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
import FloatingActionButton from '../../ui/FloatingActionButton';
@ -257,7 +258,7 @@ const ManageGroupAdminRights = ({
return (
<div className="Management">
<div className="panel-content custom-scroll">
<div className="section">
<Island>
<ListItem inactive className="chat-item-clickable">
<PrivateChatInfo
userId={selectedChatMember.userId}
@ -266,8 +267,11 @@ const ManageGroupAdminRights = ({
/>
</ListItem>
<h3 className="section-heading mt-4" dir="auto">{lang('EditAdminWhatCanDo')}</h3>
</Island>
<IslandTitle dir="auto">{lang('EditAdminWhatCanDo')}</IslandTitle>
<Island>
<div className="ListItem">
<Checkbox
name="changeInfo"
@ -442,9 +446,9 @@ const ManageGroupAdminRights = ({
)}
{isFormFullyDisabled && (
<p className="section-info mb-4" dir="auto">
<IslandDescription className="mb-4" dir="auto">
{lang('EditAdminUnavailable')}
</p>
</IslandDescription>
)}
{!isChannel && (
@ -469,7 +473,7 @@ const ManageGroupAdminRights = ({
{lang('EditAdminRemoveAdmin')}
</ListItem>
)}
</div>
</Island>
</div>
<FloatingActionButton

View File

@ -21,11 +21,13 @@ import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling'
import useHistoryBack from '../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import NothingFound from '../../common/NothingFound';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription } from '../../gili/layout/Island';
import FloatingActionButton from '../../ui/FloatingActionButton';
import InfiniteScroll from '../../ui/InfiniteScroll';
import InputText from '../../ui/InputText';
@ -86,7 +88,8 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
openChat, setUserSearchQuery, closeManagement,
toggleParticipantsHidden, setNewChatMembersDialogState, toggleManagement,
} = getActions();
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
const inputRef = useRef<HTMLInputElement>();
const containerRef = useRef<HTMLDivElement>();
@ -182,7 +185,7 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined {
return memberId === currentUserId || !canDeleteMembers ? undefined : [{
title: lang('lng_context_remove_from_group'),
title: oldLang('lng_context_remove_from_group'),
icon: 'stop',
handler: () => {
setDeletingUserId(memberId);
@ -192,34 +195,37 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
function renderSearchField() {
return (
<div className="Management__filter" dir={lang.isRtl ? 'rtl' : undefined}>
<Island dir={oldLang.isRtl ? 'rtl' : undefined}>
<InputText
ref={inputRef}
value={searchQuery}
onChange={handleFilterChange}
placeholder={lang('Search')}
placeholder={oldLang('Search')}
noMargin
/>
</div>
</Island>
);
}
return (
<div className="Management">
{noAdmins && renderSearchField()}
<div className="panel-content custom-scroll">
<div className="Management ManageGroupMembers">
<div className="panel-content">
{noAdmins && renderSearchField()}
{canHideParticipants && !isChannel && (
<div className="section">
<ListItem icon="group" ripple onClick={handleToggleParticipantsHidden}>
<span>{lang('ChannelHideMembers')}</span>
<Switcher label={lang('ChannelHideMembers')} checked={areParticipantsHidden} />
</ListItem>
<p className="section-info">
{lang(areParticipantsHidden ? 'GroupMembers.MembersHiddenOn' : 'GroupMembers.MembersHiddenOff')}
</p>
</div>
<>
<Island>
<ListItem icon="group" ripple onClick={handleToggleParticipantsHidden}>
<span>{oldLang('ChannelHideMembers')}</span>
<Switcher label={oldLang('ChannelHideMembers')} checked={areParticipantsHidden} />
</ListItem>
</Island>
<IslandDescription>
{oldLang(areParticipantsHidden ? 'GroupMembers.MembersHiddenOn' : 'GroupMembers.MembersHiddenOff')}
</IslandDescription>
</>
)}
<div className="section">
{viewportIds?.length ? (
{viewportIds?.length ? (
<Island className="island-list">
<InfiniteScroll
className="picker-list custom-scroll"
items={displayedIds}
@ -232,7 +238,6 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
<ListItem
key={id}
className="chat-item-clickable scroll-item"
onClick={() => handleMemberClick(id)}
contextActions={getMemberContextAction(id)}
withPortalForMenu
@ -241,22 +246,22 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
</ListItem>
))}
</InfiniteScroll>
) : !isSearching && viewportIds && !viewportIds.length ? (
<NothingFound
teactOrderKey={0}
key="nothing-found"
text={isChannel ? 'No subscribers found' : 'No members found'}
/>
) : (
<Loading />
)}
</div>
</Island>
) : !isSearching && viewportIds && !viewportIds.length ? (
<NothingFound
teactOrderKey={0}
key="nothing-found"
text={lang(isChannel ? 'NoSubscribersFound' : 'NoMembersFound')}
/>
) : (
<Loading />
)}
</div>
{canAddMembers && (
<FloatingActionButton
isShown
onClick={handleNewMemberDialogOpen}
ariaLabel={lang('lng_channel_add_users')}
ariaLabel={oldLang('lng_channel_add_users')}
iconName="add-user-filled"
/>
)}

View File

@ -27,6 +27,7 @@ import useManagePermissions from '../hooks/useManagePermissions';
import PaidMessagePrice from '../../common/paidMessage/PaidMessagePrice';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
import FloatingActionButton from '../../ui/FloatingActionButton';
import ListItem from '../../ui/ListItem';
@ -222,7 +223,6 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
const arePermissionsChanged = isPriceForMessagesChanged || havePermissionChanged;
const arePermissionsLoading = progress === ManagementProgress.InProgress || isLoading;
return (
<div
className="Management with-shifted-dropdown"
@ -230,8 +230,8 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
+ `--before-shift-height: ${BEFORE_ITEMS_COUNT * ITEM_HEIGHT}px;`}
>
<div className="panel-content custom-scroll">
<div className="section without-bottom-shadow">
<h3 className="section-heading" dir="auto">{lang('ChannelPermissionsHeader')}</h3>
<IslandTitle dir="auto">{lang('ChannelPermissionsHeader')}</IslandTitle>
<Island className={buildClassName('without-bottom-shadow', isMediaDropdownOpen && 'dropdown-open')}>
<PermissionCheckboxList
chatId={chat?.id}
isMediaDropdownOpen={isMediaDropdownOpen}
@ -243,17 +243,12 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
'DropdownList',
isMediaDropdownOpen && 'DropdownList--open',
)}
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
shiftedClassName="part"
/>
</div>
</Island>
{arePaidMessagesAvailable && (
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<Island>
<ListItem onClick={handleChargeStarsForMessages}>
<span>{lang('GroupMessagesChargePrice')}</span>
<Switcher
@ -262,34 +257,26 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
checked={isPriceForMessagesOpen}
/>
</ListItem>
<p className="settings-item-description-larger" dir={lang.isRtl ? 'rtl' : undefined}>
{lang('RightsChargeStarsAbout')}
</p>
</div>
</Island>
)}
{arePaidMessagesAvailable && (
<IslandDescription dir={lang.isRtl ? 'rtl' : undefined}>
{lang('RightsChargeStarsAbout')}
</IslandDescription>
)}
{isPriceForMessagesOpen && (
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<Island>
<PaidMessagePrice
canChangeChargeForMessages
isGroupChat
chargeForMessages={chargeForMessages}
onChange={handleChargeForMessagesChange}
/>
</div>
</Island>
)}
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<Island>
<ListItem
icon="delete-user"
multiline
@ -299,16 +286,10 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
<span className="title">{lang('ChannelBlockedUsers')}</span>
<span className="subtitle">{removedUsersCount}</span>
</ListItem>
</div>
<div
className={buildClassName(
'section',
isMediaDropdownOpen && 'shifted',
)}
>
<h3 className="section-heading" dir="auto">{lang('PrivacyExceptions')}</h3>
</Island>
<IslandTitle dir="auto">{lang('PrivacyExceptions')}</IslandTitle>
<Island>
<ListItem
icon="add-user"
onClick={handleAddExceptionClick}
@ -330,7 +311,7 @@ const ManageGroupPermissions: FC<OwnProps & StateProps> = ({
/>
</ListItem>
))}
</div>
</Island>
</div>
<FloatingActionButton

View File

@ -17,6 +17,7 @@ import useOldLang from '../../../hooks/useOldLang';
import useManagePermissions from '../hooks/useManagePermissions';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandTitle } from '../../gili/layout/Island';
import PermissionCheckboxList from '../../main/PermissionCheckboxList';
import ConfirmDialog from '../../ui/ConfirmDialog';
import FloatingActionButton from '../../ui/FloatingActionButton';
@ -134,12 +135,12 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
+ `--before-shift-height: ${BEFORE_ITEMS_COUNT * ITEM_HEIGHT + BEFORE_USER_INFO_HEIGHT}px;`}
>
<div className="custom-scroll">
<div className="section without-bottom-shadow">
<Island className={buildClassName('without-bottom-shadow', isMediaDropdownOpen && 'dropdown-open')}>
<ListItem inactive className="chat-item-clickable">
<PrivateChatInfo userId={selectedChatMember.userId} forceShowSelf />
</ListItem>
<h3 className="section-heading mt-4" dir="auto">{oldLang('UserRestrictionsCanDo')}</h3>
<IslandTitle className="mt-4" dir="auto">{oldLang('UserRestrictionsCanDo')}</IslandTitle>
<PermissionCheckboxList
chatId={chat?.id}
isMediaDropdownOpen={isMediaDropdownOpen}
@ -151,22 +152,22 @@ const ManageGroupUserPermissions: FC<OwnProps & StateProps> = ({
isMediaDropdownOpen && 'DropdownList--open',
)}
dropdownClassName="DropdownListTrap"
shiftedClassName={buildClassName('part', isMediaDropdownOpen && 'shifted')}
shiftedClassName="part"
getControlIsDisabled={getControlIsDisabled}
/>
</div>
</Island>
{!isFormFullyDisabled && (
<div
<Island
className={buildClassName(
'section',
'part',
isMediaDropdownOpen && 'shifted',
)}
>
<ListItem icon="delete-user" ripple destructive onClick={openBanConfirmationDialog}>
{oldLang('UserRestrictionsBlock')}
</ListItem>
</div>
</Island>
)}
</div>

View File

@ -9,9 +9,11 @@ import { isChatChannel, sortUserIds } from '../../../global/helpers';
import { selectChat, selectChatFullInfo } from '../../../global/selectors';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import NothingFound from '../../common/NothingFound';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
type OwnProps = {
@ -39,6 +41,8 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
onClose,
isActive,
}) => {
const lang = useLang();
useHistoryBack({
isActive,
onBack: onClose,
@ -64,9 +68,9 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="custom-scroll">
<div className="section" teactFastList>
{memberIds ? (
memberIds.map((id, i) => (
{memberIds?.length ? (
<Island teactFastList>
{memberIds.map((id, i) => (
<ListItem
key={id}
teactOrderKey={i}
@ -76,15 +80,13 @@ const ManageGroupUserPermissionsCreate: FC<OwnProps & StateProps> = ({
>
<PrivateChatInfo userId={id} forceShowSelf />
</ListItem>
))
) : (
<NothingFound
teactOrderKey={0}
key="nothing-found"
text={isChannel ? 'No subscribers found' : 'No members found'}
/>
)}
</div>
))}
</Island>
) : (
<NothingFound
text={lang(isChannel ? 'NoSubscribersFound' : 'NoMembersFound')}
/>
)}
</div>
</div>
);

View File

@ -16,6 +16,7 @@ import useOldLang from '../../../hooks/useOldLang';
import useSyncEffect from '../../../hooks/useSyncEffect';
import CalendarModal from '../../common/CalendarModal';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Button from '../../ui/Button';
import Checkbox from '../../ui/Checkbox';
import FloatingActionButton from '../../ui/FloatingActionButton';
@ -158,25 +159,25 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
return (
<div className="Management ManageInvite">
<div className="panel-content custom-scroll">
<div className="section">
<Island>
<Checkbox
label={lang('ApproveNewMembers')}
subLabel={lang('ApproveNewMembersDescription')}
checked={isRequestNeeded}
onChange={handleIsRequestChange}
/>
</div>
<div className="section">
</Island>
<Island>
<InputText
className="link-name"
placeholder={lang('LinkNameHint')}
value={title}
onChange={handleTitleChange}
/>
<p className="section-help hint">{lang('LinkNameHelp')}</p>
</div>
<div className="section">
<div className="section-heading">{lang('LimitByPeriod')}</div>
<IslandDescription>{lang('LinkNameHelp')}</IslandDescription>
</Island>
<IslandTitle>{lang('LimitByPeriod')}</IslandTitle>
<Island>
<RadioGroup
name="expireOptions"
options={[
@ -211,39 +212,41 @@ const ManageInvite: FC<OwnProps & StateProps> = ({
{formatTime(lang, customExpireDate)}
</Button>
)}
<p className="section-help hint">{lang('TimeLimitHelp')}</p>
</div>
<IslandDescription>{lang('TimeLimitHelp')}</IslandDescription>
</Island>
{!isRequestNeeded && (
<div className="section">
<div className="section-heading">{lang('LimitNumberOfUses')}</div>
<RadioGroup
name="usageOptions"
options={[
...DEFAULT_USAGE_LIMITS.map((n) => ({ value: n.toString(), label: n })),
{
value: '0',
label: lang('NoLimit'),
},
{
value: 'custom',
label: lang('lng_group_invite_usage_custom'),
},
]}
onChange={setSelectedUsageOption}
selected={selectedUsageOption}
/>
{selectedUsageOption === 'custom' && (
<input
className="form-control usage-limit"
type="number"
min="1"
max="99999"
value={customUsageLimit}
onChange={handleCustomUsageLimitChange}
<>
<IslandTitle>{lang('LimitNumberOfUses')}</IslandTitle>
<Island>
<RadioGroup
name="usageOptions"
options={[
...DEFAULT_USAGE_LIMITS.map((n) => ({ value: n.toString(), label: n })),
{
value: '0',
label: lang('NoLimit'),
},
{
value: 'custom',
label: lang('lng_group_invite_usage_custom'),
},
]}
onChange={setSelectedUsageOption}
selected={selectedUsageOption}
/>
)}
<p className="section-help hint">{lang('UsesLimitHelp')}</p>
</div>
{selectedUsageOption === 'custom' && (
<input
className="form-control usage-limit"
type="number"
min="1"
max="99999"
value={customUsageLimit}
onChange={handleCustomUsageLimitChange}
/>
)}
<IslandDescription>{lang('UsesLimitHelp')}</IslandDescription>
</Island>
</>
)}
<FloatingActionButton
isShown

View File

@ -13,6 +13,7 @@ import useOldLang from '../../../hooks/useOldLang';
import LinkField from '../../common/LinkField';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import Spinner from '../../ui/Spinner';
@ -70,11 +71,13 @@ const ManageInviteInfo = ({
if (!importers?.length && requesters?.length) return undefined;
if (!importers) return <Spinner />;
return (
<div className="section">
<p className="section-heading">{importers.length ? lang('PeopleJoined', usage) : lang('NoOneJoined')}</p>
<p className="section-help">
<>
<IslandTitle>{importers.length ? lang('PeopleJoined', usage) : lang('NoOneJoined')}</IslandTitle>
<Island>
{!importers.length && (
usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')
<IslandDescription>
{usageLimit ? lang('PeopleCanJoinViaLinkCount', usageLimit - usage) : lang('NoOneJoinedYet')}
</IslandDescription>
)}
{importers.map((importer) => {
const joinTime = formatMediaDateTime(lang, importer.date * 1000, true);
@ -93,8 +96,8 @@ const ManageInviteInfo = ({
</ListItem>
);
})}
</p>
</div>
</Island>
</>
);
};
@ -103,9 +106,9 @@ const ManageInviteInfo = ({
if (!requesters && importers) return <Spinner />;
if (!requesters?.length) return undefined;
return (
<div className="section">
<p className="section-heading">{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</p>
<p className="section-help">
<>
<IslandTitle>{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</IslandTitle>
<Island>
{requesters.map((requester) => (
<ListItem
className="chat-item-clickable scroll-item small-icon"
@ -119,8 +122,8 @@ const ManageInviteInfo = ({
/>
</ListItem>
))}
</p>
</div>
</Island>
</>
);
};
@ -128,35 +131,37 @@ const ManageInviteInfo = ({
<div className="Management ManageInviteInfo">
<div className="panel-content custom-scroll">
{!invite && (
<p className="section-help">{lang('Loading')}</p>
<IslandDescription>{lang('Loading')}</IslandDescription>
)}
{invite && (
<>
<div className="section">
<Island>
<LinkField title={invite.title} link={invite.link} className="invite-link" />
{Boolean(expireDate) && (
<p className="section-help">
<IslandDescription>
{isExpired
? lang('ExpiredLink')
: lang('LinkExpiresIn', `${formatFullDate(lang, expireDate)} ${formatTime(lang, expireDate)}`)}
</p>
</IslandDescription>
)}
</div>
</Island>
{adminId && (
<div className="section">
<p className="section-heading">{lang('LinkCreatedeBy')}</p>
<ListItem
className="chat-item-clickable scroll-item small-icon"
<>
<IslandTitle>{lang('LinkCreatedeBy')}</IslandTitle>
<Island>
<ListItem
className="chat-item-clickable scroll-item small-icon"
onClick={() => openChat({ id: adminId })}
>
<PrivateChatInfo
userId={adminId}
status={formatMediaDateTime(lang, invite.date * 1000, true)}
forceShowSelf
/>
</ListItem>
</div>
onClick={() => openChat({ id: adminId })}
>
<PrivateChatInfo
userId={adminId}
status={formatMediaDateTime(lang, invite.date * 1000, true)}
forceShowSelf
/>
</ListItem>
</Island>
</>
)}
{renderImporters()}
{renderRequesters()}

View File

@ -26,6 +26,7 @@ import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Icon from '../../common/icons/Icon';
import LinkField from '../../common/LinkField';
import NothingFound from '../../common/NothingFound';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem, { type MenuItemContextAction } from '../../ui/ListItem';
@ -274,26 +275,33 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
return (
<div className="Management ManageInvites">
<div className="panel-content custom-scroll">
<div className="section">
<Island>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.Invite}
size={STICKER_SIZE_INVITES}
className="section-icon"
/>
<p className="section-help">{isChannel ? oldLang('PrimaryLinkHelpChannel') : oldLang('PrimaryLinkHelp')}</p>
</div>
<IslandDescription>
{isChannel ? oldLang('PrimaryLinkHelpChannel') : oldLang('PrimaryLinkHelp')}
</IslandDescription>
</Island>
{primaryInviteLink && (
<div className="section">
<LinkField
className="settings-input"
link={primaryInviteLink}
withShare
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
title={chat?.usernames ? oldLang('PublicLink') : oldLang('lng_create_permanent_link_title')}
/>
</div>
<>
<IslandTitle>
{chat?.usernames ? oldLang('PublicLink') : oldLang('lng_create_permanent_link_title')}
</IslandTitle>
<Island>
<LinkField
className="settings-input"
link={primaryInviteLink}
noTitle
withShare
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
/>
</Island>
</>
)}
<div className="section" teactFastList>
<Island teactFastList>
<ListItem icon="add" withPrimaryColor key="create" className="create-item" onClick={handleCreateNewClick}>
{oldLang('CreateNewLink')}
</ListItem>
@ -314,11 +322,11 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
</span>
</ListItem>
))}
<p className="section-help hint" key="links-hint">{oldLang('ManageLinksInfoHelp')}</p>
</div>
</Island>
<IslandDescription>{oldLang('ManageLinksInfoHelp')}</IslandDescription>
{revokedExportedInvites && Boolean(revokedExportedInvites.length) && (
<div className="section" teactFastList>
<p className="section-help" key="title">{oldLang('RevokedLinks')}</p>
<Island teactFastList>
<IslandDescription key="title">{oldLang('RevokedLinks')}</IslandDescription>
<ListItem
icon="delete"
destructive
@ -343,7 +351,7 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
</span>
</ListItem>
))}
</div>
</Island>
)}
</div>
<ConfirmDialog

View File

@ -15,6 +15,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useOldLang from '../../../hooks/useOldLang';
import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Spinner from '../../ui/Spinner';
@ -68,7 +69,7 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
return (
<div className="Management ManageJoinRequests">
<div className="custom-scroll">
<div className="section">
<Island>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.JoinRequest}
size={STICKER_SIZE_JOIN_REQUESTS}
@ -80,8 +81,8 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
<Button className="bulk-action-button" onClick={openRejectAllDialog} isText>Dismiss all</Button>
</div>
)}
</div>
<div className="section" teactFastList>
</Island>
<Island teactFastList>
<p key="title">
{!chat?.joinRequests ? lang('Loading') : chat.joinRequests.length
? lang('JoinRequests', chat.joinRequests.length) : lang('NoMemberRequests')}
@ -90,9 +91,9 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
<Spinner key="loading" />
)}
{chat?.joinRequests?.length === 0 && (
<p className="section-help" key="empty">
<IslandDescription key="empty">
{isChannel ? lang('NoSubscribeRequestsDescription') : lang('NoMemberRequestsDescription')}
</p>
</IslandDescription>
)}
{chat?.joinRequests?.map(({ userId, about, date }) => (
<JoinRequest
@ -104,7 +105,7 @@ const ManageJoinRequests: FC<OwnProps & StateProps> = ({
key={userId}
/>
))}
</div>
</Island>
</div>
<ConfirmDialog
isOpen={isAcceptAllDialogOpen}

View File

@ -17,6 +17,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
import useOldLang from '../../../hooks/useOldLang';
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import FloatingActionButton from '../../ui/FloatingActionButton';
import RadioGroup from '../../ui/RadioGroup';
@ -189,61 +190,65 @@ const ManageReactions: FC<OwnProps & StateProps> = ({
<div className="Management">
<div className="panel-content custom-scroll">
{Boolean(localReactionsLimit && shouldShowReactionsLimit) && (
<div className="section">
<h3 className="section-heading">
<>
<IslandTitle>
{lang('MaximumReactionsHeader')}
</h3>
<RangeSlider
min={1}
max={maxUniqueReactions}
value={localReactionsLimit!}
onChange={handleReactionsLimitChange}
renderValue={renderReactionsMaxCountValue}
isCenteredLayout
/>
<p className="section-info section-info_push">
</IslandTitle>
<Island>
<RangeSlider
min={1}
max={maxUniqueReactions}
value={localReactionsLimit!}
onChange={handleReactionsLimitChange}
renderValue={renderReactionsMaxCountValue}
isCenteredLayout
/>
</Island>
<IslandDescription>
{lang('ChannelReactions.MaxCount.Info')}
</p>
</div>
</IslandDescription>
</>
)}
<div className="section">
<h3 className="section-heading">
{lang('AvailableReactions')}
</h3>
<IslandTitle>
{lang('AvailableReactions')}
</IslandTitle>
<Island>
<RadioGroup
selected={localEnabledReactions?.type || 'none'}
name="reactions"
options={reactionsOptions}
onChange={handleReactionsOptionChange}
/>
<p className="section-info section-info_push">
{localEnabledReactions?.type === 'all' && lang('EnableAllReactionsInfo')}
{localEnabledReactions?.type === 'some' && lang('EnableSomeReactionsInfo')}
{!localEnabledReactions && lang('DisableReactionsInfo')}
</p>
</div>
</Island>
<IslandDescription>
{localEnabledReactions?.type === 'all' && lang('EnableAllReactionsInfo')}
{localEnabledReactions?.type === 'some' && lang('EnableSomeReactionsInfo')}
{!localEnabledReactions && lang('DisableReactionsInfo')}
</IslandDescription>
{localEnabledReactions?.type === 'some' && (
<div className="section section-with-fab">
<h3 className="section-heading">
<>
<IslandTitle>
{lang('OnlyAllowThisReactions')}
</h3>
{availableActiveReactions?.map(({ reaction, title }) => (
<div className="ListItem">
<Checkbox
name={reaction.emoticon}
checked={localEnabledReactions?.allowed.some((r) => isSameReaction(reaction, r))}
label={(
<div className="Reaction">
<ReactionStaticEmoji reaction={reaction} availableReactions={availableReactions} />
{title}
</div>
)}
withIcon
onChange={handleReactionChange}
/>
</div>
))}
</div>
</IslandTitle>
<Island>
{availableActiveReactions?.map(({ reaction, title }) => (
<div key={reaction.emoticon} className="ListItem">
<Checkbox
name={reaction.emoticon}
checked={localEnabledReactions?.allowed.some((r) => isSameReaction(reaction, r))}
label={(
<div className="Reaction">
<ReactionStaticEmoji reaction={reaction} availableReactions={availableReactions} />
{title}
</div>
)}
withIcon
onChange={handleReactionChange}
/>
</div>
))}
</Island>
</>
)}
</div>

View File

@ -28,6 +28,7 @@ import useOldLang from '../../../hooks/useOldLang';
import Avatar from '../../common/Avatar';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
import FloatingActionButton from '../../ui/FloatingActionButton';
@ -227,14 +228,14 @@ const ManageUser: FC<OwnProps & StateProps> = ({
return (
<div className="Management">
<div className="custom-scroll">
<div className="section">
<PrivateChatInfo
userId={user.id}
avatarSize="jumbo"
noStatusOrTyping
noEmojiStatus
withFullInfo
/>
<PrivateChatInfo
userId={user.id}
avatarSize="jumbo"
noStatusOrTyping
noEmojiStatus
withFullInfo
/>
<Island>
<div className="settings-edit">
<InputText
ref={firstNameRef}
@ -262,7 +263,9 @@ const ManageUser: FC<OwnProps & StateProps> = ({
noReplaceNewlines
/>
</div>
<p className="section-edit-info" dir="auto">{lang('EditUserNoteHint')}</p>
</Island>
<IslandDescription dir="auto">{lang('EditUserNoteHint')}</IslandDescription>
<Island>
<div className="ListItem narrow">
<Checkbox
checked={isNotificationsEnabled}
@ -273,40 +276,42 @@ const ManageUser: FC<OwnProps & StateProps> = ({
onChange={handleNotificationChange}
/>
</div>
</div>
</Island>
{canSetPersonalPhoto && (
<div className="section">
<ListItem icon="camera-add" ripple onClick={handleSuggestPhoto}>
<span className="list-item-ellipsis">{oldLang('UserInfo.SuggestPhoto', user.firstName)}</span>
</ListItem>
<ListItem icon="camera-add" ripple onClick={handleSetPersonalPhoto}>
<span className="list-item-ellipsis">{oldLang('UserInfo.SetCustomPhoto', user.firstName)}</span>
</ListItem>
{personalPhoto && (
<ListItem
leftElement={(
<Avatar
photo={notPersonalPhoto}
noPersonalPhoto
peer={user}
size="mini"
className="personal-photo"
/>
)}
ripple
onClick={openResetPersonalPhotoDialog}
>
{oldLang('UserInfo.ResetCustomPhoto')}
<>
<Island>
<ListItem icon="camera-add" ripple onClick={handleSuggestPhoto}>
<span className="list-item-ellipsis">{oldLang('UserInfo.SuggestPhoto', user.firstName)}</span>
</ListItem>
)}
<p className="section-help" dir="auto">{oldLang('UserInfo.CustomPhotoInfo', user.firstName)}</p>
</div>
<ListItem icon="camera-add" ripple onClick={handleSetPersonalPhoto}>
<span className="list-item-ellipsis">{oldLang('UserInfo.SetCustomPhoto', user.firstName)}</span>
</ListItem>
{personalPhoto && (
<ListItem
leftElement={(
<Avatar
photo={notPersonalPhoto}
noPersonalPhoto
peer={user}
size="mini"
className="personal-photo"
/>
)}
ripple
onClick={openResetPersonalPhotoDialog}
>
{oldLang('UserInfo.ResetCustomPhoto')}
</ListItem>
)}
</Island>
<IslandDescription dir="auto">{oldLang('UserInfo.CustomPhotoInfo', user.firstName)}</IslandDescription>
</>
)}
<div className="section">
<Island>
<ListItem icon="delete" ripple destructive onClick={openDeleteDialog}>
{oldLang('DeleteContact')}
</ListItem>
</div>
</Island>
</div>
<FloatingActionButton
isShown={isProfileFieldsTouched}

View File

@ -3,10 +3,17 @@
.Management {
height: 100%;
& > .custom-scroll {
& > .custom-scroll,
& > .panel-content {
overflow-x: hidden;
overflow-y: scroll;
height: 100%;
padding: 1rem;
background-color: var(--color-background-secondary);
@include mixins.adapt-padding-to-scrollbar(1rem);
}
.personal-photo {
@ -14,116 +21,58 @@
margin-right: 2rem;
}
.section {
padding: 1rem 0.5rem;
.section-icon {
margin: 0 auto 2rem;
}
@include mixins.adapt-padding-to-scrollbar(0.5rem);
@include mixins.side-panel-section;
& > .custom-scroll > .ChatInfo,
& > .panel-content > .ChatInfo {
display: flex;
flex-direction: column;
align-items: center;
&.wide {
padding: 1.5rem;
margin-bottom: 1rem;
padding: 1rem 1.5rem;
text-align: center;
}
.ChatInfo .title h3 {
margin-bottom: 0;
}
.ListItem {
.Reaction {
display: flex;
align-items: center;
}
&:first-of-type {
padding-top: 1rem;
.ReactionStaticEmoji {
width: 1.5rem;
margin-right: 1.6875rem;
}
&.no-border {
border-top: none;
}
&.section-with-fab {
padding-bottom: 3.5rem;
}
> .ChatInfo {
margin: 0 0 2rem !important;
.title h3 {
margin-bottom: 0;
&.with-checkbox {
body.is-ios &::after,
body.is-android &::after {
bottom: -1rem;
}
}
.section-icon {
margin: 0 auto 2rem;
}
.ListItem {
.Reaction {
display: flex;
align-items: center;
}
.ReactionStaticEmoji {
width: 1.5rem;
margin-right: 1.6875rem;
}
&.with-checkbox {
body.is-ios &::after,
body.is-android &::after {
bottom: -1rem;
}
}
&.exceptions-member {
.ChatInfo .status {
white-space: pre-wrap;
}
&.exceptions-member {
.ChatInfo .status {
white-space: pre-wrap;
}
}
}
.section-heading {
position: relative;
.invite-link {
padding: 0 1rem;
}
padding-inline-start: 1rem;
font-size: 1rem;
font-weight: var(--font-weight-semibold);
color: var(--color-text-secondary);
&[dir="auto"] {
text-align: initial;
}
}
.section-help {
padding: 0 1rem;
line-height: 1.375rem;
color: var(--color-text-secondary);
&[dir="auto"] {
text-align: initial;
}
}
.section-edit-info,
.section-info {
padding: 0 1rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.section-edit-info {
margin-top: -0.875rem;
}
.invite-link {
padding: 0 1rem;
}
.section-info_push {
margin-top: 0.25rem;
}
.input-admin-title {
margin-top: 0.875rem;
margin-inline: 1rem;
}
&[dir="rtl"] {
text-align: right;
}
.input-admin-title {
margin-top: 0.875rem;
margin-inline: 1rem;
}
textarea.form-control {
@ -183,8 +132,27 @@
}
}
.ManageGroupMembers {
padding: 0.5rem 1rem;
.ManageGroupMembers > .panel-content {
overflow: hidden;
display: flex;
flex-direction: column;
.island-list {
overflow: hidden;
display: flex;
flex: 0 1 auto;
flex-direction: column;
min-height: 0;
padding-block: 0;
.picker-list {
overflow-x: hidden;
overflow-y: auto;
flex-grow: 1;
padding-block: 0.5rem;
}
}
}
.ManageInvites {
@ -311,6 +279,9 @@
width: 100%;
height: 0;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: height 0.25s ease-in-out;
&::before {
content: "";
@ -333,27 +304,28 @@
}
.without-bottom-shadow {
padding-bottom: 0;
--before-shift-height: 48px;
position: relative;
overflow: hidden;
padding: 0.5rem 0.5rem 0;
box-shadow: none;
&.dropdown-open .DropdownListTrap {
height: var(--shift-height);
}
}
.part {
margin: 0 -1.5rem;
padding: 0 1.5rem 1rem;
@include mixins.side-panel-section;
padding-bottom: 0.5rem;
}
.section, .part {
position: relative;
transition: 0.25s ease-in-out transform;
&.shifted {
transform: translateY(var(--shift-height));
}
}
}
.settings-edit {
padding: 0 1rem !important;
padding: 1.125rem !important;
padding-bottom: 0 !important;
}

View File

@ -3,6 +3,8 @@
.root {
overflow-x: hidden;
overflow-y: scroll;
padding: 1rem;
background-color: var(--color-background-secondary);
}
.noResults {
@ -19,9 +21,6 @@
.section {
padding: 1.5rem;
padding-inline-end: calc(1.5rem - var(--scrollbar-width));
@include mixins.side-panel-section;
}
.user :global(.status) {
@ -113,10 +112,6 @@
flex-direction: column-reverse;
}
.giveawayButton {
margin: 0 -1rem 0.5rem;
}
.giveawayIcon {
width: 2.75rem;
height: 2.75rem;
@ -134,7 +129,3 @@
right: 0.5rem;
transform: translate(0, -50%);
}
.boostInfo {
margin: 0 -1rem;
}

View File

@ -22,6 +22,7 @@ import Icon from '../../common/icons/Icon';
import LinkField from '../../common/LinkField';
import PremiumProgress from '../../common/PremiumProgress';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import Island, { IslandDescription, IslandTitle } from '../../gili/layout/Island';
import ListItem from '../../ui/ListItem';
import Loading from '../../ui/Loading';
import Spinner from '../../ui/Spinner';
@ -222,7 +223,7 @@ const BoostStatistics = ({
return (
<ListItem
className={buildClassName(styles.boostInfo, 'chat-item-clickable')}
className="chat-item-clickable"
onClick={() => handleBoosterClick(boost.userId)}
>
<PrivateChatInfo
@ -254,6 +255,25 @@ const BoostStatistics = ({
openGiveawayModal({ chatId, prepaidGiveaway });
});
function renderLoadMore() {
if (!boostersToLoadCount) return undefined;
return (
<ListItem
key="load-more"
className={styles.showMore}
disabled={boostStatistics?.isLoadingBoosters}
onClick={handleLoadMore}
>
{boostStatistics?.isLoadingBoosters ? (
<Spinner className={styles.loadMoreSpinner} />
) : (
<Icon name="down" className={styles.down} />
)}
{lang('ShowVotes', boostersToLoadCount, 'i')}
</ListItem>
);
}
function renderContent() {
let listToRender;
if (tabType === 'boostList') {
@ -267,9 +287,10 @@ const BoostStatistics = ({
}
return (
<div className={styles.section}>
<Island>
{listToRender?.map((boost) => renderBoostList(boost))}
</div>
{renderLoadMore()}
</Island>
);
}
@ -278,7 +299,7 @@ const BoostStatistics = ({
{!isLoaded && <Loading />}
{isLoaded && statsOverview && (
<>
<div className={styles.section}>
<Island>
<PremiumProgress
leftText={lang('BoostsLevel', currentLevel)}
rightText={hasNextLevel ? lang('BoostsLevel', currentLevel + 1) : undefined}
@ -287,73 +308,75 @@ const BoostStatistics = ({
floatingBadgeIcon="boost"
/>
<StatisticsOverview className={styles.stats} statistics={statsOverview} type="boost" />
</div>
</Island>
{statsOverview.prepaidGiveaways && (
<div className={styles.section}>
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('BoostingPreparedGiveaways')}
</h4>
{statsOverview?.prepaidGiveaways?.map((prepaidGiveaway) => {
const isStarsGiveaway = 'stars' in prepaidGiveaway;
</IslandTitle>
<Island>
{statsOverview?.prepaidGiveaways?.map((prepaidGiveaway) => {
const isStarsGiveaway = 'stars' in prepaidGiveaway;
return (
<ListItem
key={prepaidGiveaway.id}
className="chat-item-clickable"
return (
<ListItem
key={prepaidGiveaway.id}
className="chat-item-clickable"
onClick={() => launchPrepaidGiveawayHandler(prepaidGiveaway)}
>
<div className={buildClassName(styles.status, 'status-clickable')}>
<div>
{isStarsGiveaway
? (
<img
src={GiftStar}
className={styles.giveawayIcon}
alt={lang('GiftStar')}
/>
) : (
<img
src={GIVEAWAY_IMG_LIST[prepaidGiveaway.months] || GIVEAWAY_IMG_LIST[3]}
className={styles.giveawayIcon}
alt={lang('Giveaway')}
/>
)}
</div>
<div className={styles.info}>
<h3>
onClick={() => launchPrepaidGiveawayHandler(prepaidGiveaway)}
>
<div className={buildClassName(styles.status, 'status-clickable')}>
<div>
{isStarsGiveaway
? lang('Giveaway.Stars.Prepaid.Title', prepaidGiveaway.stars)
: lang('BoostingTelegramPremiumCountPlural', prepaidGiveaway.quantity)}
</h3>
<p className={styles.month}>
{
isStarsGiveaway ? lang('Giveaway.Stars.Prepaid.Desc', prepaidGiveaway.quantity)
: lang('PrepaidGiveawayMonths', prepaidGiveaway.months)
}
</p>
</div>
<div className={styles.quantity}>
<div className={buildClassName(styles.floatingBadge,
styles.floatingBadgeButtonColor,
styles.floatingBadgeButton)}
>
<Icon name="boost" className={styles.floatingBadgeIcon} />
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
{isStarsGiveaway ? prepaidGiveaway.boosts
: prepaidGiveaway.quantity * (giveawayBoostsPerPremium ?? GIVEAWAY_BOOST_PER_PREMIUM)}
? (
<img
src={GiftStar}
className={styles.giveawayIcon}
alt={lang('GiftStar')}
/>
) : (
<img
src={GIVEAWAY_IMG_LIST[prepaidGiveaway.months] || GIVEAWAY_IMG_LIST[3]}
className={styles.giveawayIcon}
alt={lang('Giveaway')}
/>
)}
</div>
<div className={styles.info}>
<h3>
{isStarsGiveaway
? lang('Giveaway.Stars.Prepaid.Title', prepaidGiveaway.stars)
: lang('BoostingTelegramPremiumCountPlural', prepaidGiveaway.quantity)}
</h3>
<p className={styles.month}>
{
isStarsGiveaway ? lang('Giveaway.Stars.Prepaid.Desc', prepaidGiveaway.quantity)
: lang('PrepaidGiveawayMonths', prepaidGiveaway.months)
}
</p>
</div>
<div className={styles.quantity}>
<div className={buildClassName(styles.floatingBadge,
styles.floatingBadgeButtonColor,
styles.floatingBadgeButton)}
>
<Icon name="boost" className={styles.floatingBadgeIcon} />
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
{isStarsGiveaway ? prepaidGiveaway.boosts
: prepaidGiveaway.quantity * (giveawayBoostsPerPremium ?? GIVEAWAY_BOOST_PER_PREMIUM)}
</div>
</div>
</div>
</div>
</div>
</ListItem>
);
})}
<p className="text-muted hint" key="links-hint">{lang('BoostingSelectPaidGiveaway')}</p>
</div>
</ListItem>
);
})}
</Island>
<IslandDescription>{lang('BoostingSelectPaidGiveaway')}</IslandDescription>
</>
)}
<div className={styles.section}>
{shouldDisplayGiftList ? (
{shouldDisplayGiftList ? (
<Island>
<div
className={buildClassName(styles.boostSection, styles.content)}
>
@ -368,52 +391,45 @@ const BoostStatistics = ({
</Transition>
<SquareTabList activeTab={renderingActiveTab} tabs={tabs} onSwitchTab={setActiveTab} />
</div>
) : (
<>
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
{lang('BoostingBoostsCount', boostStatistics?.boosts?.count)}
</h4>
{!boostStatistics?.boosts?.list?.length && (
<div className={styles.noResults}>
{lang(isChannel ? 'NoBoostersHint' : 'NoBoostersGroupHint')}
</div>
)}
{boostStatistics?.boosts?.list?.map((boost) => renderBoostList(boost))}
</>
)}
{Boolean(boostersToLoadCount) && (
<ListItem
key="load-more"
className={styles.showMore}
disabled={boostStatistics?.isLoadingBoosters}
onClick={handleLoadMore}
>
{boostStatistics?.isLoadingBoosters ? (
<Spinner className={styles.loadMoreSpinner} />
) : (
<Icon name="down" className={styles.down} />
)}
{lang('ShowVotes', boostersToLoadCount, 'i')}
</ListItem>
)}
</div>
<LinkField className={styles.section} link={status!.boostUrl} withShare title={lang('LinkForBoosting')} />
</Island>
) : (
<>
<IslandTitle dir={lang.isRtl ? 'rtl' : undefined}>
{lang('BoostingBoostsCount', boostStatistics?.boosts?.count)}
</IslandTitle>
{!boostStatistics?.boosts?.list?.length ? (
<IslandDescription>
{lang(isChannel ? 'NoBoostersHint' : 'NoBoostersGroupHint')}
</IslandDescription>
) : (
<Island>
{boostStatistics?.boosts?.list?.map((boost) => renderBoostList(boost))}
{renderLoadMore()}
</Island>
)}
</>
)}
<IslandTitle>{lang('LinkForBoosting')}</IslandTitle>
<Island>
<LinkField link={status!.boostUrl} withShare noTitle />
</Island>
{isGiveawayAvailable && (
<div className={styles.section}>
<ListItem
key="load-more"
icon="gift"
onClick={handleGiveawayClick}
className={styles.giveawayButton}
>
{lang('BoostingGetBoostsViaGifts')}
</ListItem>
<p className="text-muted hint" key="links-hint">
<>
<Island>
<ListItem
key="load-more"
icon="gift"
onClick={handleGiveawayClick}
>
{lang('BoostingGetBoostsViaGifts')}
</ListItem>
</Island>
<IslandDescription>
{lang(
isChannel ? 'BoostingGetMoreBoosts' : 'BoostingGetMoreBoostsGroup',
)}
</p>
</div>
</IslandDescription>
</>
)}
</>
)}

View File

@ -15,9 +15,11 @@ import { callApi } from '../../../api/gramjs';
import { isGraph } from './helpers/isGraph';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import Island, { IslandTitle } from '../../gili/layout/Island';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Loading from '../../ui/Loading';
import StatisticsMessagePublicForward from './StatisticsMessagePublicForward';
@ -62,7 +64,8 @@ function MessageStatistics({
dcId,
messageId,
}: OwnProps & StateProps) {
const lang = useOldLang();
const lang = useLang();
const oldLang = useOldLang();
const containerRef = useRef<HTMLDivElement>();
const [isReady, setIsReady] = useState(false);
const loadedChartsRef = useRef<Set<string>>(new Set());
@ -141,10 +144,10 @@ function MessageStatistics({
LovelyChart.create(
containerRef.current!.children[index] as HTMLElement,
{
title: lang((GRAPH_TITLES as Record<string, string>)[name]),
title: oldLang((GRAPH_TITLES as Record<string, string>)[name]),
...zoomToken ? {
onZoom: (x: number) => callApi('fetchStatisticsAsyncGraph', { token: zoomToken, x, dcId }),
zoomOutLabel: lang('Graph.ZoomOut'),
zoomOutLabel: oldLang('Graph.ZoomOut'),
} : {},
...graph,
},
@ -156,7 +159,7 @@ function MessageStatistics({
forceUpdate();
})();
}, [
isReady, statistics, lang, chatId, messageId, loadStatisticsAsyncGraph, dcId, forceUpdate,
isReady, statistics, oldLang, chatId, messageId, loadStatisticsAsyncGraph, dcId, forceUpdate,
]);
const handleLoadMore = useLastCallback(({ direction }: { direction: LoadMoreDirection }) => {
@ -174,11 +177,14 @@ function MessageStatistics({
key={`${chatId}-${messageId}`}
className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}
>
<StatisticsOverview statistics={statistics} type="message" title={lang('StatisticOverview')} />
<IslandTitle>{lang('StatisticOverview')}</IslandTitle>
<Island>
<StatisticsOverview statistics={statistics} type="message" />
</Island>
{(!loadedChartsRef.current.size || !statistics.publicForwardsData) && <Loading />}
<div ref={containerRef} data-stricterdom-ignore>
<div ref={containerRef} className={styles.graphContainer} data-stricterdom-ignore>
{GRAPHS.map((graph) => {
const isGraphReady = loadedChartsRef.current.has(graph) && !errorChartsRef.current.has(graph);
return (
@ -189,18 +195,19 @@ function MessageStatistics({
{Boolean(statistics.publicForwards) && (
<div className={styles.publicForwards}>
<h2 className={styles.publicForwardsTitle}>{lang('Stats.Message.PublicShares')}</h2>
<InfiniteScroll
items={statistics.publicForwardsData}
itemSelector=".statistic-public-forward"
onLoadMore={handleLoadMore}
noFastList
>
{(statistics.publicForwardsData as ApiMessagePublicForward[]).map((item) => (
<StatisticsMessagePublicForward key={item.messageId} data={item} />
))}
</InfiniteScroll>
<IslandTitle>{lang('StatsMessagePublicShares')}</IslandTitle>
<Island>
<InfiniteScroll
items={statistics.publicForwardsData}
itemSelector=".statistic-public-forward"
onLoadMore={handleLoadMore}
noFastList
>
{(statistics.publicForwardsData as ApiMessagePublicForward[]).map((item) => (
<StatisticsMessagePublicForward key={item.messageId} data={item} />
))}
</InfiniteScroll>
</Island>
</div>
)}
</div>

View File

@ -4,17 +4,16 @@
overflow-x: hidden;
overflow-y: hidden;
height: 100%;
padding: 1rem;
}
.graph {
margin-bottom: 1rem;
border-bottom: 0.0625rem solid var(--color-borders);
opacity: 1;
transition: opacity 0.3s ease;
&:last-of-type {
margin-bottom: 0;
border-bottom: none;
}
&.hidden {
@ -23,21 +22,25 @@
}
}
.charts {
display: flex;
flex-direction: column;
}
.ready {
overflow-y: scroll !important;
}
.section {
.balanceSection {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem 0.75rem;
border-bottom: 0.0625rem solid var(--color-borders);
padding: 1rem;
}
.topText {
display: block;
.balanceTitle {
font-size: 0.9375rem;
font-weight: var(--font-weight-semibold);
}
.availableReward {

View File

@ -20,6 +20,7 @@ import useOldLang from '../../../hooks/useOldLang';
import AboutMonetizationModal from '../../common/AboutMonetizationModal.async';
import Icon from '../../common/icons/Icon';
import SafeLink from '../../common/SafeLink';
import Island, { IslandDescription } from '../../gili/layout/Island';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import Link from '../../ui/Link';
@ -230,29 +231,31 @@ const MonetizationStatistics = ({
}
return (
<div className={buildClassName(styles.root, 'custom-scroll', isReady && styles.ready)}>
<div className={buildClassName(styles.section, styles.topText)}>{topText}</div>
<div className={buildClassName(styles.root, 'panel-content custom-scroll', isReady && styles.ready)}>
<IslandDescription>{topText}</IslandDescription>
<StatisticsOverview
statistics={statistics}
isToncoin
type="monetization"
title={oldLang('MonetizationOverview')}
subtitle={
<div className={styles.textBottom}>{oldLang('MonetizationProceedsTONInfo')}</div>
}
/>
<Island>
<StatisticsOverview
statistics={statistics}
isToncoin
type="monetization"
title={oldLang('MonetizationOverview')}
/>
</Island>
<IslandDescription>{oldLang('MonetizationProceedsTONInfo')}</IslandDescription>
{!loadedChartsRef.current.size && <Loading />}
<div ref={containerRef} className={styles.section} data-stricterdom-ignore>
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => (
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
))}
</div>
<Island>
<div ref={containerRef} data-stricterdom-ignore className={styles.charts}>
{MONETIZATION_GRAPHS.filter(Boolean).map((graph) => (
<div key={graph} className={buildClassName(styles.graph, styles.hidden)} />
))}
</div>
</Island>
<div className={styles.section}>
{oldLang('lng_channel_earn_balance_title')}
<Island className={styles.balanceSection}>
<h4 className={styles.balanceTitle}>{oldLang('lng_channel_earn_balance_title')}</h4>
{renderAvailableReward()}
@ -264,8 +267,8 @@ const MonetizationStatistics = ({
{oldLang('MonetizationWithdraw')}
</Button>
<div className={styles.textBottom}>{rewardsText}</div>
</div>
</Island>
<IslandDescription>{rewardsText}</IslandDescription>
<AboutMonetizationModal
isOpen={isAboutMonetizationModalOpen}

View File

@ -3,9 +3,9 @@
overflow-y: hidden;
height: 100%;
border-top: 1px solid transparent;
padding: 1rem;
transition: border-top-color 0.2s ease-in-out;
background-color: var(--color-background-secondary);
:global(.lovely-chart--container) {
font: inherit !important;
@ -13,7 +13,7 @@
}
:global(.lovely-chart--header) {
margin: 0 0.75rem;
margin: 0.75rem;
}
:global(.lovely-chart--header),
@ -35,7 +35,8 @@
}
.messages, .publicForwards {
padding: 1rem 0;
margin-top: 1rem;
padding: 0;
&-title {
padding-left: 0.75rem;
@ -55,20 +56,25 @@
overflow-y: scroll !important;
}
.graphContainer {
margin-top: 1rem;
}
.graph {
margin-bottom: 1rem;
border-bottom: 1px solid var(--color-borders);
border-radius: var(--border-radius-island);
opacity: 1;
background-color: var(--color-background);
box-shadow: 0 1px 4px 0 #0000000D;
transition: opacity 0.3s ease;
&:last-of-type {
margin-bottom: 0;
border-bottom: none;
& + & {
margin-top: 1rem;
}
&.hidden {
margin: 0;
border-bottom: none;
opacity: 0;
}
}

View File

@ -26,6 +26,7 @@ import { isGraph } from './helpers/isGraph';
import useForceUpdate from '../../../hooks/useForceUpdate';
import useOldLang from '../../../hooks/useOldLang';
import Island from '../../gili/layout/Island';
import Loading from '../../ui/Loading';
import StatisticsOverview from './StatisticsOverview';
import StatisticsRecentMessage from './StatisticsRecentMessage';
@ -200,16 +201,18 @@ const Statistics = ({
return (
<div className={buildClassName(styles.root, 'panel-content custom-scroll', isReady && styles.ready)}>
{statistics && (
<StatisticsOverview
statistics={statistics}
type={isGroup ? 'group' : 'channel'}
title={lang('StatisticOverview')}
/>
<Island>
<StatisticsOverview
statistics={statistics}
type={isGroup ? 'group' : 'channel'}
title={lang('StatisticOverview')}
/>
</Island>
)}
{!loadedChartsRef.current.size && <Loading />}
<div ref={containerRef} data-stricterdom-ignore>
<div ref={containerRef} data-stricterdom-ignore className={styles.graphContainer}>
{graphs.map((graph) => {
const isGraphReady = loadedChartsRef.current.has(graph) && !errorChartsRef.current.has(graph);
return (
@ -219,7 +222,7 @@ const Statistics = ({
</div>
{Boolean((statistics as ApiChannelStatistics)?.recentPosts?.length) && (
<div className={styles.messages}>
<Island className={styles.messages}>
<h2 className={styles.messagesTitle}>{lang('ChannelStats.Recent.Header')}</h2>
{(statistics as ApiChannelStatistics).recentPosts.map((postStatistic) => {
@ -251,7 +254,7 @@ const Statistics = ({
return undefined;
})}
</div>
</Island>
)}
</div>
);

View File

@ -1,7 +1,5 @@
.root {
margin-bottom: 1rem;
padding: 1rem 1.5rem;
border-bottom: 0.0625rem solid var(--color-borders);
padding: 0.5rem;
}
.header {
@ -41,12 +39,12 @@
}
.tableHeading {
font-size: 0.9375rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
.tableValue {
font-size: 1.25rem;
font-size: 1rem;
font-weight: var(--font-weight-medium);
}

View File

@ -26,6 +26,7 @@ type OwnProps = {
title?: string;
autoFocus?: boolean;
teactExperimentControlled?: boolean;
noMargin?: boolean;
inputMode?: 'text' | 'none' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search';
onChange?: (e: ChangeEvent<HTMLInputElement>) => void;
onInput?: (e: FormEvent<HTMLInputElement>) => void;
@ -54,6 +55,7 @@ const InputText = ({
title,
autoFocus,
teactExperimentControlled,
noMargin,
onChange,
onInput,
onKeyPress,
@ -71,6 +73,7 @@ const InputText = ({
disabled && 'disabled',
readOnly && 'disabled',
labelText && 'with-label',
noMargin && 'no-margin',
className,
);

View File

@ -163,11 +163,11 @@
}
&-slideFadeAndroid {
--background-color: var(--color-background);
--slide-background-color: var(--color-background);
> .Transition_slide {
z-index: 0;
background: var(--background-color);
background: var(--slide-background-color);
}
> .Transition_slide-to {
@ -179,11 +179,11 @@
}
&-slideFadeAndroidBackwards {
--background-color: var(--color-background);
--slide-background-color: var(--color-background);
> .Transition_slide {
z-index: 0;
background: var(--background-color);
background: var(--slide-background-color);
}
> .Transition_slide-from {
@ -277,12 +277,12 @@
}
&-slideLayers {
--background-color: var(--color-background);
--slide-background-color: var(--color-background);
background: black !important;
> .Transition_slide {
background: var(--background-color);
background: var(--slide-background-color);
}
> .Transition_slide-to {
@ -296,12 +296,12 @@
}
&-slideLayersBackwards {
--background-color: var(--color-background);
--slide-background-color: var(--color-background);
background: black !important;
> .Transition_slide {
background: var(--background-color);
background: var(--slide-background-color);
}
> .Transition_slide-to {
@ -317,8 +317,10 @@
}
&-pushSlide {
--slide-background-color: var(--color-background);
> .Transition_slide {
background: var(--color-background);
background: var(--slide-background-color);
}
> .Transition_slide-from {
@ -343,8 +345,10 @@
}
&-pushSlideBackwards {
--slide-background-color: var(--color-background);
> .Transition_slide {
background: var(--color-background);
background: var(--slide-background-color);
}
> .Transition_slide-to {

View File

@ -13,11 +13,13 @@ const useScrollNotch = ({
selector,
isBottomNotch,
shouldHideTopNotch,
onScrolled,
}: {
containerRef: ElementRef<HTMLDivElement>;
selector: string;
isBottomNotch?: boolean;
shouldHideTopNotch?: boolean;
onScrolled?: (isScrolled: boolean) => void;
}, deps: unknown[]) => {
useLayoutEffect(() => {
const elements = containerRef.current?.querySelectorAll<HTMLElement>(selector);
@ -29,6 +31,8 @@ const useScrollNotch = ({
const { scrollHeight, scrollTop, clientHeight } = target;
const isAtEnd = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
onScrolled?.(isScrolled);
requestMutation(() => {
if (!shouldHideTopNotch) {
toggleExtraClass(target, 'scrolled', isScrolled);
@ -63,13 +67,18 @@ const useScrollNotch = ({
useEffect(() => {
const elements = containerRef.current?.querySelectorAll<HTMLElement>(selector);
if (!elements?.length) return undefined;
if (!elements?.length) {
onScrolled?.(false);
return undefined;
}
elements.forEach((el) => {
const isScrolled = el.scrollTop > 0;
const { scrollHeight, scrollTop, clientHeight } = el;
const isAtEnd = scrollHeight - scrollTop - clientHeight < SCROLL_THRESHOLD;
onScrolled?.(isScrolled);
requestMutation(() => {
if (!shouldHideTopNotch) {
toggleExtraClass(el, 'scrolled', isScrolled);

Some files were not shown because too many files have changed in this diff Show More