add conversations with working search on leftpanel

This commit is contained in:
Audric Ackermann 2019-12-17 17:35:27 +11:00
parent 3893a26251
commit 225f002648
9 changed files with 398 additions and 274 deletions

View File

@ -2387,5 +2387,8 @@
},
"completeSignUp": {
"message": "Complete Sign Up"
},
"compose": {
"message": "Compose"
}
}

View File

@ -435,6 +435,10 @@ label {
visibility: hidden;
}
.session-button div[role='button'] {
cursor: pointer;
}
.input-with-label-container {
height: 46.5px;
width: 280px;

View File

@ -1,12 +1,12 @@
// Messages
.dark-theme {
.conversation {
background: none !important;
}
.module-conversation-header {
border-bottom: none;
background-color: $session-shade-4;
}
.discussion-container {
background-image: $session-gradient-black;
}
.module-message {
&__author,
@ -53,4 +53,15 @@
margin-left: 5px;
}
}
.inbox {
background-image: $session-gradient-black;
}
}
.conversation {
background: none !important;
}
.discussion-container {
background: none !important;
}

View File

@ -5,11 +5,21 @@
background-color: $session-shade-11;
}
}
.gutter {
width: 380px;
background: none;
}
}
.inbox {
background: linear-gradient(90deg, $session-shade-2 100%, #171717 0%);
}
.module-left-pane {
border-right: none !important;
width: 300px;
height: -webkit-fill-available;
background: none !important;
&-session {
display: flex;
@ -20,7 +30,6 @@
width: 80px;
display: inline-flex;
flex-direction: column;
background: $session-shade-2;
.module-avatar,
.session-icon-button {
@ -30,8 +39,50 @@
}
}
}
&__header {
display: flex;
flex-direction: row;
margin: 28px 7px 28px 0px;
}
&__title {
font-family: $session-font-family;
color: $session-color-white;
font-size: 25px;
flex-grow: 1;
}
}
.gutter {
width: 380px;
padding-right: 5px;
background: none;
}
.session-search-input {
height: 34px;
width: 100%;
margin-right: 1px;
margin-bottom: 10px;
background: $session-shade-4;
color: $session-color-white;
display: inline-flex;
.session-icon-button {
margin: auto 10px;
}
input {
width: inherit;
height: inherit;
background: $session-shade-4;
color: $session-color-white;
border: none;
flex-grow: 1;
&:focus {
outline: none !important;
}
}
}

View File

