Archive Conversation

This commit is contained in:
Scott Nonnenberg 2019-03-11 17:20:16 -07:00
parent d72f89d776
commit 6ffbc0ac06
20 changed files with 568 additions and 109 deletions

View File

@ -187,6 +187,27 @@
}
}
},
"archivedConversations": {
"message": "Archived Conversations",
"description":
"Shown in place of the search box when showing archived conversation list"
},
"archiveHelperText": {
"message":
"These conversations are archived and will only appear in the Inbox if new messages are received.",
"description":
"Shown at the top of the archived converations list in the left pane"
},
"archiveConversation": {
"message": "Archive Conversation",
"description":
"Shown in menu for conversation, and moves conversation out of main conversation list"
},
"moveConversationToInbox": {
"message": "Move Converstion to Inbox",
"description":
"Undoes Archive Conversation action, and moves archived conversation back to the main conversation list"
},
"chooseDirectory": {
"message": "Choose folder",
"description": "Button to allow the user to find a folder on disk"

View File

@ -312,6 +312,7 @@
const result = {
id: this.id,
isArchived: this.get('isArchived'),
activeAt: this.get('active_at'),
avatarPath: this.getAvatarPath(),
color,
@ -889,6 +890,7 @@
lastMessageStatus: 'sending',
active_at: now,
timestamp: now,
isArchived: false,
});
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
@ -1170,6 +1172,13 @@
}
},
async setArchived(isArchived) {
this.set({ isArchived });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
},
async updateExpirationTimer(
providedExpireTimer,
providedSource,

View File

@ -1645,10 +1645,10 @@
c.onReadMessage(message);
}
} else {
conversation.set(
'unreadCount',
conversation.get('unreadCount') + 1
);
conversation.set({
unreadCount: conversation.get('unreadCount') + 1,
isArchived: false,
});
}
}

View File

@ -1,7 +1,6 @@
/* global Signal:false */
/* global Backbone: false */
/* global ConversationController: false */
/* global drawAttention: false */
/* global i18n: false */
/* global isFocused: false */

View File

