Read Pinned Chats
Co-authored-by: Sidney Keese <sidney@carbonfive.com>
This commit is contained in:
parent
3ca547f3dd
commit
63b2644cb4
15 changed files with 444 additions and 46 deletions
|
@ -193,6 +193,14 @@
|
||||||
"message": "Archived Conversations",
|
"message": "Archived Conversations",
|
||||||
"description": "Shown in place of the search box when showing archived conversation list"
|
"description": "Shown in place of the search box when showing archived conversation list"
|
||||||
},
|
},
|
||||||
|
"LeftPane--pinned": {
|
||||||
|
"message": "Pinned",
|
||||||
|
"description": "Shown as a header for pinned conversations in the left pane"
|
||||||
|
},
|
||||||
|
"LeftPane--chats": {
|
||||||
|
"message": "Chats",
|
||||||
|
"description": "Shown as a header for non-pinned conversations in the left pane"
|
||||||
|
},
|
||||||
"archiveHelperText": {
|
"archiveHelperText": {
|
||||||
"message": "These conversations are archived and will only appear in the Inbox if new messages are received.",
|
"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 conversations list in the left pane"
|
"description": "Shown at the top of the archived conversations list in the left pane"
|
||||||
|
|
|
@ -90,15 +90,29 @@ message GroupV2Record {
|
||||||
}
|
}
|
||||||
|
|
||||||
message AccountRecord {
|
message AccountRecord {
|
||||||
optional bytes profileKey = 1;
|
message PinnedConversation {
|
||||||
optional string givenName = 2;
|
message Contact {
|
||||||
optional string familyName = 3;
|
optional string uuid = 1;
|
||||||
optional string avatarUrl = 4;
|
optional string e164 = 2;
|
||||||
optional bool noteToSelfArchived = 5;
|
}
|
||||||
optional bool readReceipts = 6;
|
|
||||||
optional bool sealedSenderIndicators = 7;
|
oneof identifier {
|
||||||
optional bool typingIndicators = 8;
|
Contact contact = 1;
|
||||||
optional bool proxiedLinkPreviews = 9;
|
bytes legacyGroupId = 3;
|
||||||
optional bool noteToSelfUnread = 10;
|
bytes groupMasterKey = 4;
|
||||||
optional bool linkPreviews = 11;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
optional bytes profileKey = 1;
|
||||||
|
optional string givenName = 2;
|
||||||
|
optional string familyName = 3;
|
||||||
|
optional string avatarUrl = 4;
|
||||||
|
optional bool noteToSelfArchived = 5;
|
||||||
|
optional bool readReceipts = 6;
|
||||||
|
optional bool sealedSenderIndicators = 7;
|
||||||
|
optional bool typingIndicators = 8;
|
||||||
|
optional bool proxiedLinkPreviews = 9;
|
||||||
|
optional bool noteToSelfUnread = 10;
|
||||||
|
optional bool linkPreviews = 11;
|
||||||
|
repeated PinnedConversation pinnedConversations = 14;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6176,6 +6176,18 @@ button.module-image__border-overlay:focus {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.module-left-pane__header-row {
|
||||||
|
@include font-body-1-bold;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 16px;
|
||||||
|
|
||||||
|
@include dark-theme {
|
||||||
|
color: $color-gray-05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.module-left-pane__to-inbox-button {
|
.module-left-pane__to-inbox-button {
|
||||||
@include button-reset;
|
@include button-reset;
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ export function stringFromBytes(buffer: ArrayBuffer): string {
|
||||||
export function hexFromBytes(buffer: ArrayBuffer): string {
|
export function hexFromBytes(buffer: ArrayBuffer): string {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
return window.dcodeIO.ByteBuffer.wrap(buffer).toString('hex');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bytesFromHexString(string: string): ArrayBuffer {
|
export function bytesFromHexString(string: string): ArrayBuffer {
|
||||||
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export type PropsData = {
|
||||||
text: string;
|
text: string;
|
||||||
deletedForEveryone?: boolean;
|
deletedForEveryone?: boolean;
|
||||||
};
|
};
|
||||||
|
isPinned?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PropsHousekeeping = {
|
type PropsHousekeeping = {
|
||||||
|
|
|
@ -40,12 +40,32 @@ const defaultArchivedConversations: Array<PropsData> = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const pinnedConversations: Array<PropsData> = [
|
||||||
|
{
|
||||||
|
id: 'philly-convo',
|
||||||
|
isPinned: true,
|
||||||
|
isSelected: false,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
title: 'Philip Glass',
|
||||||
|
type: 'direct',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'robbo-convo',
|
||||||
|
isPinned: true,
|
||||||
|
isSelected: false,
|
||||||
|
lastUpdated: Date.now(),
|
||||||
|
title: 'Robert Moog',
|
||||||
|
type: 'direct',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
const createProps = (overrideProps: Partial<PropsType> = {}): PropsType => ({
|
||||||
archivedConversations:
|
archivedConversations:
|
||||||
overrideProps.archivedConversations || defaultArchivedConversations,
|
overrideProps.archivedConversations || defaultArchivedConversations,
|
||||||
conversations: overrideProps.conversations || defaultConversations,
|
conversations: overrideProps.conversations || defaultConversations,
|
||||||
i18n,
|
i18n,
|
||||||
openConversationInternal: action('openConversationInternal'),
|
openConversationInternal: action('openConversationInternal'),
|
||||||
|
pinnedConversations: overrideProps.pinnedConversations || [],
|
||||||
renderExpiredBuildDialog: () => <div />,
|
renderExpiredBuildDialog: () => <div />,
|
||||||
renderMainHeader: () => <div />,
|
renderMainHeader: () => <div />,
|
||||||
renderMessageSearchResult: () => <div />,
|
renderMessageSearchResult: () => <div />,
|
||||||
|
@ -69,6 +89,14 @@ story.add('Conversation States (Active, Selected, Archived)', () => {
|
||||||
return <LeftPane {...props} />;
|
return <LeftPane {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
story.add('Pinned Conversations', () => {
|
||||||
|
const props = createProps({
|
||||||
|
pinnedConversations,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <LeftPane {...props} />;
|
||||||
|
});
|
||||||
|
|
||||||
story.add('Archived Conversations Shown', () => {
|
story.add('Archived Conversations Shown', () => {
|
||||||
const props = createProps({
|
const props = createProps({
|
||||||
showArchived: true,
|
showArchived: true,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { cleanId } from './_util';
|
||||||
export interface PropsType {
|
export interface PropsType {
|
||||||
conversations?: Array<ConversationListItemPropsType>;
|
conversations?: Array<ConversationListItemPropsType>;
|
||||||
archivedConversations?: Array<ConversationListItemPropsType>;
|
archivedConversations?: Array<ConversationListItemPropsType>;
|
||||||
|
pinnedConversations?: Array<ConversationListItemPropsType>;
|
||||||
selectedConversationId?: string;
|
selectedConversationId?: string;
|
||||||
searchResults?: SearchResultsProps;
|
searchResults?: SearchResultsProps;
|
||||||
showArchived?: boolean;
|
showArchived?: boolean;
|
||||||
|
@ -51,6 +52,43 @@ type RowRendererParamsType = {
|
||||||
style: CSSProperties;
|
style: CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
enum RowType {
|
||||||
|
ArchiveButton,
|
||||||
|
ArchivedConversation,
|
||||||
|
Conversation,
|
||||||
|
Header,
|
||||||
|
PinnedConversation,
|
||||||
|
Undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HeaderType {
|
||||||
|
Pinned,
|
||||||
|
Chats,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ArchiveButtonRow {
|
||||||
|
type: RowType.ArchiveButton;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConversationRow {
|
||||||
|
index: number;
|
||||||
|
type:
|
||||||
|
| RowType.ArchivedConversation
|
||||||
|
| RowType.Conversation
|
||||||
|
| RowType.PinnedConversation;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HeaderRow {
|
||||||
|
headerType: HeaderType;
|
||||||
|
type: RowType.Header;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UndefinedRow {
|
||||||
|
type: RowType.Undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Row = ArchiveButtonRow | ConversationRow | HeaderRow | UndefinedRow;
|
||||||
|
|
||||||
export class LeftPane extends React.Component<PropsType> {
|
export class LeftPane extends React.Component<PropsType> {
|
||||||
public listRef = React.createRef<List>();
|
public listRef = React.createRef<List>();
|
||||||
|
|
||||||
|
@ -60,31 +98,77 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
|
|
||||||
public setFocusToLastNeeded = false;
|
public setFocusToLastNeeded = false;
|
||||||
|
|
||||||
public renderRow = ({
|
public calculateRowHeight = ({ index }: { index: number }): number => {
|
||||||
index,
|
const { type } = this.getRowFromIndex(index);
|
||||||
key,
|
return type === RowType.Header ? 40 : 68;
|
||||||
style,
|
};
|
||||||
}: RowRendererParamsType): JSX.Element => {
|
|
||||||
|
public getRowFromIndex = (index: number): Row => {
|
||||||
const {
|
const {
|
||||||
archivedConversations,
|
archivedConversations,
|
||||||
conversations,
|
conversations,
|
||||||
i18n,
|
pinnedConversations,
|
||||||
openConversationInternal,
|
|
||||||
showArchived,
|
showArchived,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
if (!conversations || !archivedConversations) {
|
|
||||||
throw new Error(
|
if (!conversations || !pinnedConversations || !archivedConversations) {
|
||||||
'renderRow: Tried to render without conversations or archivedConversations'
|
return {
|
||||||
);
|
type: RowType.Undefined,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!showArchived && index === conversations.length) {
|
if (showArchived) {
|
||||||
return this.renderArchivedButton({ key, style });
|
return {
|
||||||
|
index,
|
||||||
|
type: RowType.ArchivedConversation,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const conversation = showArchived
|
let conversationIndex = index;
|
||||||
? archivedConversations[index]
|
|
||||||
: conversations[index];
|
if (pinnedConversations.length) {
|
||||||
|
if (index === 0) {
|
||||||
|
return {
|
||||||
|
headerType: HeaderType.Pinned,
|
||||||
|
type: RowType.Header,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index <= pinnedConversations.length) {
|
||||||
|
return {
|
||||||
|
index: index - 1,
|
||||||
|
type: RowType.PinnedConversation,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === pinnedConversations.length + 1) {
|
||||||
|
return {
|
||||||
|
headerType: HeaderType.Chats,
|
||||||
|
type: RowType.Header,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
conversationIndex -= pinnedConversations.length + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationIndex === conversations.length) {
|
||||||
|
return {
|
||||||
|
type: RowType.ArchiveButton,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: conversationIndex,
|
||||||
|
type: RowType.Conversation,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
public renderConversationRow(
|
||||||
|
conversation: ConversationListItemPropsType,
|
||||||
|
key: string,
|
||||||
|
style: CSSProperties
|
||||||
|
): JSX.Element {
|
||||||
|
const { i18n, openConversationInternal } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -99,15 +183,90 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renderHeaderRow = (
|
||||||
|
index: number,
|
||||||
|
key: string,
|
||||||
|
style: CSSProperties
|
||||||
|
): JSX.Element => {
|
||||||
|
const { i18n } = this.props;
|
||||||
|
|
||||||
|
switch (index) {
|
||||||
|
case HeaderType.Pinned: {
|
||||||
|
return (
|
||||||
|
<div className="module-left-pane__header-row" key={key} style={style}>
|
||||||
|
{i18n('LeftPane--pinned')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case HeaderType.Chats: {
|
||||||
|
return (
|
||||||
|
<div className="module-left-pane__header-row" key={key} style={style}>
|
||||||
|
{i18n('LeftPane--chats')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
window.log.warn('LeftPane: invalid HeaderRowIndex received');
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public renderArchivedButton = ({
|
public renderRow = ({
|
||||||
|
index,
|
||||||
key,
|
key,
|
||||||
style,
|
style,
|
||||||
}: {
|
}: RowRendererParamsType): JSX.Element => {
|
||||||
key: string;
|
const {
|
||||||
style: CSSProperties;
|
archivedConversations,
|
||||||
}): JSX.Element => {
|
conversations,
|
||||||
|
pinnedConversations,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (!conversations || !pinnedConversations || !archivedConversations) {
|
||||||
|
throw new Error(
|
||||||
|
'renderRow: Tried to render without conversations or pinnedConversations or archivedConversations'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = this.getRowFromIndex(index);
|
||||||
|
|
||||||
|
switch (row.type) {
|
||||||
|
case RowType.ArchiveButton: {
|
||||||
|
return this.renderArchivedButton(key, style);
|
||||||
|
}
|
||||||
|
case RowType.ArchivedConversation: {
|
||||||
|
return this.renderConversationRow(
|
||||||
|
archivedConversations[row.index],
|
||||||
|
key,
|
||||||
|
style
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case RowType.Conversation: {
|
||||||
|
return this.renderConversationRow(conversations[row.index], key, style);
|
||||||
|
}
|
||||||
|
case RowType.Header: {
|
||||||
|
return this.renderHeaderRow(row.headerType, key, style);
|
||||||
|
}
|
||||||
|
case RowType.PinnedConversation: {
|
||||||
|
return this.renderConversationRow(
|
||||||
|
pinnedConversations[row.index],
|
||||||
|
key,
|
||||||
|
style
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
window.log.warn('LeftPane: unknown RowType received');
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public renderArchivedButton = (
|
||||||
|
key: string,
|
||||||
|
style: CSSProperties
|
||||||
|
): JSX.Element => {
|
||||||
const {
|
const {
|
||||||
archivedConversations,
|
archivedConversations,
|
||||||
i18n,
|
i18n,
|
||||||
|
@ -199,6 +358,14 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
this.listRef.current.scrollToRow(row);
|
this.listRef.current.scrollToRow(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public recomputeRowHeights = (): void => {
|
||||||
|
if (!this.listRef || !this.listRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.listRef.current.recomputeRowHeights();
|
||||||
|
};
|
||||||
|
|
||||||
public getScrollContainer = (): HTMLDivElement | null => {
|
public getScrollContainer = (): HTMLDivElement | null => {
|
||||||
if (!this.listRef || !this.listRef.current) {
|
if (!this.listRef || !this.listRef.current) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -269,16 +436,34 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
|
|
||||||
public getLength = (): number => {
|
public getLength = (): number => {
|
||||||
const { archivedConversations, conversations, showArchived } = this.props;
|
const {
|
||||||
|
archivedConversations,
|
||||||
|
conversations,
|
||||||
|
pinnedConversations,
|
||||||
|
showArchived,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (!conversations || !archivedConversations) {
|
if (!conversations || !archivedConversations || !pinnedConversations) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// That extra 1 element added to the list is the 'archived conversations' button
|
if (showArchived) {
|
||||||
return showArchived
|
return archivedConversations.length;
|
||||||
? archivedConversations.length
|
}
|
||||||
: conversations.length + (archivedConversations.length ? 1 : 0);
|
|
||||||
|
let { length } = conversations;
|
||||||
|
|
||||||
|
// includes two additional rows for pinned/chats headers
|
||||||
|
if (pinnedConversations.length) {
|
||||||
|
length += pinnedConversations.length + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// includes one additional row for 'archived conversations' button
|
||||||
|
if (archivedConversations.length) {
|
||||||
|
length += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length;
|
||||||
};
|
};
|
||||||
|
|
||||||
public renderList = ({
|
public renderList = ({
|
||||||
|
@ -290,6 +475,7 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
i18n,
|
i18n,
|
||||||
conversations,
|
conversations,
|
||||||
openConversationInternal,
|
openConversationInternal,
|
||||||
|
pinnedConversations,
|
||||||
renderMessageSearchResult,
|
renderMessageSearchResult,
|
||||||
startNewConversation,
|
startNewConversation,
|
||||||
searchResults,
|
searchResults,
|
||||||
|
@ -310,7 +496,7 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!conversations || !archivedConversations) {
|
if (!conversations || !archivedConversations || !pinnedConversations) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'render: must provided conversations and archivedConverstions if no search results are provided'
|
'render: must provided conversations and archivedConverstions if no search results are provided'
|
||||||
);
|
);
|
||||||
|
@ -345,7 +531,7 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
ref={this.listRef}
|
ref={this.listRef}
|
||||||
rowCount={length}
|
rowCount={length}
|
||||||
rowHeight={68}
|
rowHeight={this.calculateRowHeight}
|
||||||
rowRenderer={this.renderRow}
|
rowRenderer={this.renderRow}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
width={width || 0}
|
width={width || 0}
|
||||||
|
@ -412,4 +598,16 @@ export class LeftPane extends React.Component<PropsType> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(oldProps: PropsType): void {
|
||||||
|
const { pinnedConversations: oldPinned } = oldProps;
|
||||||
|
const { pinnedConversations: pinned } = this.props;
|
||||||
|
|
||||||
|
const oldLength = (oldPinned && oldPinned.length) || 0;
|
||||||
|
const newLength = (pinned && pinned.length) || 0;
|
||||||
|
|
||||||
|
if (oldLength !== newLength) {
|
||||||
|
this.recomputeRowHeights();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
2
ts/model-types.d.ts
vendored
2
ts/model-types.d.ts
vendored
|
@ -145,12 +145,14 @@ export type ConversationAttributesType = {
|
||||||
draftAttachments: Array<unknown>;
|
draftAttachments: Array<unknown>;
|
||||||
draftTimestamp: number | null;
|
draftTimestamp: number | null;
|
||||||
inbox_position: number;
|
inbox_position: number;
|
||||||
|
isPinned: boolean;
|
||||||
lastMessageDeletedForEveryone: unknown;
|
lastMessageDeletedForEveryone: unknown;
|
||||||
lastMessageStatus: LastMessageStatus | null;
|
lastMessageStatus: LastMessageStatus | null;
|
||||||
messageCount: number;
|
messageCount: number;
|
||||||
messageCountBeforeMessageRequests: number;
|
messageCountBeforeMessageRequests: number;
|
||||||
messageRequestResponseType: number;
|
messageRequestResponseType: number;
|
||||||
muteExpiresAt: number;
|
muteExpiresAt: number;
|
||||||
|
pinIndex?: number;
|
||||||
profileAvatar: WhatIsThis;
|
profileAvatar: WhatIsThis;
|
||||||
profileKeyCredential: unknown | null;
|
profileKeyCredential: unknown | null;
|
||||||
profileKeyVersion: string;
|
profileKeyVersion: string;
|
||||||
|
|
|
@ -748,6 +748,7 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
isArchived: this.get('isArchived')!,
|
isArchived: this.get('isArchived')!,
|
||||||
isBlocked: this.isBlocked(),
|
isBlocked: this.isBlocked(),
|
||||||
isMe: this.isMe(),
|
isMe: this.isMe(),
|
||||||
|
isPinned: this.get('isPinned'),
|
||||||
isVerified: this.isVerified(),
|
isVerified: this.isVerified(),
|
||||||
lastMessage: {
|
lastMessage: {
|
||||||
status: this.get('lastMessageStatus')!,
|
status: this.get('lastMessageStatus')!,
|
||||||
|
@ -762,6 +763,7 @@ export class ConversationModel extends window.Backbone.Model<
|
||||||
muteExpiresAt: this.get('muteExpiresAt')!,
|
muteExpiresAt: this.get('muteExpiresAt')!,
|
||||||
name: this.get('name')!,
|
name: this.get('name')!,
|
||||||
phoneNumber: this.getNumber()!,
|
phoneNumber: this.getNumber()!,
|
||||||
|
pinIndex: this.get('pinIndex'),
|
||||||
profileName: this.getProfileName()!,
|
profileName: this.getProfileName()!,
|
||||||
sharedGroupNames: this.get('sharedGroupNames')!,
|
sharedGroupNames: this.get('sharedGroupNames')!,
|
||||||
shouldShowDraft,
|
shouldShowDraft,
|
||||||
|
|
|
@ -648,7 +648,9 @@ async function processManifest(
|
||||||
|
|
||||||
const decryptedStorageItems = await pMap(
|
const decryptedStorageItems = await pMap(
|
||||||
storageItems.items,
|
storageItems.items,
|
||||||
async (storageRecordWrapper: StorageItemClass) => {
|
async (
|
||||||
|
storageRecordWrapper: StorageItemClass
|
||||||
|
): Promise<MergeableItemType> => {
|
||||||
const { key, value: storageItemCiphertext } = storageRecordWrapper;
|
const { key, value: storageItemCiphertext } = storageRecordWrapper;
|
||||||
|
|
||||||
if (!key || !storageItemCiphertext) {
|
if (!key || !storageItemCiphertext) {
|
||||||
|
@ -695,11 +697,19 @@ async function processManifest(
|
||||||
{ concurrency: 50 }
|
{ concurrency: 50 }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Merge Account records last
|
||||||
|
const sortedStorageItems = ([] as Array<MergeableItemType>).concat(
|
||||||
|
..._.partition(
|
||||||
|
decryptedStorageItems,
|
||||||
|
storageRecord => storageRecord.storageRecord.account === undefined
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
window.log.info(
|
window.log.info(
|
||||||
`storageService.processManifest: Attempting to merge ${decryptedStorageItems.length} records`
|
`storageService.processManifest: Attempting to merge ${sortedStorageItems.length} records`
|
||||||
);
|
);
|
||||||
const mergedRecords = await pMap(decryptedStorageItems, mergeRecord, {
|
const mergedRecords = await pMap(sortedStorageItems, mergeRecord, {
|
||||||
concurrency: 5,
|
concurrency: 5,
|
||||||
});
|
});
|
||||||
window.log.info(
|
window.log.info(
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from '../textsecure.d';
|
} from '../textsecure.d';
|
||||||
import { deriveGroupFields, waitThenMaybeUpdateGroup } from '../groups';
|
import { deriveGroupFields, waitThenMaybeUpdateGroup } from '../groups';
|
||||||
import { ConversationModel } from '../models/conversations';
|
import { ConversationModel } from '../models/conversations';
|
||||||
|
import { ConversationAttributesTypeType } from '../model-types.d';
|
||||||
|
|
||||||
const { updateConversation } = dataInterface;
|
const { updateConversation } = dataInterface;
|
||||||
|
|
||||||
|
@ -496,6 +497,7 @@ export async function mergeAccountRecord(
|
||||||
avatarUrl,
|
avatarUrl,
|
||||||
linkPreviews,
|
linkPreviews,
|
||||||
noteToSelfArchived,
|
noteToSelfArchived,
|
||||||
|
pinnedConversations: remotelyPinnedConversationClasses,
|
||||||
profileKey,
|
profileKey,
|
||||||
readReceipts,
|
readReceipts,
|
||||||
sealedSenderIndicators,
|
sealedSenderIndicators,
|
||||||
|
@ -520,6 +522,104 @@ export async function mergeAccountRecord(
|
||||||
window.storage.put('profileKey', profileKey.toArrayBuffer());
|
window.storage.put('profileKey', profileKey.toArrayBuffer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (remotelyPinnedConversationClasses) {
|
||||||
|
const locallyPinnedConversations = window.ConversationController._conversations.filter(
|
||||||
|
conversation => Boolean(conversation.get('isPinned'))
|
||||||
|
);
|
||||||
|
|
||||||
|
const remotelyPinnedConversationPromises = remotelyPinnedConversationClasses.map(
|
||||||
|
async pinnedConversation => {
|
||||||
|
let conversationId;
|
||||||
|
let conversationType: ConversationAttributesTypeType = 'private';
|
||||||
|
|
||||||
|
switch (pinnedConversation.identifier) {
|
||||||
|
case 'contact': {
|
||||||
|
if (!pinnedConversation.contact) {
|
||||||
|
throw new Error('mergeAccountRecord: no contact found');
|
||||||
|
}
|
||||||
|
conversationId = window.ConversationController.ensureContactIds(
|
||||||
|
pinnedConversation.contact
|
||||||
|
);
|
||||||
|
conversationType = 'private';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'legacyGroupId': {
|
||||||
|
if (!pinnedConversation.legacyGroupId) {
|
||||||
|
throw new Error('mergeAccountRecord: no legacyGroupId found');
|
||||||
|
}
|
||||||
|
conversationId = pinnedConversation.legacyGroupId.toBinary();
|
||||||
|
conversationType = 'group';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'groupMasterKey': {
|
||||||
|
if (!pinnedConversation.groupMasterKey) {
|
||||||
|
throw new Error('mergeAccountRecord: no groupMasterKey found');
|
||||||
|
}
|
||||||
|
const masterKeyBuffer = pinnedConversation.groupMasterKey.toArrayBuffer();
|
||||||
|
const groupFields = deriveGroupFields(masterKeyBuffer);
|
||||||
|
const groupId = arrayBufferToBase64(groupFields.id);
|
||||||
|
|
||||||
|
conversationId = groupId;
|
||||||
|
conversationType = 'group';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
window.log.error('mergeAccountRecord: Invalid identifier received');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!conversationId) {
|
||||||
|
window.log.error(
|
||||||
|
`mergeAccountRecord: missing conversation id. looking based on ${pinnedConversation.identifier}`
|
||||||
|
);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conversationType === 'private') {
|
||||||
|
return window.ConversationController.getOrCreateAndWait(
|
||||||
|
conversationId,
|
||||||
|
conversationType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.ConversationController.get(conversationId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const remotelyPinnedConversations = (
|
||||||
|
await Promise.all(remotelyPinnedConversationPromises)
|
||||||
|
).filter(
|
||||||
|
(conversation): conversation is ConversationModel =>
|
||||||
|
conversation !== undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
const remotelyPinnedConversationIds = remotelyPinnedConversations.map(
|
||||||
|
({ id }) => id
|
||||||
|
);
|
||||||
|
|
||||||
|
const conversationsToUnpin = locallyPinnedConversations.filter(
|
||||||
|
({ id }) => !remotelyPinnedConversationIds.includes(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`mergeAccountRecord: unpinning ${conversationsToUnpin.length} conversations`
|
||||||
|
);
|
||||||
|
|
||||||
|
window.log.info(
|
||||||
|
`mergeAccountRecord: pinning ${conversationsToUnpin.length} conversations`
|
||||||
|
);
|
||||||
|
|
||||||
|
conversationsToUnpin.forEach(conversation => {
|
||||||
|
conversation.set({ isPinned: false, pinIndex: undefined });
|
||||||
|
updateConversation(conversation.attributes);
|
||||||
|
});
|
||||||
|
|
||||||
|
remotelyPinnedConversations.forEach((conversation, index) => {
|
||||||
|
conversation.set({ isPinned: true, pinIndex: index });
|
||||||
|
updateConversation(conversation.attributes);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const ourID = window.ConversationController.getOurConversationId();
|
const ourID = window.ConversationController.getOurConversationId();
|
||||||
|
|
||||||
if (!ourID) {
|
if (!ourID) {
|
||||||
|
|
|
@ -45,6 +45,7 @@ export type ConversationType = {
|
||||||
color?: ColorType;
|
color?: ColorType;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
isBlocked?: boolean;
|
isBlocked?: boolean;
|
||||||
|
isPinned?: boolean;
|
||||||
isVerified?: boolean;
|
isVerified?: boolean;
|
||||||
activeAt?: number;
|
activeAt?: number;
|
||||||
timestamp?: number;
|
timestamp?: number;
|
||||||
|
@ -54,6 +55,7 @@ export type ConversationType = {
|
||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
phoneNumber?: string;
|
phoneNumber?: string;
|
||||||
|
pinIndex?: number;
|
||||||
membersCount?: number;
|
membersCount?: number;
|
||||||
muteExpiresAt?: number;
|
muteExpiresAt?: number;
|
||||||
type: ConversationTypeType;
|
type: ConversationTypeType;
|
||||||
|
|
|
@ -128,9 +128,11 @@ export const _getLeftPaneLists = (
|
||||||
): {
|
): {
|
||||||
conversations: Array<ConversationType>;
|
conversations: Array<ConversationType>;
|
||||||
archivedConversations: Array<ConversationType>;
|
archivedConversations: Array<ConversationType>;
|
||||||
|
pinnedConversations: Array<ConversationType>;
|
||||||
} => {
|
} => {
|
||||||
const conversations: Array<ConversationType> = [];
|
const conversations: Array<ConversationType> = [];
|
||||||
const archivedConversations: Array<ConversationType> = [];
|
const archivedConversations: Array<ConversationType> = [];
|
||||||
|
const pinnedConversations: Array<ConversationType> = [];
|
||||||
|
|
||||||
const values = Object.values(lookup);
|
const values = Object.values(lookup);
|
||||||
const max = values.length;
|
const max = values.length;
|
||||||
|
@ -146,6 +148,8 @@ export const _getLeftPaneLists = (
|
||||||
|
|
||||||
if (conversation.isArchived) {
|
if (conversation.isArchived) {
|
||||||
archivedConversations.push(conversation);
|
archivedConversations.push(conversation);
|
||||||
|
} else if (conversation.isPinned) {
|
||||||
|
pinnedConversations.push(conversation);
|
||||||
} else {
|
} else {
|
||||||
conversations.push(conversation);
|
conversations.push(conversation);
|
||||||
}
|
}
|
||||||
|
@ -154,8 +158,9 @@ export const _getLeftPaneLists = (
|
||||||
|
|
||||||
conversations.sort(comparator);
|
conversations.sort(comparator);
|
||||||
archivedConversations.sort(comparator);
|
archivedConversations.sort(comparator);
|
||||||
|
pinnedConversations.sort((a, b) => (a.pinIndex || 0) - (b.pinIndex || 0));
|
||||||
|
|
||||||
return { conversations, archivedConversations };
|
return { conversations, archivedConversations, pinnedConversations };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLeftPaneLists = createSelector(
|
export const getLeftPaneLists = createSelector(
|
||||||
|
|
15
ts/textsecure.d.ts
vendored
15
ts/textsecure.d.ts
vendored
|
@ -961,6 +961,20 @@ export declare class GroupV2RecordClass {
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export declare class PinnedConversationClass {
|
||||||
|
toArrayBuffer: () => ArrayBuffer;
|
||||||
|
|
||||||
|
// identifier is produced by the oneof field in the PinnedConversation protobuf
|
||||||
|
// and determined which one of the following optional fields are in use
|
||||||
|
identifier: 'contact' | 'legacyGroupId' | 'groupMasterKey';
|
||||||
|
contact?: {
|
||||||
|
uuid?: string;
|
||||||
|
e164?: string;
|
||||||
|
};
|
||||||
|
legacyGroupId?: ProtoBinaryType;
|
||||||
|
groupMasterKey?: ProtoBinaryType;
|
||||||
|
}
|
||||||
|
|
||||||
export declare class AccountRecordClass {
|
export declare class AccountRecordClass {
|
||||||
static decode: (
|
static decode: (
|
||||||
data: ArrayBuffer | ByteBufferClass,
|
data: ArrayBuffer | ByteBufferClass,
|
||||||
|
@ -977,6 +991,7 @@ export declare class AccountRecordClass {
|
||||||
sealedSenderIndicators?: boolean | null;
|
sealedSenderIndicators?: boolean | null;
|
||||||
typingIndicators?: boolean | null;
|
typingIndicators?: boolean | null;
|
||||||
linkPreviews?: boolean | null;
|
linkPreviews?: boolean | null;
|
||||||
|
pinnedConversations?: PinnedConversationClass[];
|
||||||
|
|
||||||
__unknownFields?: ArrayBuffer;
|
__unknownFields?: ArrayBuffer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -12932,7 +12932,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/LeftPane.js",
|
"path": "ts/components/LeftPane.js",
|
||||||
"line": " this.listRef = react_1.default.createRef();",
|
"line": " this.listRef = react_1.default.createRef();",
|
||||||
"lineNumber": 16,
|
"lineNumber": 30,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-11T17:24:56.124Z",
|
||||||
"reasonDetail": "Used for scroll calculations"
|
"reasonDetail": "Used for scroll calculations"
|
||||||
|
@ -12941,7 +12941,7 @@
|
||||||
"rule": "React-createRef",
|
"rule": "React-createRef",
|
||||||
"path": "ts/components/LeftPane.js",
|
"path": "ts/components/LeftPane.js",
|
||||||
"line": " this.containerRef = react_1.default.createRef();",
|
"line": " this.containerRef = react_1.default.createRef();",
|
||||||
"lineNumber": 17,
|
"lineNumber": 31,
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2020-09-11T17:24:56.124Z",
|
"updated": "2020-09-11T17:24:56.124Z",
|
||||||
"reasonDetail": "Used for scroll calculations"
|
"reasonDetail": "Used for scroll calculations"
|
||||||
|
|
Loading…
Add table
Reference in a new issue