@ -1,27 +1,21 @@
import React from 'react';
import classNames from 'classnames';
import { AutoSizer, List } from 'react-virtualized';
import {
ConversationListItem,
PropsData as ConversationListItemPropsType,
} from './ConversationListItem';
import {
PropsData as SearchResultsProps,
SearchResults,
} from './SearchResults';
import { LocalizerType } from '../types/Util';
import { LeftPaneSections, SectionType } from './session/LeftPaneSections';
import { LeftPaneMessageSection } from './session/LeftPaneMessageSection';
import { LeftPaneSections, SectionType } from './LeftPaneSections';
import { PropsData as ConversationListItemPropsType } from './ConversationListItem';
import { PropsData as SearchResultsProps } from './SearchResults';
import { SearchOptions } from '../types/Search';
export interface Props {
interface State {
selectedSection: SectionType;
}
interface Props {
conversations?: Array<ConversationListItemPropsType>;
friends?: Array<ConversationListItemPropsType>;
archivedConversations?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps;
showArchived?: boolean;
i18n: LocalizerType;
searchTerm: string;
isSecondaryDevice: boolean;
// Action Creators
startNewConversation: (
@ -30,255 +24,26 @@ export interface Props {
) => void;
openConversationInternal: (id: string, messageId?: string) => void;
showArchivedConversations: () => void;
showInbox: () => void;
// Render Props
renderMainHeader: () => JSX.Element;
updateSearchTerm: (searchTerm: string) => void;
search: (query: string, options: SearchOptions) => void;
clearSearch: () => void;
}
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
type RowRendererParamsType = {
index: number;
isScrolling: boolean;
isVisible: boolean;
key: string;
parent: Object;
style: Object;
};
export class LeftPane extends React.Component<Props> {
export class LeftPane extends React.Component<Props, State> {
public state = {
currentTab: 'conversations',
selectedSection: SectionType.Message,
};
public constructor(props: Props) {
public constructor(props: any) {
super(props);
this.handleSectionSelected = this.handleSectionSelected.bind(this);
}
public getCurrentConversations():
| Array<ConversationListItemPropsType>
| undefined {
const { conversations, friends } = this.props;
const { currentTab } = this.state;
let conversationList =
currentTab === 'conversations' ? conversations : friends;
if (conversationList !== undefined) {
conversationList = conversationList.filter(
conversation => !conversation.isSecondary
);
}
return conversationList;
}
public renderTabs(): JSX.Element {
const { i18n } = this.props;
const { currentTab } = this.state;
const tabs = [
{
id: 'conversations',
name: i18n('conversationsTab'),
},
{
id: 'friends',
name: i18n('friendsTab'),
},
];
return (
<div className="module-left-pane__tabs" key="tabs">
{tabs.map(tab => (
<div
role="button"
className={classNames('tab', tab.id === currentTab && 'selected')}
key={tab.id}
onClick={() => {
this.setState({ currentTab: tab.id });
}}
>
{tab.name}
</div>
))}
</div>
);
}
public renderRow = ({
index,
key,
style,
}: RowRendererParamsType): JSX.Element => {
const {
archivedConversations,
i18n,
openConversationInternal,
showArchived,
} = this.props;
const { currentTab } = this.state;
const conversations = this.getCurrentConversations();
if (!conversations || !archivedConversations) {
throw new Error(
'renderRow: Tried to render without conversations or archivedConversations'
);
}
if (!showArchived && index === conversations.length) {
return this.renderArchivedButton({ key, style });
}
const conversation = showArchived
? archivedConversations[index]
: conversations[index];
return (
<ConversationListItem
key={key}
style={style}
{...conversation}
onClick={openConversationInternal}
i18n={i18n}
isFriendItem={currentTab !== 'conversations'}
/>
);
};
public renderArchivedButton({
key,
style,
}: {
key: string;
style: Object;
}): JSX.Element {
const {
archivedConversations,
i18n,
showArchivedConversations,
} = this.props;
if (!archivedConversations || !archivedConversations.length) {
throw new Error(
'renderArchivedButton: Tried to render without archivedConversations'
);
}
return (
<div
key={key}
className="module-left-pane__archived-button"
style={style}
role="button"
onClick={showArchivedConversations}
>
{i18n('archivedConversations')}{' '}
<span className="module-left-pane__archived-button__archived-count">
{archivedConversations.length}
</span>
</div>
);
}
public renderList(): JSX.Element | Array<JSX.Element | null> {
const {
archivedConversations,
i18n,
openConversationInternal,
startNewConversation,
searchResults,
showArchived,
} = this.props;
if (searchResults) {
return (
<SearchResults
{...searchResults}
openConversation={openConversationInternal}
startNewConversation={startNewConversation}
i18n={i18n}
/>
);
}
const conversations = this.getCurrentConversations();
if (!conversations || !archivedConversations) {
throw new Error(
'render: must provided conversations and archivedConverstions if no search results are provided'
);
}
// That extra 1 element added to the list is the 'archived converastions' button
const length = showArchived
? archivedConversations.length
: conversations.length + (archivedConversations.length ? 1 : 0);
const archived = showArchived ? (
<div className="module-left-pane__archive-helper-text" key={0}>
{i18n('archiveHelperText')}
</div>
) : null;
// We ensure that the listKey differs between inbox and archive views, which ensures
// that AutoSizer properly detects the new size of its slot in the flexbox. The
// archive explainer text at the top of the archive view causes problems otherwise.
// It also ensures that we scroll to the top when switching views.
const listKey = showArchived ? 1 : 0;
// Note: conversations is not a known prop for List, but it is required to ensure that
// it re-renders when our conversation data changes. Otherwise it would just render
// on startup and scroll.
const list = (
<div className="module-left-pane__list" key={listKey}>
<AutoSizer>
{({ height, width }) => (
<List
className="module-left-pane__virtual-list"
conversations={conversations}
height={height}
rowCount={length}
rowHeight={64}
rowRenderer={this.renderRow}
width={width}
autoHeight={true}
/>
)}
</AutoSizer>
</div>
);
return [this.renderTabs(), archived, list];
}
public renderArchivedHeader(): JSX.Element {
const { i18n, showInbox } = this.props;
return (
<div className="module-left-pane__archive-header">
<div
role="button"
onClick={showInbox}
className="module-left-pane__to-inbox-button"
/>
<div className="module-left-pane__archive-header-text">
{i18n('archivedConversations')}
</div>
</div>
);
}
public handleSectionSelected(section: SectionType) {
console.log('section switch to:', section);
this.setState({ selectedSection: section });
}
public render(): JSX.Element {
const { renderMainHeader, showArchived } = this.props;
return (
<div className="module-left-pane-session">
<LeftPaneSections
@ -286,12 +51,38 @@ export class LeftPane extends React.Component<Props> {
onSectionSelected={this.handleSectionSelected}
/>
<div className="module-left-pane">
<div className="module-left-pane__header">
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
</div>
{this.renderList()}
{this.state.selectedSection === SectionType.Message &&
this.renderMessageSection()}
</div>
</div>
);
}
private renderMessageSection() {
const {
startNewConversation,
openConversationInternal,
conversations,
searchResults,
searchTerm,
isSecondaryDevice,
updateSearchTerm,
search,
clearSearch,
} = this.props;
return (
<LeftPaneMessageSection
startNewConversation={startNewConversation}
openConversationInternal={openConversationInternal}
conversations={conversations}
searchResults={searchResults}
searchTerm={searchTerm}
isSecondaryDevice={isSecondaryDevice}
updateSearchTerm={updateSearchTerm}
search={search}
clearSearch={clearSearch}
/>
);
}
}

View File

@ -0,0 +1,234 @@
import React from 'react';
import { AutoSizer, List } from 'react-virtualized';
import {
ConversationListItem,
PropsData as ConversationListItemPropsType,
} from '../ConversationListItem';
import {
PropsData as SearchResultsProps,
SearchResults,
} from '../SearchResults';
import { SessionButton } from './SessionButton';
import { SessionConversationSearch } from './SessionConversationSearch';
import { debounce } from 'lodash';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import { SearchOptions } from '../../types/Search';
export interface Props {
searchTerm: string;
isSecondaryDevice: boolean;
conversations?: Array<ConversationListItemPropsType>;
friends?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps;
// Action Creators
startNewConversation: (
query: string,
options: { regionCode: string }
) => void;
updateSearchTerm: (searchTerm: string) => void;
search: (query: string, options: SearchOptions) => void;
openConversationInternal: (id: string, messageId?: string) => void;
clearSearch: () => void;
}
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
type RowRendererParamsType = {
index: number;
isScrolling: boolean;
isVisible: boolean;
key: string;
parent: Object;
style: Object;
};
export class LeftPaneMessageSection extends React.Component<Props, any> {
private readonly updateSearchBound: (searchedString: string) => void;
private readonly debouncedSearch: (searchTerm: string) => void;
public constructor(props: Props) {
super(props);
this.state = {
showComposeView: false,
};
this.updateSearchBound = this.updateSearch.bind(this);
this.debouncedSearch = debounce(this.search.bind(this), 20);
}
public getCurrentConversations():
| Array<ConversationListItemPropsType>
| undefined {
const { conversations } = this.props;
let conversationList = conversations;
if (conversationList !== undefined) {
conversationList = conversationList.filter(
conversation => !conversation.isSecondary
);
}
return conversationList;
}
public renderRow = ({
index,
key,
style,
}: RowRendererParamsType): JSX.Element => {
const { openConversationInternal } = this.props;
const conversations = this.getCurrentConversations();
if (!conversations) {
throw new Error('renderRow: Tried to render without conversations');
}
const conversation = conversations[index];
return (
<ConversationListItem
key={key}
style={style}
{...conversation}
onClick={openConversationInternal}
i18n={window.i18n}
/>
);
};
public renderList(): JSX.Element | Array<JSX.Element | null> {
const {
openConversationInternal,
startNewConversation,
searchResults,
} = this.props;
if (searchResults) {
return (
<SearchResults
{...searchResults}
openConversation={openConversationInternal}
startNewConversation={startNewConversation}
i18n={window.i18n}
/>
);
}
const conversations = this.getCurrentConversations();
if (!conversations) {
throw new Error(
'render: must provided conversations if no search results are provided'
);
}
// That extra 1 element added to the list is the 'archived conversations' button
const length = conversations.length;
// We ensure that the listKey differs between inbox and archive views, which ensures
// that AutoSizer properly detects the new size of its slot in the flexbox. The
// archive explainer text at the top of the archive view causes problems otherwise.
// It also ensures that we scroll to the top when switching views.
const listKey = 0;
// Note: conversations is not a known prop for List, but it is required to ensure that
// it re-renders when our conversation data changes. Otherwise it would just render
// on startup and scroll.
const list = (
<div className="module-left-pane__list" key={listKey}>
<AutoSizer>
{({ height, width }) => (
<List
className="module-left-pane__virtual-list"
conversations={conversations}
height={height}
rowCount={length}
rowHeight={64}
rowRenderer={this.renderRow}
width={width}
autoHeight={true}
/>
)}
</AutoSizer>
</div>
);
return [list];
}
public renderHeader(): JSX.Element {
return (
<div className="module-left-pane__header">
<div className="module-left-pane__title">
{window.i18n('messagesHeader')}
</div>
<SessionButton text={window.i18n('compose')} />
</div>
);
}
public render(): JSX.Element {
return (
<div className="module-left-pane-session">
<div className="module-left-pane">
{this.renderHeader()}
<SessionConversationSearch
searchString={this.props.searchTerm}
onChange={this.updateSearchBound}
/>
{this.renderList()}
</div>
</div>
);
}
public updateSearch(searchTerm: string) {
const { updateSearchTerm, clearSearch } = this.props;
if (!searchTerm) {
clearSearch();
return;
}
if (updateSearchTerm) {
updateSearchTerm(searchTerm);
}
if (searchTerm.length < 2) {
return;
}
const cleanedTerm = cleanSearchTerm(searchTerm);
if (!cleanedTerm) {
return;
}
this.debouncedSearch(cleanedTerm);
}
public clearSearch() {
const { clearSearch } = this.props;
clearSearch();
//this.setFocus();
}
public search() {
const { search } = this.props;
const { searchTerm, isSecondaryDevice } = this.props;
if (search) {
search(searchTerm, {
noteToSelf: window.i18n('noteToSelf').toLowerCase(),
ourNumber: window.textsecure.storage.user.getNumber(),
regionCode: '',
isSecondaryDevice,
});
}
}
}

View File

@ -1,10 +1,6 @@
import React from 'react';
import {
SessionIconButton,
SessionIconSize,
SessionIconType,
} from './session/icon';
import { Avatar } from './Avatar';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
import { Avatar } from '../Avatar';
export enum SectionType {
Profile,

View File

@ -0,0 +1,30 @@
import React from 'react';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
interface Props {
searchString: string;
onChange: any;
}
export class SessionConversationSearch extends React.Component<Props> {
public constructor(props: Props) {
super(props);
}
public render() {
const { searchString } = this.props;
return (
<div className="session-search-input">
<SessionIconButton
iconSize={SessionIconSize.Medium}
iconType={SessionIconType.Search}
/>
<input
value={searchString}
onChange={e => this.props.onChange(e.target.value)}
/>
</div>
);
}
}

View File

@ -1,18 +1,19 @@
import React from 'react';
import { connect } from 'react-redux';
import { mapDispatchToProps } from '../actions';
import { LeftPane } from '../../components/LeftPane';
import { StateType } from '../reducer';
import { getSearchResults, isSearching } from '../selectors/search';
import { getIntl } from '../selectors/user';
import { getQuery, getSearchResults, isSearching } from '../selectors/search';
import {
getIntl,
getIsSecondaryDevice,
getRegionCode,
getUserNumber,
} from '../selectors/user';
import { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
import { SmartMainHeader } from './MainHeader';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
const FilteredSmartMainHeader = SmartMainHeader as any;
const mapStateToProps = (state: StateType) => {
const showSearch = isSearching(state);
@ -22,10 +23,13 @@ const mapStateToProps = (state: StateType) => {
return {
...lists,
searchTerm: getQuery(state),
regionCode: getRegionCode(state),
ourNumber: getUserNumber(state),
isSecondaryDevice: getIsSecondaryDevice(state),
searchResults,
showArchived: getShowArchived(state),
i18n: getIntl(state),
renderMainHeader: () => <FilteredSmartMainHeader />,
};
};