@ -185,9 +185,12 @@
profileName: this.model.getProfileName(),
color: this.model.getColor(),
avatarPath: this.model.getAvatarPath(),
isVerified: this.model.isVerified(),
isMe: this.model.isMe(),
isGroup: !this.model.isPrivate(),
isArchived: this.model.get('isArchived'),
expirationSettingName,
showBackButton: Boolean(this.panels && this.panels.length),
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
@ -217,6 +220,14 @@
this.resetPanel();
this.updateHeader();
},
onArchive: () => {
this.unload();
this.model.setArchived(true);
},
onMoveToInbox: () => {
this.model.setArchived(false);
},
};
};
this.titleView = new Whisper.ReactWrapperView({

View File

@ -220,7 +220,7 @@
window.location.reload();
},
async openConversation(id, messageId) {
const conversation = await window.ConversationController.getOrCreateAndWait(
const conversation = await ConversationController.getOrCreateAndWait(
id,
'private'
);

View File

@ -2986,15 +2986,74 @@
flex-grow: 0;
}
.module-left-pane__archive-header {
height: 48px;
width: 100%;
display: inline-flex;
flex-direction: row;
align-items: center;
border-bottom: 1px solid $color-gray-15;
}
.module-left-pane__to-inbox-button {
margin-left: 2px;
width: 35px;
height: 35px;
cursor: pointer;
@include color-svg('../images/back.svg', $color-gray-60);
}
.module-left-pane__archive-header-text {
color: $color-gray-90;
font-size: 16px;
font-weight: 300px;
}
.module-left-pane__list {
flex-grow: 1;
flex-shrink: 1;
}
.module-left-pane__archive-helper-text {
padding: 1em;
font-size: 12px;
color: $color-gray-60;
background-color: $color-gray-05;
}
.module-left-pane__virtual-list {
outline: none;
}
.module-left-pane__archived-button {
font-size: 14px;
height: 64px;
line-height: 64px;
text-align: center;
font-weight: 300;
color: $color-gray-60;
cursor: pointer;
&:hover {
background-color: $color-gray-05;
}
}
.module-left-pane__archived-button__archived-count {
font-size: 12px;
font-weight: 300;
color: $color-gray-60;
background-color: $color-gray-05;
padding: 6px;
padding-top: 1px;
padding-bottom: 1px;
border-radius: 10px;
}
// Module: Start New Conversation
.module-start-new-conversation {

View File

@ -1346,7 +1346,7 @@ body.dark-theme {
}
.module-main-header__search__cancel-icon {
@include color-svg('../images/x.svg', $color-gray-25);
@include color-svg('../images/x-16.svg', $color-gray-25);
}
// Module: Image
@ -1382,7 +1382,7 @@ body.dark-theme {
}
.module-attachments__close-button {
@include color-svg('../images/x.svg', $color-gray-45);
@include color-svg('../images/x-16.svg', $color-gray-45);
}
// Module: Staged Generic Attachment
@ -1482,7 +1482,7 @@ body.dark-theme {
}
.module-caption-editor__close-button {
@include color-svg('../images/x.svg', $color-white);
@include color-svg('../images/x-16.svg', $color-white);
}
.module-caption-editor__media-container {
@ -1553,6 +1553,35 @@ body.dark-theme {
border-right: 1px solid $color-gray-75;
}
.module-left-pane__archive-header {
border-bottom: 1px solid $color-gray-75;
}
.module-left-pane__to-inbox-button {
background-color: $color-gray-25;
}
.module-left-pane__archive-header-text {
color: $color-gray-05;
}
.module-left-pane__archive-helper-text {
color: $color-gray-25;
background-color: $color-gray-75;
}
.module-left-pane__archived-button {
color: $color-gray-25;
&:hover {
background-color: $color-gray-75;
}
}
.module-left-pane__archived-button__archived-count {
color: $color-gray-25;
background-color: $color-gray-75;
}
// Module: Start New Conversation
.module-start-new-conversation {

View File

@ -129,8 +129,14 @@ window.searchResults.messages = [
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
<LeftPane
searchResults={window.searchResults}
openConversation={result => console.log('openConversation', result)}
openMessage={result => console.log('onClickMessage', result)}
startNewConversation={(query, options) =>
console.log('startNewConversation', query, options)
}
openConversationInternal={(id, messageId) =>
console.log('openConversation', id, messageId)
}
showArchivedConversations={() => console.log('showArchivedConversations')}
showInbox={() => console.log('showInbox')}
renderMainHeader={() => (
<MainHeader
searchTerm="Hi there!"
@ -151,8 +157,74 @@ window.searchResults.messages = [
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
<LeftPane
conversations={window.searchResults.conversations}
openConversation={result => console.log('openConversation', result)}
openMessage={result => console.log('onClickMessage', result)}
archivedConversations={[]}
startNewConversation={(query, options) =>
console.log('startNewConversation', query, options)
}
openConversationInternal={(id, messageId) =>
console.log('openConversation', id, messageId)
}
showArchivedConversations={() => console.log('showArchivedConversations')}
showInbox={() => console.log('showInbox')}
renderMainHeader={() => (
<MainHeader
searchTerm="Hi there!"
search={result => console.log('search', result)}
updateSearch={result => console.log('updateSearch', result)}
clearSearch={result => console.log('clearSearch', result)}
i18n={util.i18n}
/>
)}
i18n={util.i18n}
/>
</util.LeftPaneContext>
```
#### Showing inbox, with some archived
```jsx
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
<LeftPane
conversations={window.searchResults.conversations.slice(0, 2)}
archivedConversations={window.searchResults.conversations.slice(2)}
startNewConversation={(query, options) =>
console.log('startNewConversation', query, options)
}
openConversationInternal={(id, messageId) =>
console.log('openConversation', id, messageId)
}
showArchivedConversations={() => console.log('showArchivedConversations')}
showInbox={() => console.log('showInbox')}
renderMainHeader={() => (
<MainHeader
searchTerm="Hi there!"
search={result => console.log('search', result)}
updateSearch={result => console.log('updateSearch', result)}
clearSearch={result => console.log('clearSearch', result)}
i18n={util.i18n}
/>
)}
i18n={util.i18n}
/>
</util.LeftPaneContext>
```
#### Showing archived conversations
```jsx
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
<LeftPane
conversations={window.searchResults.conversations.slice(0, 2)}
archivedConversations={window.searchResults.conversations.slice(2)}
showArchived={true}
startNewConversation={(query, options) =>
console.log('startNewConversation', query, options)
}
openConversationInternal={(id, messageId) =>
console.log('openConversation', id, messageId)
}
showArchivedConversations={() => console.log('showArchivedConversations')}
showInbox={() => console.log('showInbox')}
renderMainHeader={() => (
<MainHeader
searchTerm="Hi there!"

View File

@ -13,19 +13,27 @@ import { LocalizerType } from '../types/Util';
export interface Props {
conversations?: Array<ConversationListItemPropsType>;
archivedConversations?: Array<ConversationListItemPropsType>;
searchResults?: SearchResultsProps;
showArchived?: boolean;
i18n: LocalizerType;
// Action Creators
startNewConversation: () => void;
startNewConversation: (
query: string,
options: { regionCode: string }
) => void;
openConversationInternal: (id: string, messageId?: string) => void;
showArchivedConversations: () => void;
showInbox: () => void;
// Render Props
renderMainHeader: () => JSX.Element;
}
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
type RowRendererParams = {
type RowRendererParamsType = {
index: number;
isScrolling: boolean;
isVisible: boolean;
@ -35,12 +43,51 @@ type RowRendererParams = {
};
export class LeftPane extends React.Component<Props> {
public renderRow = ({ index, key, style }: RowRendererParams) => {
const { conversations, i18n, openConversationInternal } = this.props;
if (!conversations) {
return null;
public listRef: React.RefObject<any> = React.createRef();
public scrollToTop() {
if (this.listRef && this.listRef.current) {
const { current } = this.listRef;
current.scrollToRow(0);
}
const conversation = conversations[index];
}
public componentDidUpdate(prevProps: Props) {
const { showArchived, searchResults } = this.props;
const isNotShowingSearchResults = !searchResults;
const hasArchiveViewChanged = showArchived !== prevProps.showArchived;
if (isNotShowingSearchResults && hasArchiveViewChanged) {
this.scrollToTop();
}
}
public renderRow = ({
index,
key,
style,
}: RowRendererParamsType): JSX.Element => {
const {
archivedConversations,
conversations,
i18n,
openConversationInternal,
showArchived,
} = this.props;
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
@ -53,13 +100,50 @@ export class LeftPane extends React.Component<Props> {
);
};
public renderList() {
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 {
const {
archivedConversations,
i18n,
conversations,
openConversationInternal,
startNewConversation,
searchResults,
showArchived,
} = this.props;
if (searchResults) {
@ -73,22 +157,35 @@ export class LeftPane extends React.Component<Props> {
);
}
if (!conversations || !conversations.length) {
return null;
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);
// 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.
return (
<div className="module-left-pane__list">
{showArchived ? (
<div className="module-left-pane__archive-helper-text">
{i18n('archiveHelperText')}
</div>
) : null}
<AutoSizer>
{({ height, width }) => (
<List
className="module-left-pane__virtual-list"
ref={this.listRef}
conversations={conversations}
height={height}
rowCount={conversations.length}
rowCount={length}
rowHeight={64}
rowRenderer={this.renderRow}
width={width}
@ -99,12 +196,31 @@ export class LeftPane extends React.Component<Props> {
);
}
public render() {
const { renderMainHeader } = this.props;
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 render(): JSX.Element {
const { renderMainHeader, showArchived } = this.props;
return (
<div className="module-left-pane">
<div className="module-left-pane__header">{renderMainHeader()}</div>
<div className="module-left-pane__header">
{showArchived ? this.renderArchivedHeader() : renderMainHeader()}
</div>
{this.renderList()}
</div>
);

View File

@ -113,7 +113,9 @@ window.searchResults.messages = [
i18n={util.i18n}
onClickMessage={id => console.log('onClickMessage', id)}
onClickConversation={id => console.log('onClickConversation', id)}
onStartNewConversation={() => console.log('onStartNewConversation')}
onStartNewConversation={(query, options) =>
console.log('onStartNewConversation', query, options)
}
/>
</util.LeftPaneContext>;
```
@ -131,7 +133,9 @@ window.searchResults.messages = [
i18n={util.i18n}
onClickMessage={id => console.log('onClickMessage', id)}
onClickConversation={id => console.log('onClickConversation', id)}
onStartNewConversation={() => console.log('onStartNewConversation')}
onStartNewConversation={(query, options) =>
console.log('onStartNewConversation', query, options)
}
/>
</util.LeftPaneContext>
```
@ -147,7 +151,9 @@ window.searchResults.messages = [
i18n={util.i18n}
onClickMessage={id => console.log('onClickMessage', id)}
onClickConversation={id => console.log('onClickConversation', id)}
onStartNewConversation={() => console.log('onStartNewConversation')}
onStartNewConversation={(query, options) =>
console.log('onStartNewConversation', query, options)
}
/>
</util.LeftPaneContext>
```
@ -163,7 +169,9 @@ window.searchResults.messages = [
i18n={util.i18n}
onClickMessage={id => console.log('onClickMessage', id)}
onClickConversation={id => console.log('onClickConversation', id)}
onStartNewConversation={() => console.log('onStartNewConversation')}
onStartNewConversation={(query, options) =>
console.log('onStartNewConversation', query, options)
}
/>
</util.LeftPaneContext>
```

View File

@ -16,6 +16,7 @@ export type PropsData = {
conversations: Array<ConversationListItemPropsType>;
hideMessagesHeader: boolean;
messages: Array<MessageSearchResultPropsType>;
regionCode: string;
searchTerm: string;
showStartNewConversation: boolean;
};
@ -23,12 +24,21 @@ export type PropsData = {
type PropsHousekeeping = {
i18n: LocalizerType;
openConversation: (id: string, messageId?: string) => void;
startNewConversation: (id: string) => void;
startNewConversation: (
query: string,
options: { regionCode: string }
) => void;
};
type Props = PropsData & PropsHousekeeping;
export class SearchResults extends React.Component<Props> {
public handleStartNewConversation = () => {
const { regionCode, searchTerm, startNewConversation } = this.props;
startNewConversation(searchTerm, { regionCode });
};
public render() {
const {
conversations,
@ -37,7 +47,6 @@ export class SearchResults extends React.Component<Props> {
i18n,
messages,
openConversation,
startNewConversation,
searchTerm,
showStartNewConversation,
} = this.props;
@ -62,7 +71,7 @@ export class SearchResults extends React.Component<Props> {
<StartNewConversation
phoneNumber={searchTerm}
i18n={i18n}
onClick={startNewConversation}
onClick={this.handleStartNewConversation}
/>
) : null}
{haveConversations ? (

View File

@ -7,7 +7,7 @@ import { LocalizerType } from '../types/Util';
export interface Props {
phoneNumber: string;
i18n: LocalizerType;
onClick: (id: string) => void;
onClick: () => void;
}
export class StartNewConversation extends React.PureComponent<Props> {
@ -18,9 +18,7 @@ export class StartNewConversation extends React.PureComponent<Props> {
<div
role="button"
className="module-start-new-conversation"
onClick={() => {
onClick(phoneNumber);
}}
onClick={onClick}
>
<Avatar
color="grey"

View File

@ -16,17 +16,19 @@ interface TimerOption {
}
interface Props {
i18n: LocalizerType;
isVerified: boolean;
name?: string;
id: string;
name?: string;
phoneNumber: string;
profileName?: string;
color: string;
avatarPath?: string;
isVerified: boolean;
isMe: boolean;
isGroup: boolean;
isArchived: boolean;
expirationSettingName?: string;
showBackButton: boolean;
timerOptions: Array<TimerOption>;
@ -39,6 +41,11 @@ interface Props {
onShowAllMedia: () => void;
onShowGroupMembers: () => void;
onGoBack: () => void;
onArchive: () => void;
onMoveToInbox: () => void;
i18n: LocalizerType;
}
export class ConversationHeader extends React.Component<Props> {
@ -184,12 +191,15 @@ export class ConversationHeader extends React.Component<Props> {
i18n,
isMe,
isGroup,
isArchived,
onDeleteMessages,
onResetSession,
onSetDisappearingMessages,
onShowAllMedia,
onShowGroupMembers,
onShowSafetyNumber,
onArchive,
onMoveToInbox,
timerOptions,
} = this.props;
@ -223,6 +233,13 @@ export class ConversationHeader extends React.Component<Props> {
{!isGroup ? (
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
) : null}
{isArchived ? (
<MenuItem onClick={onMoveToInbox}>
{i18n('moveConversationToInbox')}
</MenuItem>
) : (
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
)}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
</ContextMenu>
);

View File

@ -34,6 +34,7 @@ export type MessageType = {
export type ConversationType = {
id: string;
name?: string;
isArchived: boolean;
activeAt?: number;
timestamp: number;
lastMessage?: {
@ -55,6 +56,7 @@ export type ConversationLookupType = {
export type ConversationsStateType = {
conversationLookup: ConversationLookupType;
selectedConversation?: string;
showArchived: boolean;
};
// Actions
@ -97,6 +99,14 @@ export type SelectedConversationChangedActionType = {
messageId?: string;
};
};
type ShowInboxActionType = {
type: 'SHOW_INBOX';
payload: null;
};
type ShowArchivedConversationsActionType = {
type: 'SHOW_ARCHIVED_CONVERSATIONS';
payload: null;
};
export type ConversationActionType =
| ConversationAddedActionType
@ -104,7 +114,11 @@ export type ConversationActionType =
| ConversationRemovedActionType
| RemoveAllConversationsActionType
| MessageExpiredActionType
| SelectedConversationChangedActionType;
| SelectedConversationChangedActionType
| MessageExpiredActionType
| SelectedConversationChangedActionType
| ShowInboxActionType
| ShowArchivedConversationsActionType;
// Action Creators
@ -116,6 +130,8 @@ export const actions = {
messageExpired,
openConversationInternal,
openConversationExternal,
showInbox,
showArchivedConversations,
};
function conversationAdded(
@ -156,6 +172,7 @@ function removeAllConversations(): RemoveAllConversationsActionType {
payload: null,
};
}
function messageExpired(
id: string,
conversationId: string
@ -196,11 +213,25 @@ function openConversationExternal(
};
}
function showInbox() {
return {
type: 'SHOW_INBOX',
payload: null,
};
}
function showArchivedConversations() {
return {
type: 'SHOW_ARCHIVED_CONVERSATIONS',
payload: null,
};
}
// Reducer
function getEmptyState(): ConversationsStateType {
return {
conversationLookup: {},
showArchived: false,
};
}
@ -225,27 +256,38 @@ export function reducer(
},
};
}
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action;
const { id } = payload;
return {
...state,
selectedConversation: id,
};
}
if (action.type === 'CONVERSATION_CHANGED') {
const { payload } = action;
const { id, data } = payload;
const { conversationLookup } = state;
let showArchived = state.showArchived;
let selectedConversation = state.selectedConversation;
const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation
if (!conversationLookup[id]) {
if (!existing) {
return state;
}
if (selectedConversation === id) {
// Archived -> Inbox: we go back to the normal inbox view
if (existing.isArchived && !data.isArchived) {
showArchived = false;
}
// Inbox -> Archived: no conversation is selected
// Note: With today's stacked converastions architecture, this can result in weird
// behavior - no selected conversation in the left pane, but a conversation show
// in the right pane.
if (!existing.isArchived && data.isArchived) {
selectedConversation = undefined;
}
}
return {
...state,
selectedConversation,
showArchived,
conversationLookup: {
...conversationLookup,
[id]: data,
@ -268,6 +310,27 @@ export function reducer(
if (action.type === 'MESSAGE_EXPIRED') {
// noop - for now this is only important for search
}
if (action.type === 'SELECTED_CONVERSATION_CHANGED') {
const { payload } = action;
const { id } = payload;
return {
...state,
selectedConversation: id,
};
}
if (action.type === 'SHOW_INBOX') {
return {
...state,
showArchived: false,
};
}
if (action.type === 'SHOW_ARCHIVED_CONVERSATIONS') {
return {
...state,
showArchived: true,
};
}
return state;
}

View File

@ -1,4 +1,3 @@
import { compact } from 'lodash';
import { createSelector } from 'reselect';
import { format } from '../../types/PhoneNumber';
@ -29,6 +28,13 @@ export const getSelectedConversation = createSelector(
}
);
export const getShowArchived = createSelector(
getConversations,
(state: ConversationsStateType): boolean => {
return Boolean(state.showArchived);
}
);
function getConversationTitle(
conversation: ConversationType,
options: { i18n: LocalizerType; ourRegionCode: string }
@ -83,37 +89,49 @@ export const getConversationComparator = createSelector(
_getConversationComparator
);
export const _getLeftPaneList = (
export const _getLeftPaneLists = (
lookup: ConversationLookupType,
comparator: (left: ConversationType, right: ConversationType) => number,
selectedConversation?: string
): Array<ConversationType> => {
): {
conversations: Array<ConversationType>;
archivedConversations: Array<ConversationType>;
} => {
const values = Object.values(lookup);
const filtered = compact(
values.map(conversation => {
if (!conversation.activeAt) {
return null;
}
const sorted = values.sort(comparator);
if (selectedConversation === conversation.id) {
return {
...conversation,
isSelected: true,
};
}
const conversations: Array<ConversationType> = [];
const archivedConversations: Array<ConversationType> = [];
return conversation;
})
);
const max = sorted.length;
for (let i = 0; i < max; i += 1) {
let conversation = sorted[i];
if (!conversation.activeAt) {
continue;
}
return filtered.sort(comparator);
if (selectedConversation === conversation.id) {
conversation = {
...conversation,
isSelected: true,
};
}
if (conversation.isArchived) {
archivedConversations.push(conversation);
} else {
conversations.push(conversation);
}
}
return { conversations, archivedConversations };
};
export const getLeftPaneList = createSelector(
export const getLeftPaneLists = createSelector(
getConversationLookup,
getConversationComparator,
getSelectedConversation,
_getLeftPaneList
_getLeftPaneLists
);
export const getMe = createSelector(

View File

@ -2,14 +2,16 @@ import { compact } from 'lodash';
import { createSelector } from 'reselect';
import { StateType } from '../reducer';
import { SearchStateType } from '../ducks/search';
import { SearchStateType } from '../ducks/search';
import {
getConversationLookup,
getSelectedConversation,
} from './conversations';
import { ConversationLookupType } from '../ducks/conversations';
import { getRegionCode } from './user';
export const getSearch = (state: StateType): SearchStateType => state.search;
export const getQuery = createSelector(
@ -34,12 +36,14 @@ export const isSearching = createSelector(
export const getSearchResults = createSelector(
[
getSearch,
getRegionCode,
getConversationLookup,
getSelectedConversation,
getSelectedMessage,
],
(
state: SearchStateType,
regionCode: string,
lookup: ConversationLookupType,
selectedConversation?: string,
selectedMessage?: string
@ -84,6 +88,7 @@ export const getSearchResults = createSelector(
return message;
}),
regionCode: regionCode,
searchTerm: state.query,
showStartNewConversation: Boolean(
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]

View File

@ -4,9 +4,9 @@ import { mapDispatchToProps } from '../actions';
import { LeftPane } from '../../components/LeftPane';
import { StateType } from '../reducer';
import { getQuery, getSearchResults, isSearching } from '../selectors/search';
import { getSearchResults, isSearching } from '../selectors/search';
import { getIntl } from '../selectors/user';
import { getLeftPaneList, getMe } from '../selectors/conversations';
import { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
import { SmartMainHeader } from './MainHeader';
@ -17,12 +17,14 @@ const FilteredSmartMainHeader = SmartMainHeader as any;
const mapStateToProps = (state: StateType) => {
const showSearch = isSearching(state);
const lists = showSearch ? undefined : getLeftPaneLists(state);
const searchResults = showSearch ? getSearchResults(state) : undefined;
return {
...lists,
searchResults,
showArchived: getShowArchived(state),
i18n: getIntl(state),
me: getMe(state),
query: getQuery(state),
conversations: showSearch ? undefined : getLeftPaneList(state),
searchResults: showSearch ? getSearchResults(state) : undefined,
renderMainHeader: () => <FilteredSmartMainHeader />,
};
};

View File

@ -3,7 +3,7 @@ import { assert } from 'chai';
import { ConversationLookupType } from '../../../state/ducks/conversations';
import {
_getConversationComparator,
_getLeftPaneList,
_getLeftPaneLists,
} from '../../../state/selectors/conversations';
describe('state/selectors/conversations', () => {
@ -11,13 +11,14 @@ describe('state/selectors/conversations', () => {
it('sorts conversations based on timestamp then by intl-friendly title', () => {
const i18n = (key: string) => key;
const regionCode = 'US';
const conversations: ConversationLookupType = {
const data: ConversationLookupType = {
id1: {
id: 'id1',
activeAt: Date.now(),
name: 'No timestamp',
timestamp: 0,
phoneNumber: 'notused',
isArchived: false,
type: 'direct',
isMe: false,
@ -32,6 +33,7 @@ describe('state/selectors/conversations', () => {
name: 'B',
timestamp: 20,
phoneNumber: 'notused',
isArchived: false,
type: 'direct',
isMe: false,
@ -46,6 +48,7 @@ describe('state/selectors/conversations', () => {
name: 'C',
timestamp: 20,
phoneNumber: 'notused',
isArchived: false,
type: 'direct',
isMe: false,
@ -60,6 +63,7 @@ describe('state/selectors/conversations', () => {
name: 'Á',
timestamp: 20,
phoneNumber: 'notused',
isArchived: false,
type: 'direct',
isMe: false,
@ -74,6 +78,7 @@ describe('state/selectors/conversations', () => {
name: 'First!',
timestamp: 30,
phoneNumber: 'notused',
isArchived: false,
type: 'direct',
isMe: false,
@ -84,13 +89,13 @@ describe('state/selectors/conversations', () => {
},
};
const comparator = _getConversationComparator(i18n, regionCode);
const list = _getLeftPaneList(conversations, comparator);
const { conversations } = _getLeftPaneLists(data, comparator);
assert.strictEqual(list[0].name, 'First!');
assert.strictEqual(list[1].name, 'Á');
assert.strictEqual(list[2].name, 'B');
assert.strictEqual(list[3].name, 'C');
assert.strictEqual(list[4].name, 'No timestamp');
assert.strictEqual(conversations[0].name, 'First!');
assert.strictEqual(conversations[1].name, 'Á');
assert.strictEqual(conversations[2].name, 'B');
assert.strictEqual(conversations[3].name, 'C');
assert.strictEqual(conversations[4].name, 'No timestamp');
});
});
});

View File

@ -164,7 +164,7 @@
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " async load() {",
"lineNumber": 179,
"lineNumber": 177,
"reasonCategory": "falseMatch",
"updated": "2018-10-02T21:00:44.007Z"
},
@ -172,7 +172,7 @@
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 214,
"lineNumber": 212,
"reasonCategory": "falseMatch",
"updated": "2018-10-02T21:00:44.007Z"
},
@ -562,7 +562,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " .append(this.networkStatusView.render().el);",
"lineNumber": 89,
"lineNumber": 88,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -571,7 +571,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " banner.$el.prependTo(this.$el);",
"lineNumber": 93,
"lineNumber": 92,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -580,7 +580,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 164,
"lineNumber": 166,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -589,7 +589,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 164,
"lineNumber": 166,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -598,7 +598,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
"lineNumber": 205,
"lineNumber": 207,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -607,7 +607,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('#header, .gutter').addClass('inactive');",
"lineNumber": 209,
"lineNumber": 211,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -616,25 +616,25 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation-stack').addClass('inactive');",
"lineNumber": 213,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 215,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 217,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
},
{
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"lineNumber": 230,
"lineNumber": 236,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -643,7 +643,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"lineNumber": 233,
"lineNumber": 239,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -5464,6 +5464,24 @@
"updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used only to set focus"
},
{
"rule": "React-createRef",
"path": "ts/components/LeftPane.js",
"line": " this.listRef = react_1.default.createRef();",
"lineNumber": 13,
"reasonCategory": "usageTrusted",
"updated": "2019-03-12T23:33:50.889Z",
"reasonDetail": "Used only to scroll to top on archive/inbox switch"
},
{
"rule": "React-createRef",
"path": "ts/components/LeftPane.tsx",
"line": " public listRef: React.RefObject<any> = React.createRef();",
"lineNumber": 46,
"reasonCategory": "usageTrusted",
"updated": "2019-03-12T23:33:50.889Z",
"reasonDetail": "Used only to scroll to top on archive/inbox switch"
},
{
"rule": "React-createRef",
"path": "ts/components/Lightbox.js",
@ -5513,9 +5531,9 @@
"rule": "React-createRef",
"path": "ts/components/conversation/ConversationHeader.tsx",
"line": " this.menuTriggerRef = React.createRef();",
"lineNumber": 51,
"lineNumber": 58,
"reasonCategory": "usageTrusted",
"updated": "2019-03-09T00:08:44.242Z",
"reasonDetail": "Used only to trigger menu display"
}
]
]