Archive Conversation
This commit is contained in:
parent
d72f89d776
commit
6ffbc0ac06
20 changed files with 568 additions and 109 deletions
|
@ -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": {
|
"chooseDirectory": {
|
||||||
"message": "Choose folder",
|
"message": "Choose folder",
|
||||||
"description": "Button to allow the user to find a folder on disk"
|
"description": "Button to allow the user to find a folder on disk"
|
||||||
|
|
|
@ -312,6 +312,7 @@
|
||||||
const result = {
|
const result = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
|
|
||||||
|
isArchived: this.get('isArchived'),
|
||||||
activeAt: this.get('active_at'),
|
activeAt: this.get('active_at'),
|
||||||
avatarPath: this.getAvatarPath(),
|
avatarPath: this.getAvatarPath(),
|
||||||
color,
|
color,
|
||||||
|
@ -889,6 +890,7 @@
|
||||||
lastMessageStatus: 'sending',
|
lastMessageStatus: 'sending',
|
||||||
active_at: now,
|
active_at: now,
|
||||||
timestamp: now,
|
timestamp: now,
|
||||||
|
isArchived: false,
|
||||||
});
|
});
|
||||||
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
await window.Signal.Data.updateConversation(this.id, this.attributes, {
|
||||||
Conversation: Whisper.Conversation,
|
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(
|
async updateExpirationTimer(
|
||||||
providedExpireTimer,
|
providedExpireTimer,
|
||||||
providedSource,
|
providedSource,
|
||||||
|
|
|
@ -1645,10 +1645,10 @@
|
||||||
c.onReadMessage(message);
|
c.onReadMessage(message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
conversation.set(
|
conversation.set({
|
||||||
'unreadCount',
|
unreadCount: conversation.get('unreadCount') + 1,
|
||||||
conversation.get('unreadCount') + 1
|
isArchived: false,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
/* global Signal:false */
|
/* global Signal:false */
|
||||||
/* global Backbone: false */
|
/* global Backbone: false */
|
||||||
|
|
||||||
/* global ConversationController: false */
|
|
||||||
/* global drawAttention: false */
|
/* global drawAttention: false */
|
||||||
/* global i18n: false */
|
/* global i18n: false */
|
||||||
/* global isFocused: false */
|
/* global isFocused: false */
|
||||||
|
|
|
@ -185,9 +185,12 @@
|
||||||
profileName: this.model.getProfileName(),
|
profileName: this.model.getProfileName(),
|
||||||
color: this.model.getColor(),
|
color: this.model.getColor(),
|
||||||
avatarPath: this.model.getAvatarPath(),
|
avatarPath: this.model.getAvatarPath(),
|
||||||
|
|
||||||
isVerified: this.model.isVerified(),
|
isVerified: this.model.isVerified(),
|
||||||
isMe: this.model.isMe(),
|
isMe: this.model.isMe(),
|
||||||
isGroup: !this.model.isPrivate(),
|
isGroup: !this.model.isPrivate(),
|
||||||
|
isArchived: this.model.get('isArchived'),
|
||||||
|
|
||||||
expirationSettingName,
|
expirationSettingName,
|
||||||
showBackButton: Boolean(this.panels && this.panels.length),
|
showBackButton: Boolean(this.panels && this.panels.length),
|
||||||
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
|
timerOptions: Whisper.ExpirationTimerOptions.map(item => ({
|
||||||
|
@ -217,6 +220,14 @@
|
||||||
this.resetPanel();
|
this.resetPanel();
|
||||||
this.updateHeader();
|
this.updateHeader();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onArchive: () => {
|
||||||
|
this.unload();
|
||||||
|
this.model.setArchived(true);
|
||||||
|
},
|
||||||
|
onMoveToInbox: () => {
|
||||||
|
this.model.setArchived(false);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
this.titleView = new Whisper.ReactWrapperView({
|
this.titleView = new Whisper.ReactWrapperView({
|
||||||
|
|
|
@ -220,7 +220,7 @@
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
},
|
},
|
||||||
async openConversation(id, messageId) {
|
async openConversation(id, messageId) {
|
||||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
const conversation = await ConversationController.getOrCreateAndWait(
|
||||||
id,
|
id,
|
||||||
'private'
|
'private'
|
||||||
);
|
);
|
||||||
|
|
|
@ -2986,15 +2986,74 @@
|
||||||
flex-grow: 0;
|
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 {
|
.module-left-pane__list {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 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 {
|
.module-left-pane__virtual-list {
|
||||||
outline: none;
|
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
|
||||||
|
|
||||||
.module-start-new-conversation {
|
.module-start-new-conversation {
|
||||||
|
|
|
@ -1346,7 +1346,7 @@ body.dark-theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-main-header__search__cancel-icon {
|
.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
|
// Module: Image
|
||||||
|
@ -1382,7 +1382,7 @@ body.dark-theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-attachments__close-button {
|
.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
|
// Module: Staged Generic Attachment
|
||||||
|
@ -1482,7 +1482,7 @@ body.dark-theme {
|
||||||
}
|
}
|
||||||
|
|
||||||
.module-caption-editor__close-button {
|
.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 {
|
.module-caption-editor__media-container {
|
||||||
|
@ -1553,6 +1553,35 @@ body.dark-theme {
|
||||||
border-right: 1px solid $color-gray-75;
|
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
|
||||||
|
|
||||||
.module-start-new-conversation {
|
.module-start-new-conversation {
|
||||||
|
|
|
@ -129,8 +129,14 @@ window.searchResults.messages = [
|
||||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||||
<LeftPane
|
<LeftPane
|
||||||
searchResults={window.searchResults}
|
searchResults={window.searchResults}
|
||||||
openConversation={result => console.log('openConversation', result)}
|
startNewConversation={(query, options) =>
|
||||||
openMessage={result => console.log('onClickMessage', result)}
|
console.log('startNewConversation', query, options)
|
||||||
|
}
|
||||||
|
openConversationInternal={(id, messageId) =>
|
||||||
|
console.log('openConversation', id, messageId)
|
||||||
|
}
|
||||||
|
showArchivedConversations={() => console.log('showArchivedConversations')}
|
||||||
|
showInbox={() => console.log('showInbox')}
|
||||||
renderMainHeader={() => (
|
renderMainHeader={() => (
|
||||||
<MainHeader
|
<MainHeader
|
||||||
searchTerm="Hi there!"
|
searchTerm="Hi there!"
|
||||||
|
@ -151,8 +157,74 @@ window.searchResults.messages = [
|
||||||
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
<util.LeftPaneContext theme={util.theme} style={{ height: '200px' }}>
|
||||||
<LeftPane
|
<LeftPane
|
||||||
conversations={window.searchResults.conversations}
|
conversations={window.searchResults.conversations}
|
||||||
openConversation={result => console.log('openConversation', result)}
|
archivedConversations={[]}
|
||||||
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!"
|
||||||
|
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={() => (
|
renderMainHeader={() => (
|
||||||
<MainHeader
|
<MainHeader
|
||||||
searchTerm="Hi there!"
|
searchTerm="Hi there!"
|
||||||
|
|
|
@ -13,19 +13,27 @@ import { LocalizerType } from '../types/Util';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
conversations?: Array<ConversationListItemPropsType>;
|
conversations?: Array<ConversationListItemPropsType>;
|
||||||
|
archivedConversations?: Array<ConversationListItemPropsType>;
|
||||||
searchResults?: SearchResultsProps;
|
searchResults?: SearchResultsProps;
|
||||||
|
showArchived?: boolean;
|
||||||
|
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
startNewConversation: () => void;
|
startNewConversation: (
|
||||||
|
query: string,
|
||||||
|
options: { regionCode: string }
|
||||||
|
) => void;
|
||||||
openConversationInternal: (id: string, messageId?: string) => void;
|
openConversationInternal: (id: string, messageId?: string) => void;
|
||||||
|
showArchivedConversations: () => void;
|
||||||
|
showInbox: () => void;
|
||||||
|
|
||||||
// Render Props
|
// Render Props
|
||||||
renderMainHeader: () => JSX.Element;
|
renderMainHeader: () => JSX.Element;
|
||||||
}
|
}
|
||||||
|
|
||||||
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
// from https://github.com/bvaughn/react-virtualized/blob/fb3484ed5dcc41bffae8eab029126c0fb8f7abc0/source/List/types.js#L5
|
||||||
type RowRendererParams = {
|
type RowRendererParamsType = {
|
||||||
index: number;
|
index: number;
|
||||||
isScrolling: boolean;
|
isScrolling: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
@ -35,12 +43,51 @@ type RowRendererParams = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class LeftPane extends React.Component<Props> {
|
export class LeftPane extends React.Component<Props> {
|
||||||
public renderRow = ({ index, key, style }: RowRendererParams) => {
|
public listRef: React.RefObject<any> = React.createRef();
|
||||||
const { conversations, i18n, openConversationInternal } = this.props;
|
|
||||||
if (!conversations) {
|
public scrollToTop() {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<ConversationListItem
|
<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 {
|
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,
|
i18n,
|
||||||
conversations,
|
conversations,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
startNewConversation,
|
startNewConversation,
|
||||||
searchResults,
|
searchResults,
|
||||||
|
showArchived,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (searchResults) {
|
if (searchResults) {
|
||||||
|
@ -73,22 +157,35 @@ export class LeftPane extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conversations || !conversations.length) {
|
if (!conversations || !archivedConversations) {
|
||||||
return null;
|
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
|
// 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
|
// it re-renders when our conversation data changes. Otherwise it would just render
|
||||||
// on startup and scroll.
|
// on startup and scroll.
|
||||||
return (
|
return (
|
||||||
<div className="module-left-pane__list">
|
<div className="module-left-pane__list">
|
||||||
|
{showArchived ? (
|
||||||
|
<div className="module-left-pane__archive-helper-text">
|
||||||
|
{i18n('archiveHelperText')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<List
|
<List
|
||||||
className="module-left-pane__virtual-list"
|
className="module-left-pane__virtual-list"
|
||||||
|
ref={this.listRef}
|
||||||
conversations={conversations}
|
conversations={conversations}
|
||||||
height={height}
|
height={height}
|
||||||
rowCount={conversations.length}
|
rowCount={length}
|
||||||
rowHeight={64}
|
rowHeight={64}
|
||||||
rowRenderer={this.renderRow}
|
rowRenderer={this.renderRow}
|
||||||
width={width}
|
width={width}
|
||||||
|
@ -99,12 +196,31 @@ export class LeftPane extends React.Component<Props> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public renderArchivedHeader(): JSX.Element {
|
||||||
const { renderMainHeader } = this.props;
|
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 (
|
return (
|
||||||
<div className="module-left-pane">
|
<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()}
|
{this.renderList()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -113,7 +113,9 @@ window.searchResults.messages = [
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
onClickMessage={id => console.log('onClickMessage', id)}
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
onClickConversation={id => console.log('onClickConversation', id)}
|
||||||
onStartNewConversation={() => console.log('onStartNewConversation')}
|
onStartNewConversation={(query, options) =>
|
||||||
|
console.log('onStartNewConversation', query, options)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>;
|
</util.LeftPaneContext>;
|
||||||
```
|
```
|
||||||
|
@ -131,7 +133,9 @@ window.searchResults.messages = [
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
onClickMessage={id => console.log('onClickMessage', id)}
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
onClickConversation={id => console.log('onClickConversation', id)}
|
||||||
onStartNewConversation={() => console.log('onStartNewConversation')}
|
onStartNewConversation={(query, options) =>
|
||||||
|
console.log('onStartNewConversation', query, options)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>
|
</util.LeftPaneContext>
|
||||||
```
|
```
|
||||||
|
@ -147,7 +151,9 @@ window.searchResults.messages = [
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
onClickMessage={id => console.log('onClickMessage', id)}
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
onClickConversation={id => console.log('onClickConversation', id)}
|
||||||
onStartNewConversation={() => console.log('onStartNewConversation')}
|
onStartNewConversation={(query, options) =>
|
||||||
|
console.log('onStartNewConversation', query, options)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>
|
</util.LeftPaneContext>
|
||||||
```
|
```
|
||||||
|
@ -163,7 +169,9 @@ window.searchResults.messages = [
|
||||||
i18n={util.i18n}
|
i18n={util.i18n}
|
||||||
onClickMessage={id => console.log('onClickMessage', id)}
|
onClickMessage={id => console.log('onClickMessage', id)}
|
||||||
onClickConversation={id => console.log('onClickConversation', id)}
|
onClickConversation={id => console.log('onClickConversation', id)}
|
||||||
onStartNewConversation={() => console.log('onStartNewConversation')}
|
onStartNewConversation={(query, options) =>
|
||||||
|
console.log('onStartNewConversation', query, options)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</util.LeftPaneContext>
|
</util.LeftPaneContext>
|
||||||
```
|
```
|
||||||
|
|
|
@ -16,6 +16,7 @@ export type PropsData = {
|
||||||
conversations: Array<ConversationListItemPropsType>;
|
conversations: Array<ConversationListItemPropsType>;
|
||||||
hideMessagesHeader: boolean;
|
hideMessagesHeader: boolean;
|
||||||
messages: Array<MessageSearchResultPropsType>;
|
messages: Array<MessageSearchResultPropsType>;
|
||||||
|
regionCode: string;
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
showStartNewConversation: boolean;
|
showStartNewConversation: boolean;
|
||||||
};
|
};
|
||||||
|
@ -23,12 +24,21 @@ export type PropsData = {
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
openConversation: (id: string, messageId?: string) => void;
|
openConversation: (id: string, messageId?: string) => void;
|
||||||
startNewConversation: (id: string) => void;
|
startNewConversation: (
|
||||||
|
query: string,
|
||||||
|
options: { regionCode: string }
|
||||||
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = PropsData & PropsHousekeeping;
|
type Props = PropsData & PropsHousekeeping;
|
||||||
|
|
||||||
export class SearchResults extends React.Component<Props> {
|
export class SearchResults extends React.Component<Props> {
|
||||||
|
public handleStartNewConversation = () => {
|
||||||
|
const { regionCode, searchTerm, startNewConversation } = this.props;
|
||||||
|
|
||||||
|
startNewConversation(searchTerm, { regionCode });
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {
|
const {
|
||||||
conversations,
|
conversations,
|
||||||
|
@ -37,7 +47,6 @@ export class SearchResults extends React.Component<Props> {
|
||||||
i18n,
|
i18n,
|
||||||
messages,
|
messages,
|
||||||
openConversation,
|
openConversation,
|
||||||
startNewConversation,
|
|
||||||
searchTerm,
|
searchTerm,
|
||||||
showStartNewConversation,
|
showStartNewConversation,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -62,7 +71,7 @@ export class SearchResults extends React.Component<Props> {
|
||||||
<StartNewConversation
|
<StartNewConversation
|
||||||
phoneNumber={searchTerm}
|
phoneNumber={searchTerm}
|
||||||
i18n={i18n}
|
i18n={i18n}
|
||||||
onClick={startNewConversation}
|
onClick={this.handleStartNewConversation}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{haveConversations ? (
|
{haveConversations ? (
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { LocalizerType } from '../types/Util';
|
||||||
export interface Props {
|
export interface Props {
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
onClick: (id: string) => void;
|
onClick: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StartNewConversation extends React.PureComponent<Props> {
|
export class StartNewConversation extends React.PureComponent<Props> {
|
||||||
|
@ -18,9 +18,7 @@ export class StartNewConversation extends React.PureComponent<Props> {
|
||||||
<div
|
<div
|
||||||
role="button"
|
role="button"
|
||||||
className="module-start-new-conversation"
|
className="module-start-new-conversation"
|
||||||
onClick={() => {
|
onClick={onClick}
|
||||||
onClick(phoneNumber);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar
|
||||||
color="grey"
|
color="grey"
|
||||||
|
|
|
@ -16,17 +16,19 @@ interface TimerOption {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
i18n: LocalizerType;
|
|
||||||
isVerified: boolean;
|
|
||||||
name?: string;
|
|
||||||
id: string;
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
|
||||||
phoneNumber: string;
|
phoneNumber: string;
|
||||||
profileName?: string;
|
profileName?: string;
|
||||||
color: string;
|
color: string;
|
||||||
|
|
||||||
avatarPath?: string;
|
avatarPath?: string;
|
||||||
|
|
||||||
|
isVerified: boolean;
|
||||||
isMe: boolean;
|
isMe: boolean;
|
||||||
isGroup: boolean;
|
isGroup: boolean;
|
||||||
|
isArchived: boolean;
|
||||||
|
|
||||||
expirationSettingName?: string;
|
expirationSettingName?: string;
|
||||||
showBackButton: boolean;
|
showBackButton: boolean;
|
||||||
timerOptions: Array<TimerOption>;
|
timerOptions: Array<TimerOption>;
|
||||||
|
@ -39,6 +41,11 @@ interface Props {
|
||||||
onShowAllMedia: () => void;
|
onShowAllMedia: () => void;
|
||||||
onShowGroupMembers: () => void;
|
onShowGroupMembers: () => void;
|
||||||
onGoBack: () => void;
|
onGoBack: () => void;
|
||||||
|
|
||||||
|
onArchive: () => void;
|
||||||
|
onMoveToInbox: () => void;
|
||||||
|
|
||||||
|
i18n: LocalizerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConversationHeader extends React.Component<Props> {
|
export class ConversationHeader extends React.Component<Props> {
|
||||||
|
@ -184,12 +191,15 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
i18n,
|
i18n,
|
||||||
isMe,
|
isMe,
|
||||||
isGroup,
|
isGroup,
|
||||||
|
isArchived,
|
||||||
onDeleteMessages,
|
onDeleteMessages,
|
||||||
onResetSession,
|
onResetSession,
|
||||||
onSetDisappearingMessages,
|
onSetDisappearingMessages,
|
||||||
onShowAllMedia,
|
onShowAllMedia,
|
||||||
onShowGroupMembers,
|
onShowGroupMembers,
|
||||||
onShowSafetyNumber,
|
onShowSafetyNumber,
|
||||||
|
onArchive,
|
||||||
|
onMoveToInbox,
|
||||||
timerOptions,
|
timerOptions,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -223,6 +233,13 @@ export class ConversationHeader extends React.Component<Props> {
|
||||||
{!isGroup ? (
|
{!isGroup ? (
|
||||||
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isArchived ? (
|
||||||
|
<MenuItem onClick={onMoveToInbox}>
|
||||||
|
{i18n('moveConversationToInbox')}
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
|
||||||
|
)}
|
||||||
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
|
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,6 +34,7 @@ export type MessageType = {
|
||||||
export type ConversationType = {
|
export type ConversationType = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
|
isArchived: boolean;
|
||||||
activeAt?: number;
|
activeAt?: number;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
lastMessage?: {
|
lastMessage?: {
|
||||||
|
@ -55,6 +56,7 @@ export type ConversationLookupType = {
|
||||||
export type ConversationsStateType = {
|
export type ConversationsStateType = {
|
||||||
conversationLookup: ConversationLookupType;
|
conversationLookup: ConversationLookupType;
|
||||||
selectedConversation?: string;
|
selectedConversation?: string;
|
||||||
|
showArchived: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
@ -97,6 +99,14 @@ export type SelectedConversationChangedActionType = {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type ShowInboxActionType = {
|
||||||
|
type: 'SHOW_INBOX';
|
||||||
|
payload: null;
|
||||||
|
};
|
||||||
|
type ShowArchivedConversationsActionType = {
|
||||||
|
type: 'SHOW_ARCHIVED_CONVERSATIONS';
|
||||||
|
payload: null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ConversationActionType =
|
export type ConversationActionType =
|
||||||
| ConversationAddedActionType
|
| ConversationAddedActionType
|
||||||
|
@ -104,7 +114,11 @@ export type ConversationActionType =
|
||||||
| ConversationRemovedActionType
|
| ConversationRemovedActionType
|
||||||
| RemoveAllConversationsActionType
|
| RemoveAllConversationsActionType
|
||||||
| MessageExpiredActionType
|
| MessageExpiredActionType
|
||||||
| SelectedConversationChangedActionType;
|
| SelectedConversationChangedActionType
|
||||||
|
| MessageExpiredActionType
|
||||||
|
| SelectedConversationChangedActionType
|
||||||
|
| ShowInboxActionType
|
||||||
|
| ShowArchivedConversationsActionType;
|
||||||
|
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
|
@ -116,6 +130,8 @@ export const actions = {
|
||||||
messageExpired,
|
messageExpired,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
openConversationExternal,
|
openConversationExternal,
|
||||||
|
showInbox,
|
||||||
|
showArchivedConversations,
|
||||||
};
|
};
|
||||||
|
|
||||||
function conversationAdded(
|
function conversationAdded(
|
||||||
|
@ -156,6 +172,7 @@ function removeAllConversations(): RemoveAllConversationsActionType {
|
||||||
payload: null,
|
payload: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function messageExpired(
|
function messageExpired(
|
||||||
id: string,
|
id: string,
|
||||||
conversationId: 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
|
// Reducer
|
||||||
|
|
||||||
function getEmptyState(): ConversationsStateType {
|
function getEmptyState(): ConversationsStateType {
|
||||||
return {
|
return {
|
||||||
conversationLookup: {},
|
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') {
|
if (action.type === 'CONVERSATION_CHANGED') {
|
||||||
const { payload } = action;
|
const { payload } = action;
|
||||||
const { id, data } = payload;
|
const { id, data } = payload;
|
||||||
const { conversationLookup } = state;
|
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
|
// In the change case we only modify the lookup if we already had that conversation
|
||||||
if (!conversationLookup[id]) {
|
if (!existing) {
|
||||||
return state;
|
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 {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
selectedConversation,
|
||||||
|
showArchived,
|
||||||
conversationLookup: {
|
conversationLookup: {
|
||||||
...conversationLookup,
|
...conversationLookup,
|
||||||
[id]: data,
|
[id]: data,
|
||||||
|
@ -268,6 +310,27 @@ export function reducer(
|
||||||
if (action.type === 'MESSAGE_EXPIRED') {
|
if (action.type === 'MESSAGE_EXPIRED') {
|
||||||
// noop - for now this is only important for search
|
// 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;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { compact } from 'lodash';
|
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { format } from '../../types/PhoneNumber';
|
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(
|
function getConversationTitle(
|
||||||
conversation: ConversationType,
|
conversation: ConversationType,
|
||||||
options: { i18n: LocalizerType; ourRegionCode: string }
|
options: { i18n: LocalizerType; ourRegionCode: string }
|
||||||
|
@ -83,37 +89,49 @@ export const getConversationComparator = createSelector(
|
||||||
_getConversationComparator
|
_getConversationComparator
|
||||||
);
|
);
|
||||||
|
|
||||||
export const _getLeftPaneList = (
|
export const _getLeftPaneLists = (
|
||||||
lookup: ConversationLookupType,
|
lookup: ConversationLookupType,
|
||||||
comparator: (left: ConversationType, right: ConversationType) => number,
|
comparator: (left: ConversationType, right: ConversationType) => number,
|
||||||
selectedConversation?: string
|
selectedConversation?: string
|
||||||
): Array<ConversationType> => {
|
): {
|
||||||
|
conversations: Array<ConversationType>;
|
||||||
|
archivedConversations: Array<ConversationType>;
|
||||||
|
} => {
|
||||||
const values = Object.values(lookup);
|
const values = Object.values(lookup);
|
||||||
const filtered = compact(
|
const sorted = values.sort(comparator);
|
||||||
values.map(conversation => {
|
|
||||||
|
const conversations: Array<ConversationType> = [];
|
||||||
|
const archivedConversations: Array<ConversationType> = [];
|
||||||
|
|
||||||
|
const max = sorted.length;
|
||||||
|
for (let i = 0; i < max; i += 1) {
|
||||||
|
let conversation = sorted[i];
|
||||||
if (!conversation.activeAt) {
|
if (!conversation.activeAt) {
|
||||||
return null;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedConversation === conversation.id) {
|
if (selectedConversation === conversation.id) {
|
||||||
return {
|
conversation = {
|
||||||
...conversation,
|
...conversation,
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return conversation;
|
if (conversation.isArchived) {
|
||||||
})
|
archivedConversations.push(conversation);
|
||||||
);
|
} else {
|
||||||
|
conversations.push(conversation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return filtered.sort(comparator);
|
return { conversations, archivedConversations };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLeftPaneList = createSelector(
|
export const getLeftPaneLists = createSelector(
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getConversationComparator,
|
getConversationComparator,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
_getLeftPaneList
|
_getLeftPaneLists
|
||||||
);
|
);
|
||||||
|
|
||||||
export const getMe = createSelector(
|
export const getMe = createSelector(
|
||||||
|
|
|
@ -2,14 +2,16 @@ import { compact } from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
import { SearchStateType } from '../ducks/search';
|
|
||||||
|
|
||||||
|
import { SearchStateType } from '../ducks/search';
|
||||||
import {
|
import {
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
} from './conversations';
|
} from './conversations';
|
||||||
import { ConversationLookupType } from '../ducks/conversations';
|
import { ConversationLookupType } from '../ducks/conversations';
|
||||||
|
|
||||||
|
import { getRegionCode } from './user';
|
||||||
|
|
||||||
export const getSearch = (state: StateType): SearchStateType => state.search;
|
export const getSearch = (state: StateType): SearchStateType => state.search;
|
||||||
|
|
||||||
export const getQuery = createSelector(
|
export const getQuery = createSelector(
|
||||||
|
@ -34,12 +36,14 @@ export const isSearching = createSelector(
|
||||||
export const getSearchResults = createSelector(
|
export const getSearchResults = createSelector(
|
||||||
[
|
[
|
||||||
getSearch,
|
getSearch,
|
||||||
|
getRegionCode,
|
||||||
getConversationLookup,
|
getConversationLookup,
|
||||||
getSelectedConversation,
|
getSelectedConversation,
|
||||||
getSelectedMessage,
|
getSelectedMessage,
|
||||||
],
|
],
|
||||||
(
|
(
|
||||||
state: SearchStateType,
|
state: SearchStateType,
|
||||||
|
regionCode: string,
|
||||||
lookup: ConversationLookupType,
|
lookup: ConversationLookupType,
|
||||||
selectedConversation?: string,
|
selectedConversation?: string,
|
||||||
selectedMessage?: string
|
selectedMessage?: string
|
||||||
|
@ -84,6 +88,7 @@ export const getSearchResults = createSelector(
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
}),
|
}),
|
||||||
|
regionCode: regionCode,
|
||||||
searchTerm: state.query,
|
searchTerm: state.query,
|
||||||
showStartNewConversation: Boolean(
|
showStartNewConversation: Boolean(
|
||||||
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]
|
state.normalizedPhoneNumber && !lookup[state.normalizedPhoneNumber]
|
||||||
|
|
|
@ -4,9 +4,9 @@ import { mapDispatchToProps } from '../actions';
|
||||||
import { LeftPane } from '../../components/LeftPane';
|
import { LeftPane } from '../../components/LeftPane';
|
||||||
import { StateType } from '../reducer';
|
import { StateType } from '../reducer';
|
||||||
|
|
||||||
import { getQuery, getSearchResults, isSearching } from '../selectors/search';
|
import { getSearchResults, isSearching } from '../selectors/search';
|
||||||
import { getIntl } from '../selectors/user';
|
import { getIntl } from '../selectors/user';
|
||||||
import { getLeftPaneList, getMe } from '../selectors/conversations';
|
import { getLeftPaneLists, getShowArchived } from '../selectors/conversations';
|
||||||
|
|
||||||
import { SmartMainHeader } from './MainHeader';
|
import { SmartMainHeader } from './MainHeader';
|
||||||
|
|
||||||
|
@ -17,12 +17,14 @@ const FilteredSmartMainHeader = SmartMainHeader as any;
|
||||||
const mapStateToProps = (state: StateType) => {
|
const mapStateToProps = (state: StateType) => {
|
||||||
const showSearch = isSearching(state);
|
const showSearch = isSearching(state);
|
||||||
|
|
||||||
|
const lists = showSearch ? undefined : getLeftPaneLists(state);
|
||||||
|
const searchResults = showSearch ? getSearchResults(state) : undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...lists,
|
||||||
|
searchResults,
|
||||||
|
showArchived: getShowArchived(state),
|
||||||
i18n: getIntl(state),
|
i18n: getIntl(state),
|
||||||
me: getMe(state),
|
|
||||||
query: getQuery(state),
|
|
||||||
conversations: showSearch ? undefined : getLeftPaneList(state),
|
|
||||||
searchResults: showSearch ? getSearchResults(state) : undefined,
|
|
||||||
renderMainHeader: () => <FilteredSmartMainHeader />,
|
renderMainHeader: () => <FilteredSmartMainHeader />,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { assert } from 'chai';
|
||||||
import { ConversationLookupType } from '../../../state/ducks/conversations';
|
import { ConversationLookupType } from '../../../state/ducks/conversations';
|
||||||
import {
|
import {
|
||||||
_getConversationComparator,
|
_getConversationComparator,
|
||||||
_getLeftPaneList,
|
_getLeftPaneLists,
|
||||||
} from '../../../state/selectors/conversations';
|
} from '../../../state/selectors/conversations';
|
||||||
|
|
||||||
describe('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', () => {
|
it('sorts conversations based on timestamp then by intl-friendly title', () => {
|
||||||
const i18n = (key: string) => key;
|
const i18n = (key: string) => key;
|
||||||
const regionCode = 'US';
|
const regionCode = 'US';
|
||||||
const conversations: ConversationLookupType = {
|
const data: ConversationLookupType = {
|
||||||
id1: {
|
id1: {
|
||||||
id: 'id1',
|
id: 'id1',
|
||||||
activeAt: Date.now(),
|
activeAt: Date.now(),
|
||||||
name: 'No timestamp',
|
name: 'No timestamp',
|
||||||
timestamp: 0,
|
timestamp: 0,
|
||||||
phoneNumber: 'notused',
|
phoneNumber: 'notused',
|
||||||
|
isArchived: false,
|
||||||
|
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
|
@ -32,6 +33,7 @@ describe('state/selectors/conversations', () => {
|
||||||
name: 'B',
|
name: 'B',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
phoneNumber: 'notused',
|
phoneNumber: 'notused',
|
||||||
|
isArchived: false,
|
||||||
|
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
|
@ -46,6 +48,7 @@ describe('state/selectors/conversations', () => {
|
||||||
name: 'C',
|
name: 'C',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
phoneNumber: 'notused',
|
phoneNumber: 'notused',
|
||||||
|
isArchived: false,
|
||||||
|
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
|
@ -60,6 +63,7 @@ describe('state/selectors/conversations', () => {
|
||||||
name: 'Á',
|
name: 'Á',
|
||||||
timestamp: 20,
|
timestamp: 20,
|
||||||
phoneNumber: 'notused',
|
phoneNumber: 'notused',
|
||||||
|
isArchived: false,
|
||||||
|
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
|
@ -74,6 +78,7 @@ describe('state/selectors/conversations', () => {
|
||||||
name: 'First!',
|
name: 'First!',
|
||||||
timestamp: 30,
|
timestamp: 30,
|
||||||
phoneNumber: 'notused',
|
phoneNumber: 'notused',
|
||||||
|
isArchived: false,
|
||||||
|
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
isMe: false,
|
isMe: false,
|
||||||
|
@ -84,13 +89,13 @@ describe('state/selectors/conversations', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const comparator = _getConversationComparator(i18n, regionCode);
|
const comparator = _getConversationComparator(i18n, regionCode);
|
||||||
const list = _getLeftPaneList(conversations, comparator);
|
const { conversations } = _getLeftPaneLists(data, comparator);
|
||||||
|
|
||||||
assert.strictEqual(list[0].name, 'First!');
|
assert.strictEqual(conversations[0].name, 'First!');
|
||||||
assert.strictEqual(list[1].name, 'Á');
|
assert.strictEqual(conversations[1].name, 'Á');
|
||||||
assert.strictEqual(list[2].name, 'B');
|
assert.strictEqual(conversations[2].name, 'B');
|
||||||
assert.strictEqual(list[3].name, 'C');
|
assert.strictEqual(conversations[3].name, 'C');
|
||||||
assert.strictEqual(list[4].name, 'No timestamp');
|
assert.strictEqual(conversations[4].name, 'No timestamp');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -164,7 +164,7 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " async load() {",
|
"line": " async load() {",
|
||||||
"lineNumber": 179,
|
"lineNumber": 177,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-02T21:00:44.007Z"
|
||||||
},
|
},
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
"rule": "jQuery-load(",
|
"rule": "jQuery-load(",
|
||||||
"path": "js/conversation_controller.js",
|
"path": "js/conversation_controller.js",
|
||||||
"line": " this._initialPromise = load();",
|
"line": " this._initialPromise = load();",
|
||||||
"lineNumber": 214,
|
"lineNumber": 212,
|
||||||
"reasonCategory": "falseMatch",
|
"reasonCategory": "falseMatch",
|
||||||
"updated": "2018-10-02T21:00:44.007Z"
|
"updated": "2018-10-02T21:00:44.007Z"
|
||||||
},
|
},
|
||||||
|
@ -562,7 +562,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " .append(this.networkStatusView.render().el);",
|
"line": " .append(this.networkStatusView.render().el);",
|
||||||
"lineNumber": 89,
|
"lineNumber": 88,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -571,7 +571,7 @@
|
||||||
"rule": "jQuery-prependTo(",
|
"rule": "jQuery-prependTo(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " banner.$el.prependTo(this.$el);",
|
"line": " banner.$el.prependTo(this.$el);",
|
||||||
"lineNumber": 93,
|
"lineNumber": 92,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2018-09-19T18:13:29.628Z",
|
"updated": "2018-09-19T18:13:29.628Z",
|
||||||
"reasonDetail": "Interacting with already-existing DOM nodes"
|
"reasonDetail": "Interacting with already-existing DOM nodes"
|
||||||
|
@ -580,7 +580,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 164,
|
"lineNumber": 166,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -589,7 +589,7 @@
|
||||||
"rule": "jQuery-append(",
|
"rule": "jQuery-append(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
|
||||||
"lineNumber": 164,
|
"lineNumber": 166,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -598,7 +598,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
|
||||||
"lineNumber": 205,
|
"lineNumber": 207,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -607,7 +607,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('#header, .gutter').addClass('inactive');",
|
"line": " this.$('#header, .gutter').addClass('inactive');",
|
||||||
"lineNumber": 209,
|
"lineNumber": 211,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -616,25 +616,25 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation-stack').addClass('inactive');",
|
"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,
|
"lineNumber": 215,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"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-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
|
||||||
"lineNumber": 230,
|
"lineNumber": 236,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -643,7 +643,7 @@
|
||||||
"rule": "jQuery-$(",
|
"rule": "jQuery-$(",
|
||||||
"path": "js/views/inbox_view.js",
|
"path": "js/views/inbox_view.js",
|
||||||
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
"line": " this.$('.conversation:first .recorder').trigger('close');",
|
||||||
"lineNumber": 233,
|
"lineNumber": 239,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-08T23:49:08.796Z",
|
"updated": "2019-03-08T23:49:08.796Z",
|
||||||
"reasonDetail": "Protected from arbitrary input"
|
"reasonDetail": "Protected from arbitrary input"
|
||||||
|
@ -5464,6 +5464,24 @@
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-03-09T00:08:44.242Z",
|
||||||
"reasonDetail": "Used only to set focus"
|
"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",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/Lightbox.js",
|
"path": "ts/components/Lightbox.js",
|
||||||
|
@ -5513,7 +5531,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/conversation/ConversationHeader.tsx",
|
"path": "ts/components/conversation/ConversationHeader.tsx",
|
||||||
"line": " this.menuTriggerRef = React.createRef();",
|
"line": " this.menuTriggerRef = React.createRef();",
|
||||||
"lineNumber": 51,
|
"lineNumber": 58,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2019-03-09T00:08:44.242Z",
|
"updated": "2019-03-09T00:08:44.242Z",
|
||||||
"reasonDetail": "Used only to trigger menu display"
|
"reasonDetail": "Used only to trigger menu display"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue