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,
.contact-selection-list,
.group-member-list__selection,
.react-contexify__item,
.session-settings-list {
.react-contexify__item {
direction: rtl;
}

@ -800,19 +800,6 @@ label {
display: flex;
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 {
display: flex;
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
isZombie?: boolean;
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
onSelect?: (pubkey: string) => void;
onUnselect?: (pubkey: string) => void;
@ -77,6 +78,7 @@ export const MemberListItem = (props: {
onSelect,
onUnselect,
inMentions,
disableBg,
dataTestId,
} = props;
@ -89,7 +91,7 @@ export const MemberListItem = (props: {
isSelected ? onUnselect?.(pubkey) : onSelect?.(pubkey);
}}
style={
!inMentions
!inMentions && !disableBg
? {
backgroundColor: 'var(--color-cell-background)',
}

@ -22,7 +22,7 @@ const StyledKnob = styled.div<{ active: boolean }>`
const StyledSessionToggle = styled.div<{ active: boolean }>`
width: 51px;
height: 25px;
border: 1.5px solid #e5e5ea;
border: 1px solid #e5e5ea;
border-radius: 16px;
position: relative;
@ -31,8 +31,8 @@ const StyledSessionToggle = styled.div<{ active: boolean }>`
transition: var(--default-duration);
background-color: ${props =>
props.active ? 'var(--color-accent)' : 'var(--color-cell-background)'};
border-color: ${props => (props.active ? 'var(--color-accent)' : 'unset')};
props.active ? 'var(--color-accent)' : 'var(--color-clickable-hovered)'};
border-color: ${props => (props.active ? 'var(--color-accent)' : 'var(--color-cell-background)')};
`;
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;
`;
const StyledSettingItem = styled.div`
export const StyledSettingItem = styled.div`
font-size: var(--font-size-md);
padding: var(--margins-lg);
margin-bottom: 20px;
background: var(--color-cell-background);
color: var(--color-text);
@ -62,7 +61,7 @@ const StyledSettingItemClickable = styled(StyledSettingItemInline)`
}
`;
const SettingsTitleAndDescription = (props: {
export const SettingsTitleAndDescription = (props: {
title?: string;
description?: string;
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> {
public settingsViewRef: React.RefObject<HTMLDivElement>;
@ -248,23 +263,23 @@ export class SessionSettingsView extends React.Component<SettingsViewProps, Stat
<div className="session-settings">
<SettingsHeader category={category} />
<div className="session-settings-view">
<StyledSettingsView>
{shouldRenderPasswordLock ? (
<PasswordLock
pwdLockError={this.state.pwdLockError}
validatePasswordLock={this.validatePasswordLock}
/>
) : (
<div ref={this.settingsViewRef} className="session-settings-list">
<StyledSettingsList ref={this.settingsViewRef}>
<SettingInCategory
category={category}
onPasswordUpdated={this.onPasswordUpdated}
hasPassword={Boolean(this.state.hasPassword)}
/>
</div>
</StyledSettingsList>
)}
<SessionInfo />
</div>
</StyledSettingsView>
</div>
);
}

@ -1,18 +1,16 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
// tslint:disable-next-line: no-submodule-imports
import useUpdate from 'react-use/lib/useUpdate';
import { SettingsKey } from '../../../data/settings-key';
import { useSet } from '../../../hooks/useSet';
import { ToastUtils } from '../../../session/utils';
import { toggleAudioAutoplay } from '../../../state/ducks/userConfig';
import { getBlockedPubkeys } from '../../../state/selectors/conversations';
import { getAudioAutoplay } from '../../../state/selectors/userConfig';
import { MemberListItem } from '../../MemberListItem';
import {
SessionSettingsItemWrapper,
SessionToggleWithDescription,
} from '../SessionSettingListItem';
import { BlockedContactsList } from '../BlockedList';
// tslint:disable: use-simple-attributes
import { SessionToggleWithDescription } from '../SessionSettingListItem';
async function toggleCommunitiesPruning() {
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 = () => {
const blockedNumbers = useSelector(getBlockedPubkeys);
return (
<>
<CommunitiesPruningSetting />
<SpellCheckSetting />
<AudioMessageAutoPlaySetting />
{blockedNumbers?.length ? (
<BlockedContactsList blockedNumbers={blockedNumbers} />
) : (
<NoBlockedContacts />
)}
<BlockedContactsList />
</>
);
};

@ -23,5 +23,12 @@ export function useSet<T>(initialValues: Array<T> = []) {
[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;
});
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(
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> {
if (blocked) {
return BlockedNumberController.block(user);

Loading…
Cancel
Save