feat: add the BlockedList component in Settings

pull/2425/head
Audric Ackermann 3 years ago
parent bf20c10f81
commit f137cad2a0

@ -6,8 +6,7 @@ body.rtl {
.group-settings-item, .group-settings-item,
.contact-selection-list, .contact-selection-list,
.group-member-list__selection, .group-member-list__selection,
.react-contexify__item, .react-contexify__item {
.session-settings-list {
direction: rtl; direction: rtl;
} }

@ -800,19 +800,6 @@ label {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&-list {
overflow-y: auto;
overflow-x: hidden;
}
&-view {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
&__password-lock { &__password-lock {
display: flex; display: flex;
align-items: center; align-items: center;

@ -64,6 +64,7 @@ export const MemberListItem = (props: {
// this bool is used to make a zombie appear with less opacity than a normal member // this bool is used to make a zombie appear with less opacity than a normal member
isZombie?: boolean; isZombie?: boolean;
inMentions?: boolean; // set to true if we are rendering members but in the Mentions picker inMentions?: boolean; // set to true if we are rendering members but in the Mentions picker
disableBg?: boolean;
isAdmin?: boolean; // if true, we add a small crown on top of their avatar isAdmin?: boolean; // if true, we add a small crown on top of their avatar
onSelect?: (pubkey: string) => void; onSelect?: (pubkey: string) => void;
onUnselect?: (pubkey: string) => void; onUnselect?: (pubkey: string) => void;
@ -77,6 +78,7 @@ export const MemberListItem = (props: {
onSelect, onSelect,
onUnselect, onUnselect,
inMentions, inMentions,
disableBg,
dataTestId, dataTestId,
} = props; } = props;
@ -89,7 +91,7 @@ export const MemberListItem = (props: {
isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey); isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey);
}} }}
style={ style={
!inMentions !inMentions && !disableBg
? { ? {
backgroundColor: 'var(--color-cell-background)', backgroundColor: 'var(--color-cell-background)',
} }

@ -22,7 +22,7 @@ const StyledKnob = styled.div<{ active: boolean }>`
const StyledSessionToggle = styled.div<{ active: boolean }>` const StyledSessionToggle = styled.div<{ active: boolean }>`
width: 51px; width: 51px;
height: 25px; height: 25px;
border: 1.5px solid #e5e5ea; border: 1px solid #e5e5ea;
border-radius: 16px; border-radius: 16px;
position: relative; position: relative;
@ -31,8 +31,8 @@ const StyledSessionToggle = styled.div<{ active: boolean }>`
transition: var(--default-duration); transition: var(--default-duration);
background-color: ${props => background-color: ${props =>
props.active ? 'var(--color-accent)' : 'var(--color-cell-background)'}; props.active ? 'var(--color-accent)' : 'var(--color-clickable-hovered)'};
border-color: ${props => (props.active ? 'var(--color-accent)' : 'unset')}; border-color: ${props => (props.active ? 'var(--color-accent)' : 'var(--color-cell-background)')};
`; `;
type Props = { type Props = {

@ -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>
);
};

@ -38,10 +38,9 @@ const StyledDescriptionContainer = styled(StyledDescription)`
align-items: center; align-items: center;
`; `;
const StyledSettingItem = styled.div` export const StyledSettingItem = styled.div`
font-size: var(--font-size-md); font-size: var(--font-size-md);
padding: var(--margins-lg); padding: var(--margins-lg);
margin-bottom: 20px;
background: var(--color-cell-background); background: var(--color-cell-background);
color: var(--color-text); color: var(--color-text);
@ -62,7 +61,7 @@ const StyledSettingItemClickable = styled(StyledSettingItemInline)`
} }
`; `;
const SettingsTitleAndDescription = (props: { export const SettingsTitleAndDescription = (props: {
title?: string; title?: string;
description?: string; description?: string;
childrenDescription?: React.ReactNode; childrenDescription?: React.ReactNode;

@ -172,6 +172,21 @@ const SettingInCategory = (props: {
} }
}; };
const StyledSettingsView = styled.div`
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
`;
const StyledSettingsList = styled.div`
overflow-y: auto;
overflow-x: hidden;
display: flex;
flex-direction: column;
`;
export class SessionSettingsView extends React.Component<SettingsViewProps, State> { export class SessionSettingsView extends React.Component<SettingsViewProps, State> {
public settingsViewRef: React.RefObject<HTMLDivElement>; public settingsViewRef: React.RefObject<HTMLDivElement>;
@ -248,23 +263,23 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
<div className="session-settings"> <div className="session-settings">
<SettingsHeader category={category} /> <SettingsHeader category={category} />
<div className="session-settings-view"> <StyledSettingsView>
{shouldRenderPasswordLock ? ( {shouldRenderPasswordLock ? (
<PasswordLock <PasswordLock
pwdLockError={this.state.pwdLockError} pwdLockError={this.state.pwdLockError}
validatePasswordLock={this.validatePasswordLock} validatePasswordLock={this.validatePasswordLock}
/> />
) : ( ) : (
<div ref={this.settingsViewRef} className="session-settings-list"> <StyledSettingsList ref={this.settingsViewRef}>
<SettingInCategory <SettingInCategory
category={category} category={category}
onPasswordUpdated={this.onPasswordUpdated} onPasswordUpdated={this.onPasswordUpdated}
hasPassword={Boolean(this.state.hasPassword)} hasPassword={Boolean(this.state.hasPassword)}
/> />
</div> </StyledSettingsList>
)} )}
<SessionInfo /> <SessionInfo />
</div> </StyledSettingsView>
</div> </div>
); );
} }

@ -1,18 +1,16 @@
import React from 'react'; import React from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useUpdate from 'react-use/lib/useUpdate'; import useUpdate from 'react-use/lib/useUpdate';
import { SettingsKey } from '../../../data/settings-key'; import { SettingsKey } from '../../../data/settings-key';
import { useSet } from '../../../hooks/useSet';
import { ToastUtils } from '../../../session/utils'; import { ToastUtils } from '../../../session/utils';
import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
import { getBlockedPubkeys } from '../../../state/selectors/conversations';
import { getAudioAutoplay } from '../../../state/selectors/userConfig'; import { getAudioAutoplay } from '../../../state/selectors/userConfig';
import { MemberListItem } from '../../MemberListItem';
import { import { BlockedContactsList } from '../BlockedList';
SessionSettingsItemWrapper, // tslint:disable: use-simple-attributes
SessionToggleWithDescription,
} from '../SessionSettingListItem'; import { SessionToggleWithDescription } from '../SessionSettingListItem';
async function toggleCommunitiesPruning() { async function toggleCommunitiesPruning() {
try { try {
@ -83,60 +81,14 @@ const AudioMessageAutoPlaySetting = () => {
); );
}; };
const NoBlockedContacts = () => {
return (
<SessionSettingsItemWrapper
inline={true}
description={window.i18n('noBlockedContacts')}
title={''}
/>
);
};
const BlockedContactsList = (props: { blockedNumbers: Array<string> }) => {
const {
uniqueValues: selectedIds,
addTo: addToSelected,
removeFrom: removeFromSelected,
} = useSet<string>([]);
const blockedEntries = props.blockedNumbers.map(blockedEntry => {
return (
<MemberListItem
pubkey={blockedEntry}
isSelected={selectedIds.includes(blockedEntry)}
key={blockedEntry}
onSelect={addToSelected}
onUnselect={removeFromSelected}
/>
);
});
return (
<>
<SessionSettingsItemWrapper
title={window.i18n('blockedSettingsTitle')}
inline={false}
children={blockedEntries}
/>
</>
);
};
export const CategoryConversations = () => { export const CategoryConversations = () => {
const blockedNumbers = useSelector(getBlockedPubkeys);
return ( return (
<> <>
<CommunitiesPruningSetting /> <CommunitiesPruningSetting />
<SpellCheckSetting /> <SpellCheckSetting />
<AudioMessageAutoPlaySetting /> <AudioMessageAutoPlaySetting />
{blockedNumbers?.length ? ( <BlockedContactsList />
<BlockedContactsList blockedNumbers={blockedNumbers} />
) : (
<NoBlockedContacts />
)}
</> </>
); );
}; };

@ -23,5 +23,12 @@ export function useSet<T>(initialValues: Array<T> = []) {
[uniqueValues, setUniqueValues] [uniqueValues, setUniqueValues]
); );
return { uniqueValues, addTo, removeFrom }; const empty = useCallback(() => {
if (uniqueValues.length) {
setUniqueValues([]);
return;
}
}, [uniqueValues, setUniqueValues]);
return { uniqueValues, addTo, removeFrom, empty };
} }

@ -51,14 +51,6 @@ export const getConversationsCount = createSelector(getConversationLookup, (stat
return Object.values(state).length; return Object.values(state).length;
}); });
export const getBlockedPubkeys = createSelector(
// make sure to extends this selector to we are rerun on conversation changes
getConversationLookup,
(_state): Array<string> => {
return BlockedNumberController.getBlockedNumbers();
}
);
export const getSelectedConversationKey = createSelector( export const getSelectedConversationKey = createSelector(
getConversations, getConversations,

@ -87,6 +87,29 @@ export class BlockedNumberController {
} }
} }
/**
* Unblock all thope users.
* This will only unblock the primary device of the user.
*
* @param user The user to unblock.
*/
public static async unblockAll(users: Array<string>): Promise<void> {
await this.load();
let changes = false;
users.forEach(user => {
const toUnblock = PubKey.cast(user);
if (this.blockedNumbers.has(toUnblock.key)) {
this.blockedNumbers.delete(toUnblock.key);
changes = true;
}
});
if (changes) {
await this.saveToDB(BLOCKED_NUMBERS_ID, this.blockedNumbers);
}
}
public static async setBlocked(user: string | PubKey, blocked: boolean): Promise<void> { public static async setBlocked(user: string | PubKey, blocked: boolean): Promise<void> {
if (blocked) { if (blocked) {
return BlockedNumberController.block(user); return BlockedNumberController.block(user);

Loading…
Cancel
Save