Merge branch 'settings-redesign' into theming
commit
691b567916
@ -1,29 +1,86 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
const StyledTypingContainer = styled.div`
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 8px;
|
||||||
|
width: 38px;
|
||||||
|
padding-inline-start: 1px;
|
||||||
|
padding-inline-end: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTypingDot = styled.div<{ index: number }>`
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--color-text-subtle);
|
||||||
|
|
||||||
|
height: 6px;
|
||||||
|
width: 6px;
|
||||||
|
opacity: 0.4;
|
||||||
|
|
||||||
|
@keyframes typing-animation-first {
|
||||||
|
0% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-animation-second {
|
||||||
|
10% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing-animation-third {
|
||||||
|
20% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animation: ${props =>
|
||||||
|
props.index === 0
|
||||||
|
? 'typing-animation-first'
|
||||||
|
: props.index === 1
|
||||||
|
? 'typing-animation-second'
|
||||||
|
: 'typing-animation-third'}
|
||||||
|
1600ms ease infinite;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSpacer = styled.div`
|
||||||
|
flex-grow: 1;
|
||||||
|
`;
|
||||||
|
|
||||||
export const TypingAnimation = () => {
|
export const TypingAnimation = () => {
|
||||||
return (
|
return (
|
||||||
<div className="module-typing-animation" title={window.i18n('typingAlt')}>
|
<StyledTypingContainer title={window.i18n('typingAlt')} aria-label={window.i18n('typingAlt')}>
|
||||||
<div
|
<StyledTypingDot index={0} />
|
||||||
className={classNames(
|
<StyledSpacer />
|
||||||
'module-typing-animation__dot',
|
<StyledTypingDot index={1} />
|
||||||
'module-typing-animation__dot--first'
|
|
||||||
)}
|
<StyledSpacer />
|
||||||
/>
|
<StyledTypingDot index={2} />
|
||||||
<div className="module-typing-animation__spacer" />
|
</StyledTypingContainer>
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'module-typing-animation__dot',
|
|
||||||
'module-typing-animation__dot--second'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="module-typing-animation__spacer" />
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
'module-typing-animation__dot',
|
|
||||||
'module-typing-animation__dot--third'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,162 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
// tslint:disable-next-line: no-submodule-imports
|
||||||
|
import useUpdate from 'react-use/lib/useUpdate';
|
||||||
|
import styled, { CSSProperties } from 'styled-components';
|
||||||
|
import { useSet } from '../../hooks/useSet';
|
||||||
|
import { ToastUtils } from '../../session/utils';
|
||||||
|
import { BlockedNumberController } from '../../util';
|
||||||
|
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
|
||||||
|
import { SpacerLG } from '../basic/Text';
|
||||||
|
import { SessionIconButton } from '../icon';
|
||||||
|
import { MemberListItem } from '../MemberListItem';
|
||||||
|
import { SettingsTitleAndDescription } from './SessionSettingListItem';
|
||||||
|
// tslint:disable: use-simple-attributes
|
||||||
|
|
||||||
|
const BlockedEntriesContainer = styled.div`
|
||||||
|
flex-shrink: 1;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 40px;
|
||||||
|
max-height: 100%;
|
||||||
|
background: var(--blocked-contact-list-bg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockedEntriesRoundedContainer = styled.div`
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: var(--margins-lg);
|
||||||
|
background: var(--blocked-contact-list-bg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockedContactsSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockedContactListTitle = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-height: 45px;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockedContactListTitleButtons = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const StyledBlockedSettingItem = styled.div<{ clickable: boolean }>`
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
padding: var(--margins-lg);
|
||||||
|
|
||||||
|
background: var(--color-cell-background);
|
||||||
|
color: var(--color-text);
|
||||||
|
border-bottom: var(--border-session);
|
||||||
|
|
||||||
|
cursor: ${props => (props.clickable ? 'pointer' : 'unset')};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const BlockedEntries = (props: {
|
||||||
|
blockedNumbers: Array<string>;
|
||||||
|
selectedIds: Array<string>;
|
||||||
|
addToSelected: (id: string) => void;
|
||||||
|
removeFromSelected: (id: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { addToSelected, blockedNumbers, removeFromSelected, selectedIds } = props;
|
||||||
|
return (
|
||||||
|
<BlockedEntriesRoundedContainer>
|
||||||
|
<BlockedEntriesContainer>
|
||||||
|
{blockedNumbers.map(blockedEntry => {
|
||||||
|
return (
|
||||||
|
<MemberListItem
|
||||||
|
pubkey={blockedEntry}
|
||||||
|
isSelected={selectedIds.includes(blockedEntry)}
|
||||||
|
key={blockedEntry}
|
||||||
|
onSelect={addToSelected}
|
||||||
|
onUnselect={removeFromSelected}
|
||||||
|
disableBg={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</BlockedEntriesContainer>
|
||||||
|
</BlockedEntriesRoundedContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoBlockedContacts = () => {
|
||||||
|
return <div>{window.i18n('noBlockedContacts')}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlockedContactsList = () => {
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const {
|
||||||
|
uniqueValues: selectedIds,
|
||||||
|
addTo: addToSelected,
|
||||||
|
removeFrom: removeFromSelected,
|
||||||
|
empty: emptySelected,
|
||||||
|
} = useSet<string>([]);
|
||||||
|
|
||||||
|
const forceUpdate = useUpdate();
|
||||||
|
|
||||||
|
const hasAtLeastOneSelected = Boolean(selectedIds.length);
|
||||||
|
const blockedNumbers = BlockedNumberController.getBlockedNumbers();
|
||||||
|
const noBlockedNumbers = !blockedNumbers.length;
|
||||||
|
|
||||||
|
function toggleUnblockList() {
|
||||||
|
if (blockedNumbers.length) {
|
||||||
|
setExpanded(!expanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unBlockThoseUsers() {
|
||||||
|
if (selectedIds.length) {
|
||||||
|
await BlockedNumberController.unblockAll(selectedIds);
|
||||||
|
emptySelected();
|
||||||
|
ToastUtils.pushToastSuccess('unblocked', window.i18n('unblocked'));
|
||||||
|
forceUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BlockedContactsSection
|
||||||
|
style={{ '--blocked-contact-list-bg': 'var(--color-input-background)' } as CSSProperties}
|
||||||
|
>
|
||||||
|
<StyledBlockedSettingItem clickable={!noBlockedNumbers}>
|
||||||
|
<BlockedContactListTitle onClick={toggleUnblockList}>
|
||||||
|
<SettingsTitleAndDescription title={window.i18n('blockedSettingsTitle')} />
|
||||||
|
{noBlockedNumbers ? (
|
||||||
|
<NoBlockedContacts />
|
||||||
|
) : (
|
||||||
|
<BlockedContactListTitleButtons>
|
||||||
|
{hasAtLeastOneSelected && expanded ? (
|
||||||
|
<SessionButton
|
||||||
|
buttonColor={SessionButtonColor.Danger}
|
||||||
|
buttonType={SessionButtonType.BrandOutline}
|
||||||
|
text={window.i18n('unblockUser')}
|
||||||
|
onClick={unBlockThoseUsers}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<SpacerLG />
|
||||||
|
<SessionIconButton
|
||||||
|
iconSize={'large'}
|
||||||
|
iconType={'chevron'}
|
||||||
|
onClick={toggleUnblockList}
|
||||||
|
iconRotation={expanded ? 0 : 180}
|
||||||
|
/>
|
||||||
|
<SpacerLG />
|
||||||
|
</BlockedContactListTitleButtons>
|
||||||
|
)}
|
||||||
|
</BlockedContactListTitle>
|
||||||
|
</StyledBlockedSettingItem>
|
||||||
|
{expanded && !noBlockedNumbers ? (
|
||||||
|
<BlockedEntries
|
||||||
|
blockedNumbers={blockedNumbers}
|
||||||
|
selectedIds={selectedIds}
|
||||||
|
addToSelected={addToSelected}
|
||||||
|
removeFromSelected={removeFromSelected}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</BlockedContactsSection>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,203 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import styled from 'styled-components';
|
||||||
|
import { switchThemeTo } from '../../session/utils/Theme';
|
||||||
|
import {
|
||||||
|
darkColorReceivedMessageBg,
|
||||||
|
darkColorSentMessageBg,
|
||||||
|
getPrimaryColors,
|
||||||
|
lightColorReceivedMessageBg,
|
||||||
|
lightColorSentMessageBg,
|
||||||
|
OceanBlueDark,
|
||||||
|
OceanBlueLight,
|
||||||
|
PrimaryColorIds,
|
||||||
|
} from '../../state/ducks/SessionTheme';
|
||||||
|
import { ThemeStateType } from '../../state/ducks/theme';
|
||||||
|
import { getTheme } from '../../state/selectors/theme';
|
||||||
|
import { SessionRadio, SessionRadioPrimaryColors } from '../basic/SessionRadio';
|
||||||
|
import { SpacerLG, SpacerMD } from '../basic/Text';
|
||||||
|
import { StyledDescriptionSettingsItem, StyledTitleSettingsItem } from './SessionSettingListItem';
|
||||||
|
|
||||||
|
// tslint:disable: use-simple-attributes
|
||||||
|
|
||||||
|
const StyledSwitcherContainer = styled.div`
|
||||||
|
font-size: var(--font-size-md);
|
||||||
|
padding: var(--margins-lg);
|
||||||
|
background: var(--color-cell-background);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ThemeContainer = styled.button`
|
||||||
|
background: var(--color-conversation-list);
|
||||||
|
border: 1px solid var(--color-clickable-hovered);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: var(--margins-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 285px;
|
||||||
|
height: 90px;
|
||||||
|
|
||||||
|
transition: var(--default-duration);
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
background: var(--color-clickable-hovered);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ThemesContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--margins-lg);
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ThemeType = {
|
||||||
|
id: ThemeStateType;
|
||||||
|
title: string;
|
||||||
|
style: StyleSessionSwitcher;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StyleSessionSwitcher = {
|
||||||
|
background: string;
|
||||||
|
border: string;
|
||||||
|
receivedBg: string;
|
||||||
|
sentBg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledPreview = styled.svg`
|
||||||
|
max-height: 100%;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ThemePreview = (props: { style: StyleSessionSwitcher }) => {
|
||||||
|
return (
|
||||||
|
<StyledPreview xmlSpace="preserve" viewBox="0 0 80 72" fill={props.style.background}>
|
||||||
|
<path
|
||||||
|
stroke={props.style.border}
|
||||||
|
d="M7.5.9h64.6c3.6 0 6.5 2.9 6.5 6.5v56.9c0 3.6-2.9 6.5-6.5 6.5H7.5c-3.6 0-6.5-2.9-6.5-6.5V7.4C1 3.9 3.9.9 7.5.9z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={props.style.receivedBg}
|
||||||
|
d="M8.7 27.9c0-3.2 2.6-5.7 5.7-5.7h30.4c3.2 0 5.7 2.6 5.7 5.7 0 3.2-2.6 5.7-5.7 5.7H14.4c-3.1.1-5.7-2.5-5.7-5.7z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill={props.style.sentBg}
|
||||||
|
d="M32.6 42.2c0-3.2 2.6-5.7 5.7-5.7h27c3.2 0 5.7 2.6 5.7 5.7 0 3.2-2.6 5.7-5.7 5.7h-27c-3.1 0-5.7-2.5-5.7-5.7z"
|
||||||
|
/>
|
||||||
|
</StyledPreview>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Themes = (props: { selectedAccent?: PrimaryColorIds }) => {
|
||||||
|
const { selectedAccent } = props;
|
||||||
|
|
||||||
|
// I am not too sure if we want to override the accent color on the Theme switcher of not.
|
||||||
|
// If we do, we also need a way to rollback to the default, I guess?
|
||||||
|
const overridenAccent = selectedAccent
|
||||||
|
? getPrimaryColors().find(e => {
|
||||||
|
return e.id === selectedAccent;
|
||||||
|
})?.color
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const themes: Array<ThemeType> = [
|
||||||
|
{
|
||||||
|
id: 'dark',
|
||||||
|
title: window.i18n('classicDarkThemeTitle'),
|
||||||
|
style: {
|
||||||
|
background: '#000000',
|
||||||
|
border: '#414141',
|
||||||
|
receivedBg: darkColorReceivedMessageBg,
|
||||||
|
sentBg: overridenAccent || darkColorSentMessageBg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'light',
|
||||||
|
title: window.i18n('classicLightThemeTitle'),
|
||||||
|
style: {
|
||||||
|
background: '#ffffff',
|
||||||
|
border: '#414141',
|
||||||
|
receivedBg: lightColorReceivedMessageBg,
|
||||||
|
sentBg: overridenAccent || lightColorSentMessageBg,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ocean-dark',
|
||||||
|
title: window.i18n('oceanDarkThemeTitle'),
|
||||||
|
style: {
|
||||||
|
background: OceanBlueDark.background,
|
||||||
|
border: OceanBlueDark.border,
|
||||||
|
receivedBg: OceanBlueDark.received,
|
||||||
|
sentBg: overridenAccent || OceanBlueDark.sent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ocean-light',
|
||||||
|
title: window.i18n('oceanLightThemeTitle'),
|
||||||
|
style: {
|
||||||
|
background: OceanBlueLight.background,
|
||||||
|
border: OceanBlueLight.border,
|
||||||
|
receivedBg: OceanBlueLight.received,
|
||||||
|
sentBg: overridenAccent || OceanBlueLight.sent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedTheme = useSelector(getTheme);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{themes.map(theme => {
|
||||||
|
function onSelectTheme() {
|
||||||
|
void switchThemeTo(theme.id, dispatch);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ThemeContainer key={theme.id} onClick={onSelectTheme}>
|
||||||
|
<ThemePreview style={theme.style} />
|
||||||
|
<SpacerLG />
|
||||||
|
|
||||||
|
<StyledTitleSettingsItem>{theme.title}</StyledTitleSettingsItem>
|
||||||
|
<SessionRadio
|
||||||
|
active={selectedTheme === theme.id}
|
||||||
|
label={''}
|
||||||
|
value={theme.id}
|
||||||
|
inputName={'theme-switcher'}
|
||||||
|
/>
|
||||||
|
</ThemeContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SettingsThemeSwitcher = () => {
|
||||||
|
//FIXME store that value somewhere in the theme object
|
||||||
|
const [selectedAccent, setSelectedAccent] = useState<PrimaryColorIds | undefined>(undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledSwitcherContainer>
|
||||||
|
<StyledTitleSettingsItem>{window.i18n('themesSettingTitle')}</StyledTitleSettingsItem>
|
||||||
|
<ThemesContainer>
|
||||||
|
<Themes selectedAccent={selectedAccent} />
|
||||||
|
</ThemesContainer>
|
||||||
|
<SpacerMD />
|
||||||
|
<StyledDescriptionSettingsItem>{window.i18n('primaryColor')}</StyledDescriptionSettingsItem>
|
||||||
|
<SpacerMD />
|
||||||
|
<ThemesContainer style={{ marginInlineStart: 'var(--margins-xs)' }}>
|
||||||
|
{getPrimaryColors().map(item => {
|
||||||
|
return (
|
||||||
|
<SessionRadioPrimaryColors
|
||||||
|
key={item.id}
|
||||||
|
active={item.id === selectedAccent}
|
||||||
|
value={item.id}
|
||||||
|
inputName="primary-colors"
|
||||||
|
ariaLabel={item.ariaLabel}
|
||||||
|
color={item.color}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedAccent(item.id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ThemesContainer>
|
||||||
|
</StyledSwitcherContainer>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,28 @@
|
|||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { switchHtmlToDarkTheme, switchHtmlToLightTheme } from '../../state/ducks/SessionTheme';
|
||||||
|
import { applyTheme, ThemeStateType } from '../../state/ducks/theme';
|
||||||
|
|
||||||
|
export async function switchThemeTo(theme: ThemeStateType, dispatch: Dispatch | null) {
|
||||||
|
await window.setTheme(theme);
|
||||||
|
|
||||||
|
// for now, do not switch to ocean light nor dark theme as the SessionTheme associated with them is not complete
|
||||||
|
let newTheme: ThemeStateType | null = null;
|
||||||
|
|
||||||
|
switch (theme) {
|
||||||
|
case 'dark':
|
||||||
|
switchHtmlToDarkTheme();
|
||||||
|
newTheme = 'dark';
|
||||||
|
break;
|
||||||
|
case 'light':
|
||||||
|
switchHtmlToLightTheme();
|
||||||
|
newTheme = 'light';
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
window.log.warn('Unsupported theme: ', theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newTheme) {
|
||||||
|
dispatch?.(applyTheme(newTheme));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue