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": {
|
||||
"message": "Choose folder",
|
||||
"description": "Button to allow the user to find a folder on disk"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1645,10 +1645,10 @@
|
|||
c.onReadMessage(message);
|
||||
}
|
||||
} else {
|
||||
conversation.set(
|
||||
'unreadCount',
|
||||
conversation.get('unreadCount') + 1
|
||||
);
|
||||
conversation.set({
|
||||
unreadCount: conversation.get('unreadCount') + 1,
|
||||
isArchived: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global Signal:false */
|
||||
/* global Backbone: false */
|
||||
|
||||
/* global ConversationController: false */
|
||||
/* global drawAttention: false */
|
||||
/* global i18n: false */
|
||||
/* global isFocused: false */
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -220,7 +220,7 @@
|
|||
window.location.reload();
|
||||
},
|
||||
async openConversation(id, messageId) {
|
||||
const conversation = await window.ConversationController.getOrCreateAndWait(
|
||||
const conversation = await ConversationController.getOrCreateAndWait(
|
||||
id,
|
||||
'private'
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
```
|
||||
|
|
|
@ -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 ? (
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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 />,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
]
|
Loading…
Reference in a new issue