feat: add sorted contacts list merged by starting char

pull/2410/head
Audric Ackermann 3 years ago
parent 374b71630a
commit f415ef36dd

@ -55,6 +55,7 @@ audio {
button {
cursor: pointer;
font-size: inherit;
border: none;
&[disabled='disabled'] {
&,

@ -0,0 +1,97 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import styled, { CSSProperties } from 'styled-components';
import { useAvatarPath } from '../../../../hooks/useParamSelector';
import { openConversationWithMessages } from '../../../../state/ducks/conversations';
import { updateUserDetailsModal } from '../../../../state/ducks/modalDialog';
import { Avatar, AvatarSize } from '../../../avatar/Avatar';
type Props = { id: string; displayName?: string; style: CSSProperties };
const StyledAvatarItem = styled.div`
padding-right: var(--margins-sm);
`;
const AvatarItem = (props: Pick<Props, 'displayName' | 'id'>) => {
const { id, displayName } = props;
const avatarPath = useAvatarPath(id);
const dispatch = useDispatch();
function onPrivateAvatarClick() {
dispatch(
updateUserDetailsModal({
conversationId: id,
userName: displayName || '',
authorAvatarPath: avatarPath,
})
);
}
return (
<StyledAvatarItem>
<Avatar size={AvatarSize.S} pubkey={id} onAvatarClick={onPrivateAvatarClick} />
</StyledAvatarItem>
);
};
const StyledContactRowName = styled.div`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* font-weight: 600; */
font-size: var(--font-size-lg);
`;
const StyledRowContainer = styled.div`
display: flex;
align-items: center;
padding: 0 var(--margins-lg);
transition: background-color var(--default-duration) linear;
cursor: pointer;
border-bottom: 1px var(--color-session-border) solid;
&:first-child {
border-top: 1px var(--color-session-border) solid;
}
:hover {
background-color: var(--color-clickable-hovered);
}
`;
export const ContactRow = (props: Props) => {
const { id, style, displayName } = props;
return (
<StyledRowContainer
style={style}
onClick={() => openConversationWithMessages({ conversationKey: id, messageId: null })}
>
<AvatarItem id={id} displayName={displayName} />
<StyledContactRowName>{displayName || id}</StyledContactRowName>
</StyledRowContainer>
);
};
const StyledBreak = styled.div`
display: flex;
align-items: center;
padding: 0 var(--margins-lg);
color: var(--color-text-subtle);
font-size: var(--font-size-md);
height: 25px; // should also be changed in rowHeight
border-bottom: 1px var(--color-session-border) solid;
`;
export const ContactRowBreak = (props: { char: string; key: string; style: CSSProperties }) => {
const { char, key, style } = props;
return (
<StyledBreak key={key} style={style}>
{char}
</StyledBreak>
);
};

@ -1,13 +1,15 @@
import { isString } from 'lodash';
import React from 'react';
import { useSelector } from 'react-redux';
import { AutoSizer, List, ListRowProps } from 'react-virtualized';
import { AutoSizer, Index, List, ListRowProps } from 'react-virtualized';
import styled from 'styled-components';
import {
getDirectContacts,
DirectContactsByNameType,
getDirectContactsByName,
getDirectContactsCount,
} from '../../../../state/selectors/conversations';
import { MemoConversationListItemWithDetails } from '../../conversation-list-item/ConversationListItem';
import { StyledLeftPaneList } from '../../LeftPaneList';
import { ContactRow, ContactRowBreak } from './ContactRow';
import { StyledChooseActionTitle } from './OverlayChooseAction';
// tslint:disable: use-simple-attributes no-submodule-imports
@ -16,23 +18,45 @@ const renderRow = (props: ListRowProps) => {
// ugly, but it seems react-viurtualized do not support very well functional components just yet
// https://stackoverflow.com/questions/54488954/how-to-pass-prop-into-rowrender-of-react-virtualized
const directContacts = (parent as any).props.directContacts;
const item = directContacts?.[index];
const directContactsByNameWithBreaks = (parent as any).props
.directContactsByNameWithBreaks as Array<DirectContactsByNameType | string>;
const item = directContactsByNameWithBreaks?.[index];
if (!item) {
return null;
}
return <MemoConversationListItemWithDetails style={style} key={key} {...item} />;
if (isString(item)) {
return <ContactRowBreak style={style} key={key} char={item} />;
}
return <ContactRow style={style} key={key} {...item} />;
};
const unknownSection = 'unknown';
const ContactListItemSection = () => {
const directContacts = useSelector(getDirectContacts);
const directContactsByName = useSelector(getDirectContactsByName);
if (!directContacts) {
if (!directContactsByName) {
return null;
}
const length = Number(directContacts.length);
// add a break wherever needed
let currentChar = '';
// if the item is a string we consider it to be a break of that string
const directContactsByNameWithBreaks: Array<DirectContactsByNameType | string> = [];
directContactsByName.forEach(m => {
if (m.displayName && m.displayName[0] !== currentChar) {
currentChar = m.displayName[0];
directContactsByNameWithBreaks.push(currentChar.toUpperCase());
} else if (!m.displayName && currentChar !== unknownSection) {
currentChar = unknownSection;
directContactsByNameWithBreaks.push(window.i18n('unknown'));
}
directContactsByNameWithBreaks.push(m);
});
const length = Number(directContactsByNameWithBreaks.length);
return (
<StyledLeftPaneList key={0} style={{ width: '100%' }}>
@ -43,8 +67,11 @@ const ContactListItemSection = () => {
className="module-left-pane__virtual-list"
height={height}
rowCount={length}
rowHeight={64}
directContacts={directContacts}
rowHeight={
(params: Index) =>
isString(directContactsByNameWithBreaks[params.index]) ? 25 : 64 // should also be changed in `ContactRowBreak`
}
directContactsByNameWithBreaks={directContactsByNameWithBreaks}
rowRenderer={renderRow}
width={300} // the same as session-left-pane-width
autoHeight={false}

@ -323,6 +323,7 @@ export const SessionGlobalStyles = createGlobalStyle`
--font-size-xs: 11px;
--font-size-sm: 13px;
--font-size-md: 15px;
--font-size-lg: 17px;
/* MARGINS */
--margins-xs: 5px;
@ -331,7 +332,7 @@ export const SessionGlobalStyles = createGlobalStyle`
--margins-lg: 20px;
/* ANIMATIONS */
--default-duration: '0.25s';
--default-duration: 0.25s;
/* FILTERS */
--filter-session-text: ${lightFilterSessionText};
/* BORDERS */

@ -4,21 +4,16 @@ import { ConversationsStateType, ReduxConversationType } from '../ducks/conversa
import { StateType } from '../reducer';
import { getConversations, getSelectedConversationKey } from './conversations';
export const getCallState = (state: StateType): CallStateType => state.call;
const getCallState = (state: StateType): CallStateType => state.call;
// --- INCOMING CALLS
export const getHasIncomingCallFrom = createSelector(getCallState, (state: CallStateType):
| string
| undefined => {
return state.ongoingWith && state.ongoingCallStatus === 'incoming'
? state.ongoingWith
export const getHasIncomingCallFrom = (state: StateType) => {
return state.call.ongoingWith && state.call.ongoingCallStatus === 'incoming'
? state.call.ongoingWith
: undefined;
});
};
export const getHasIncomingCall = createSelector(
getHasIncomingCallFrom,
(withConvo: string | undefined): boolean => !!withConvo
);
export const getHasIncomingCall = (state: StateType) => !!getHasIncomingCallFrom(state);
// --- ONGOING CALLS
export const getHasOngoingCallWith = createSelector(
@ -55,21 +50,14 @@ export const getHasOngoingCallWithFocusedConvo = createSelector(
}
);
const getCallStateWithFocusedConvo = createSelector(
getCallState,
getSelectedConversationKey,
(callState: CallStateType, selectedConvoPubkey?: string): CallStatusEnum => {
if (
selectedConvoPubkey &&
callState.ongoingWith &&
selectedConvoPubkey === callState.ongoingWith
) {
return callState.ongoingCallStatus;
}
return undefined;
const getCallStateWithFocusedConvo = (state: StateType): CallStatusEnum => {
const selected = state.conversations.selectedConversation;
const ongoingWith = state.call.ongoingWith;
if (selected && ongoingWith && selected === ongoingWith) {
return state.call.ongoingCallStatus;
}
);
return undefined;
};
export const getCallWithFocusedConvoIsOffering = createSelector(
getCallStateWithFocusedConvo,

@ -12,12 +12,11 @@ import {
SortedMessageModelProps,
} from '../ducks/conversations';
import { getIntl, getOurNumber } from './user';
import { getIntl } from './user';
import { BlockedNumberController } from '../../util';
import { ConversationModel } from '../../models/conversation';
import { LocalizerType } from '../../types/Util';
import { ConversationHeaderTitleProps } from '../../components/conversation/ConversationHeader';
import _ from 'lodash';
import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox';
import { MessageAttachmentSelectorProps } from '../../components/conversation/message/message-content/MessageAttachment';
import { MessageAuthorSelectorProps } from '../../components/conversation/message/message-content/MessageAuthorText';
@ -35,6 +34,7 @@ import { getConversationController } from '../../session/conversations';
import { UserUtils } from '../../session/utils';
import { Storage } from '../../util/storage';
import { ConversationTypeEnum } from '../../models/conversationAttributes';
import { filter, isEmpty, sortBy } from 'lodash';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
@ -456,7 +456,7 @@ export const getSortedConversations = createSelector(
const _getConversationRequests = (
sortedConversations: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
return _.filter(sortedConversations, conversation => {
return filter(sortedConversations, conversation => {
const { isApproved, isBlocked, isPrivate, isMe, activeAt } = conversation;
const isRequest = ConversationModel.hasValidIncomingRequestValues({
isApproved,
@ -477,7 +477,7 @@ export const getConversationRequests = createSelector(
const _getUnreadConversationRequests = (
sortedConversationRequests: Array<ReduxConversationType>
): Array<ReduxConversationType> => {
return _.filter(sortedConversationRequests, conversation => {
return filter(sortedConversationRequests, conversation => {
return conversation && conversation.unreadCount && conversation.unreadCount > 0;
});
};
@ -490,7 +490,7 @@ export const getUnreadConversationRequests = createSelector(
const _getPrivateContactsPubkeys = (
sortedConversations: Array<ReduxConversationType>
): Array<string> => {
return _.filter(sortedConversations, conversation => {
return filter(sortedConversations, conversation => {
return (
conversation.isPrivate &&
!conversation.isBlocked &&
@ -517,13 +517,6 @@ export const getPrivateContactsPubkeys = createSelector(
export const getLeftPaneLists = createSelector(getSortedConversations, _getLeftPaneLists);
export const getMe = createSelector(
[getConversationLookup, getOurNumber],
(lookup: ConversationLookupType, ourNumber: string): ReduxConversationType => {
return lookup[ourNumber];
}
);
export const getDirectContacts = createSelector(
getLeftPaneLists,
(state: {
@ -538,6 +531,36 @@ export const getDirectContactsCount = createSelector(
(contacts: Array<ReduxConversationType>) => contacts.length
);
export type DirectContactsByNameType = {
displayName?: string;
id: string;
};
// make sure that createSelector is called here so this function is memoized
export const getDirectContactsByName = createSelector(
getDirectContacts,
(contacts: Array<ReduxConversationType>): Array<DirectContactsByNameType> => {
const extractedContacts = contacts
.filter(m => m.id !== UserUtils.getOurPubKeyStrFromCache())
.map(m => {
return {
id: m.id,
displayName: m.nickname || m.displayNameInProfile,
};
});
const extractedContactsNoDisplayName = sortBy(
extractedContacts.filter(m => !m.displayName),
'id'
);
const extractedContactsWithDisplayName = sortBy(
extractedContacts.filter(m => Boolean(m.displayName)),
'displayName'
);
return [...extractedContactsWithDisplayName, ...extractedContactsNoDisplayName];
}
);
export const getUnreadMessageCount = createSelector(getLeftPaneLists, (state): number => {
return state.unreadCount;
});
@ -885,7 +908,7 @@ export const getMessagePropsByMessageId = createSelector(
export const getMessageAvatarProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAvatarSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -920,7 +943,7 @@ export const getMessageAvatarProps = createSelector(getMessagePropsByMessageId,
export const getMessagePreviewProps = createSelector(getMessagePropsByMessageId, (props):
| MessagePreviewSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -937,7 +960,7 @@ export const getMessagePreviewProps = createSelector(getMessagePropsByMessageId,
export const getMessageQuoteProps = createSelector(getMessagePropsByMessageId, (props):
| MessageQuoteSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -954,7 +977,7 @@ export const getMessageQuoteProps = createSelector(getMessagePropsByMessageId, (
export const getMessageStatusProps = createSelector(getMessagePropsByMessageId, (props):
| MessageStatusSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -971,7 +994,7 @@ export const getMessageStatusProps = createSelector(getMessagePropsByMessageId,
export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (props):
| MessageTextSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -991,7 +1014,7 @@ export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (p
export const getMessageContextMenuProps = createSelector(getMessagePropsByMessageId, (props):
| MessageContextMenuSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -1037,7 +1060,7 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag
export const getMessageAuthorProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAuthorSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -1058,7 +1081,7 @@ export const getMessageAuthorProps = createSelector(getMessagePropsByMessageId,
export const getMessageIsDeletable = createSelector(
getMessagePropsByMessageId,
(props): boolean => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return false;
}
@ -1069,7 +1092,7 @@ export const getMessageIsDeletable = createSelector(
export const getMessageAttachmentProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAttachmentSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -1099,7 +1122,7 @@ export const getIsMessageSelected = createSelector(
getMessagePropsByMessageId,
getSelectedMessageIds,
(props, selectedIds): boolean => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return false;
}
@ -1112,7 +1135,7 @@ export const getIsMessageSelected = createSelector(
export const getMessageContentSelectorProps = createSelector(getMessagePropsByMessageId, (props):
| MessageContentSelectorProps
| undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -1145,7 +1168,7 @@ export const getMessageContentSelectorProps = createSelector(getMessagePropsByMe
export const getMessageContentWithStatusesSelectorProps = createSelector(
getMessagePropsByMessageId,
(props): MessageContentWithStatusSelectorProps | undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}
@ -1170,7 +1193,7 @@ export const getMessageContentWithStatusesSelectorProps = createSelector(
export const getGenericReadableMessageSelectorProps = createSelector(
getMessagePropsByMessageId,
(props): GenericReadableMessageSelectorProps | undefined => {
if (!props || _.isEmpty(props)) {
if (!props || isEmpty(props)) {
return undefined;
}

Loading…
Cancel
Save