Settings Tab: Better layout for narrow windows

This commit is contained in:
Scott Nonnenberg
2025-06-07 08:16:40 +10:00
committed by GitHub
parent 9e3f397032
commit 974c29fd41
8 changed files with 438 additions and 163 deletions

View File

@@ -141,12 +141,17 @@ $NavTabs__ProfileAvatar__size: 28px;
.NavTabs__ItemUpdateBadge {
background: variables.$color-ultramarine;
border-radius: 100%;
border: 1px solid variables.$color-white;
height: 8px;
width: 8px;
position: absolute;
top: 0;
inset-inline-end: 0;
@include mixins.light-theme {
border: 1px solid variables.$color-white;
}
@include mixins.dark-theme {
border: 1px solid variables.$color-gray-80;
}
}
.NavTabs__ItemIcon {

View File

@@ -286,6 +286,7 @@ $secondary-text-color: light-dark(
height: 100%;
flex-direction: row;
overflow-y: scroll;
container-type: inline-size;
}
&__settings-pane {
@@ -374,6 +375,73 @@ $secondary-text-color: light-dark(
}
}
&__light-icon-label {
display: flex;
}
&__flow-control {
display: block;
padding-block: 4px;
padding-inline: 24px;
}
&__one-third-flow {
vertical-align: middle;
display: inline-block;
width: 33%;
@container (max-width: 350px) {
width: 100%;
}
}
&__half-flow {
vertical-align: middle;
display: inline-block;
width: 50%;
@container (max-width: 350px) {
width: 100%;
}
}
&__two-thirds-flow {
vertical-align: middle;
display: inline-block;
width: 66%;
@container (max-width: 350px) {
width: 100%;
}
}
&__half-flow--align-right {
text-align: end;
@container (max-width: 350px) {
text-align: start;
}
}
&__one-third-flow--align-right {
text-align: end;
@container (max-width: 350px) {
text-align: start;
}
}
&__full-flow {
vertical-align: middle;
display: inline-block;
width: 100%;
}
&__flow-value,
&__flow-description {
vertical-align: middle;
color: $secondary-text-color;
}
&__device-name-description {
padding-top: 8px;
}
&__flow-button {
padding-inline-start: 5px;
@container (max-width: 350px) {
padding-inline-start: 0px;
padding-top: 8px;
}
}
&__control {
align-items: center;
display: flex;
@@ -432,9 +500,16 @@ $secondary-text-color: light-dark(
}
}
&__checkbox__description,
&__description {
@include mixins.font-subtitle;
color: $secondary-text-color;
// For specificity reasons, we can't use $secondary-text-color. We need the mixins.
@include mixins.light-theme {
color: variables.$color-gray-60;
}
@include mixins.dark-theme {
color: variables.$color-gray-25;
}
&--error {
color: variables.$color-accent-red !important;
}

View File

@@ -0,0 +1,56 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import { action } from '@storybook/addon-actions';
import type { Meta } from '@storybook/react';
import type { NavTabsProps } from './NavTabs';
import { NavTabs } from './NavTabs';
import { NavTab } from '../state/ducks/nav';
import { getDefaultConversation } from '../test-both/helpers/getDefaultConversation';
import { ThemeType } from '../types/Util';
const { i18n } = window.SignalContext;
const createProps = (
overrideProps: Partial<NavTabsProps> = {}
): NavTabsProps => ({
badge: overrideProps.badge,
hasFailedStorySends: Boolean(overrideProps.hasFailedStorySends),
hasPendingUpdate: Boolean(overrideProps.hasPendingUpdate),
i18n,
me: getDefaultConversation(),
navTabsCollapsed: Boolean(overrideProps.navTabsCollapsed),
onChangeLocation: action('onChangeLocation'),
onToggleNavTabsCollapse: action('onToggleNavTabsCollapse'),
renderCallsTab: () => <div>Calls Tab goes here</div>,
renderChatsTab: () => <div>Chats Tab goes here</div>,
renderStoriesTab: () => <div>Stories Tab goes here</div>,
renderSettingsTab: () => <div>Settings Tab goes here</div>,
selectedNavTab: overrideProps.selectedNavTab ?? NavTab.Chats,
shouldShowProfileIcon: Boolean(overrideProps.shouldShowProfileIcon),
storiesEnabled: Boolean(overrideProps.storiesEnabled),
theme: overrideProps.theme ?? ThemeType.light,
unreadCallsCount: overrideProps.unreadCallsCount ?? 0,
unreadConversationsStats: overrideProps.unreadConversationsStats ?? {
unreadCount: 0,
unreadMentionsCount: 0,
markedUnread: false,
},
unreadStoriesCount: overrideProps.unreadStoriesCount ?? 0,
});
export default {
title: 'Components/NavTabs',
} satisfies Meta<NavTabsProps>;
export function HasPendingUpdate(): JSX.Element {
return (
<NavTabs
{...createProps({
hasPendingUpdate: true,
})}
/>
);
}

View File

@@ -43,6 +43,7 @@ import { FunSkinTonesList } from './fun/FunSkinTones';
import { emojiParentKeyConstant, type EmojiSkinTone } from './fun/data/emojis';
import {
SettingsControl as Control,
FlowingSettingsControl as FlowingControl,
SettingsRadio,
SettingsRow,
} from './PreferencesUtil';
@@ -672,27 +673,43 @@ export function Preferences({
const pageContents = (
<>
<SettingsRow>
<Control
left={i18n('icu:Preferences--phone-number')}
right={phoneNumber}
rightStyle={{
maxWidth: '33%',
}}
/>
<Control
left={
<>
<div>{i18n('icu:Preferences--device-name')}</div>
<div className="Preferences__description">
{i18n('icu:Preferences--device-name__description')}
</div>
</>
}
right={deviceName}
rightStyle={{
maxWidth: '33%',
}}
/>
<FlowingControl>
<div className="Preferences__half-flow">
{i18n('icu:Preferences--phone-number')}
</div>
<div
className={classNames(
'Preferences__flow-value',
'Preferences__half-flow',
'Preferences__half-flow--align-right'
)}
>
{phoneNumber}
</div>
</FlowingControl>
<FlowingControl>
<div className="Preferences__half-flow">
{i18n('icu:Preferences--device-name')}
</div>
<div
className={classNames(
'Preferences__flow-value',
'Preferences__half-flow',
'Preferences__half-flow--align-right'
)}
>
{deviceName}
</div>
<div
className={classNames(
'Preferences__device-name-description',
'Preferences__description',
'Preferences__full-flow'
)}
>
{i18n('icu:Preferences--device-name__description')}
</div>
</FlowingControl>
</SettingsRow>
<SettingsRow title={i18n('icu:Preferences--system')}>
{isAutoLaunchSupported && (
@@ -1374,24 +1391,34 @@ export function Preferences({
const pageContents = (
<>
<SettingsRow>
<Control
left={
<div className="Preferences__pnp">
<h3>{i18n('icu:Preferences__pnp__row--title')}</h3>
<div className="Preferences__description">
{i18n('icu:Preferences__pnp__row--body')}
</div>
<FlowingControl>
<div
className={classNames(
'Preferences__pnp',
'Preferences__two-thirds-flow'
)}
>
<h3>{i18n('icu:Preferences__pnp__row--title')}</h3>
<div className="Preferences__description">
{i18n('icu:Preferences__pnp__row--body')}
</div>
}
right={
</div>
<div
className={classNames(
'Preferences__pnp',
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={() => setPage(Page.PNP)}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__pnp__row--button')}
</Button>
}
/>
</div>
</FlowingControl>
</SettingsRow>
<SettingsRow>
<Control
@@ -1433,18 +1460,22 @@ export function Preferences({
/>
)}
<SettingsRow title={i18n('icu:disappearingMessages')}>
<Control
left={
<>
<div>
{i18n('icu:settings__DisappearingMessages__timer__label')}
</div>
<div className="Preferences__description">
{i18n('icu:settings__DisappearingMessages__footer')}
</div>
</>
}
right={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<div>
{i18n('icu:settings__DisappearingMessages__timer__label')}
</div>
<div className="Preferences__description">
{i18n('icu:settings__DisappearingMessages__footer')}
</div>
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Select
ariaLabel={i18n(
'icu:settings__DisappearingMessages__timer__label'
@@ -1480,8 +1511,8 @@ export function Preferences({
])}
value={universalExpireTimer}
/>
}
/>
</div>
</FlowingControl>
</SettingsRow>
{isContentProtectionSupported && (
<SettingsRow title={i18n('icu:Preferences__Privacy__Application')}>
@@ -1520,17 +1551,23 @@ export function Preferences({
</ConfirmationDialog>
) : null}
<SettingsRow title={i18n('icu:Stories__title')}>
<Control
left={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<label htmlFor={storiesId}>
<div>{i18n('icu:Stories__settings-toggle--title')}</div>
<div className="Preferences__description">
{i18n('icu:Stories__settings-toggle--description')}
</div>
</label>
}
right={
hasStoriesDisabled ? (
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
{hasStoriesDisabled ? (
<Button
onClick={() => onHasStoriesDisabledChanged(false)}
variant={ButtonVariant.Secondary}
@@ -1545,31 +1582,40 @@ export function Preferences({
>
{i18n('icu:Preferences__turn-stories-off')}
</Button>
)
}
/>
)}
</div>
</FlowingControl>
</SettingsRow>
<SettingsRow>
<Control
left={
<>
<div>{i18n('icu:clearDataHeader')}</div>
<div className="Preferences__description">
{i18n('icu:clearDataExplanation')}
</div>
</>
}
right={
<div className="Preferences__right-button">
<Button
onClick={() => setConfirmDelete(true)}
variant={ButtonVariant.SecondaryDestructive}
>
{i18n('icu:clearDataButton')}
</Button>
<FlowingControl>
<div
className={classNames(
'Preferences__pnp',
'Preferences__two-thirds-flow'
)}
>
<div>{i18n('icu:clearDataHeader')}</div>
<div className="Preferences__description">
{i18n('icu:clearDataExplanation')}
</div>
}
/>
</div>
<div
className={classNames(
'Preferences__pnp',
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={() => setConfirmDelete(true)}
variant={ButtonVariant.SecondaryDestructive}
>
{i18n('icu:clearDataButton')}
</Button>
</div>
</FlowingControl>
</SettingsRow>
{confirmDelete ? (
<ConfirmationDialog
@@ -1681,23 +1727,28 @@ export function Preferences({
</div>
</SettingsRow>
<SettingsRow>
<Control
left={
<>
<div className="Preferences__option-name">
{i18n('icu:Preferences__sent-media-quality')}
</div>
<div
className={classNames(
'Preferences__description',
'Preferences__description--medium'
)}
>
{i18n('icu:Preferences__sent-media-quality__description')}
</div>
</>
}
right={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<div className="Preferences__option-name">
{i18n('icu:Preferences__sent-media-quality')}
</div>
<div
className={classNames(
'Preferences__description',
'Preferences__description--medium'
)}
>
{i18n('icu:Preferences__sent-media-quality__description')}
</div>
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Select
onChange={onSentMediaQualityChange}
options={[
@@ -1712,8 +1763,8 @@ export function Preferences({
]}
value={sentMediaQualitySetting}
/>
}
/>
</div>
</FlowingControl>
</SettingsRow>
</>
);

View File

@@ -2,13 +2,20 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React from 'react';
import classNames from 'classnames';
import type {
BackupsSubscriptionType,
BackupStatusType,
} from '../types/backups';
import type { LocalizerType } from '../types/I18N';
import { formatTimestamp } from '../util/formatTimestamp';
import { SettingsControl as Control, SettingsRow } from './PreferencesUtil';
import {
SettingsControl as Control,
FlowingSettingsControl as FlowingControl,
LightIconLabel,
SettingsRow,
} from './PreferencesUtil';
import { missingCaseError } from '../util/missingCaseError';
import { Button, ButtonVariant } from './Button';
import type { PreferencesBackupPage } from '../types/PreferencesBackupPage';
@@ -97,29 +104,36 @@ export function PreferencesBackups({
{backupSubscriptionStatus ? (
<SettingsRow className="Preferences--BackupsRow">
<Control
icon="Preferences__BackupsIcon"
left={
<label>
{i18n('icu:Preferences--signal-backups')}{' '}
<div className="Preferences__description">
{renderBackupsSubscriptionSummary({
subscriptionStatus: backupSubscriptionStatus,
i18n,
locale,
})}
</div>
</label>
}
right={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<LightIconLabel icon="Preferences__BackupsIcon">
<label>
{i18n('icu:Preferences--signal-backups')}{' '}
<div className="Preferences__description">
{renderBackupsSubscriptionSummary({
subscriptionStatus: backupSubscriptionStatus,
i18n,
locale,
})}
</div>
</label>
</LightIconLabel>
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={() => setPage(Page.BackupsDetails)}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__button--manage')}
</Button>
}
/>
</div>
</FlowingControl>
</SettingsRow>
) : (
<SettingsRow className="Preferences--BackupsRow">
@@ -148,19 +162,26 @@ export function PreferencesBackups({
className="Preferences--BackupsRow"
title={i18n('icu:Preferences__backup-other-ways')}
>
<Control
icon="Preferences__LocalBackupsIcon"
left={
<label>
{i18n('icu:Preferences__local-backups')}{' '}
<div className="Preferences__description">
{isLocalBackupsSetup
? null
: i18n('icu:Preferences--local-backups-off-description')}
</div>
</label>
}
right={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<LightIconLabel icon="Preferences__LocalBackupsIcon">
<label>
{i18n('icu:Preferences__local-backups')}{' '}
<div className="Preferences__description">
{isLocalBackupsSetup
? null
: i18n('icu:Preferences--local-backups-off-description')}
</div>
</label>
</LightIconLabel>
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={() => setPage(Page.LocalBackups)}
variant={ButtonVariant.Secondary}
@@ -169,8 +190,8 @@ export function PreferencesBackups({
? i18n('icu:Preferences__button--manage')
: i18n('icu:Preferences__button--set-up')}
</Button>
}
/>
</div>
</FlowingControl>
</SettingsRow>
</>
);

View File

@@ -2,12 +2,14 @@
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useState, useCallback } from 'react';
import classNames from 'classnames';
import type { LocalizerType } from '../types/I18N';
import { toLogFormat } from '../types/errors';
import { formatFileSize } from '../util/formatFileSize';
import { SECOND } from '../util/durations';
import type { ValidationResultType as BackupValidationResultType } from '../services/backups';
import { SettingsRow, SettingsControl } from './PreferencesUtil';
import { SettingsRow, FlowingSettingsControl } from './PreferencesUtil';
import { Button, ButtonVariant } from './Button';
import { Spinner } from './Spinner';
import type { MessageCountBySchemaVersionType } from '../sql/Interface';
@@ -124,9 +126,17 @@ export function PreferencesInternal({
className="Preferences--internal--backups"
title={i18n('icu:Preferences__button--backups')}
>
<SettingsControl
left={i18n('icu:Preferences__internal__validate-backup--description')}
right={
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
{i18n('icu:Preferences__internal__validate-backup--description')}
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
variant={ButtonVariant.Secondary}
onClick={validateBackup}
@@ -138,8 +148,8 @@ export function PreferencesInternal({
i18n('icu:Preferences__internal__validate-backup')
)}
</Button>
}
/>
</div>
</FlowingSettingsControl>
{renderValidationResult(validationResult)}
</SettingsRow>
@@ -148,11 +158,19 @@ export function PreferencesInternal({
className="Preferences--internal--backups"
title={i18n('icu:Preferences__internal__local-backups')}
>
<SettingsControl
left={i18n(
'icu:Preferences__internal__export-local-backup--description'
)}
right={
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
{i18n(
'icu:Preferences__internal__export-local-backup--description'
)}
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
variant={ButtonVariant.Secondary}
onClick={exportLocalBackup}
@@ -164,8 +182,8 @@ export function PreferencesInternal({
i18n('icu:Preferences__internal__export-local-backup')
)}
</Button>
}
/>
</div>
</FlowingSettingsControl>
{renderValidationResult(exportResult)}
</SettingsRow>
@@ -174,9 +192,17 @@ export function PreferencesInternal({
className="Preferences--internal--message-schemas"
title="Message schema versions"
>
<SettingsControl
left="Check message schema versions"
right={
<FlowingSettingsControl>
<div className="Preferences__two-thirds-flow">
Check message schema versions
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
variant={ButtonVariant.Secondary}
onClick={async () => {
@@ -189,8 +215,8 @@ export function PreferencesInternal({
>
Fetch data
</Button>
}
/>
</div>
</FlowingSettingsControl>
{messageCountBySchemaVersion ? (
<div className="Preferences--internal--result">

View File

@@ -10,8 +10,13 @@ import React, {
useRef,
} from 'react';
import { noop } from 'lodash';
import classNames from 'classnames';
import type { LocalizerType } from '../types/I18N';
import { SettingsControl as Control, SettingsRow } from './PreferencesUtil';
import {
FlowingSettingsControl as FlowingControl,
SettingsRow,
} from './PreferencesUtil';
import { Button, ButtonSize, ButtonVariant } from './Button';
import { SIGNAL_BACKUPS_LEARN_MORE_URL } from './PreferencesBackups';
import { I18n } from './I18n';
@@ -87,42 +92,54 @@ export function PreferencesLocalBackups({
</div>
</div>
<SettingsRow className="Preferences--BackupsRow">
<Control
left={
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<label>
{i18n('icu:Preferences__local-backups-folder')}
<div className="Preferences__description">
{localBackupFolder}
</div>
</label>
}
right={
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={pickLocalBackupFolder}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__local-backups-folder__change')}
</Button>
}
/>
<Control
left={
</div>
</FlowingControl>
<FlowingControl>
<div className="Preferences__two-thirds-flow">
<label>
{i18n('icu:Preferences__backup-key')}
<div className="Preferences__description">
{i18n('icu:Preferences__backup-key-description')}
</div>
</label>
}
right={
</div>
<div
className={classNames(
'Preferences__flow-button',
'Preferences__one-third-flow',
'Preferences__one-third-flow--align-right'
)}
>
<Button
onClick={() => setPage(Page.LocalBackupsKeyReference)}
variant={ButtonVariant.Secondary}
>
{i18n('icu:Preferences__view-key')}
</Button>
}
/>
</div>
</FlowingControl>
</SettingsRow>
<SettingsRow className="Preferences--BackupsRow">
<div className="Preferences__padding">

View File

@@ -27,19 +27,42 @@ export function SettingsRow({
);
}
export function FlowingSettingsControl({
children,
}: {
children: ReactNode;
}): JSX.Element {
return <div className="Preferences__flow-control">{children}</div>;
}
export function LightIconLabel({
icon,
children,
}: {
icon: string;
children: ReactNode;
}): JSX.Element {
return (
<label className="Preferences__light-icon-label">
<div className={classNames('Preferences__control--icon', icon)} />
<div>{children}</div>
</label>
);
}
export function SettingsControl({
icon,
left,
onClick,
right,
rightStyle,
description,
}: {
/** A className or `true` to leave room for icon */
icon?: string | true;
left: ReactNode;
onClick?: () => unknown;
right: ReactNode;
rightStyle?: React.CSSProperties;
description?: boolean;
}): JSX.Element {
const content = (
<>
@@ -52,9 +75,10 @@ export function SettingsControl({
/>
)}
<div className="Preferences__control--key">{left}</div>
<div className="Preferences__control--value" style={rightStyle}>
{right}
</div>
<div className="Preferences__control--value">{right}</div>
{description ? (
<div className="Preferences__control--value">{description}</div>
) : undefined}
</>
);