Mark conversation as unread

Co-authored-by: Sidney Keese <sidney@carbonfive.com>
This commit is contained in:
Chris Svenningsen 2020-10-28 15:54:32 -07:00 committed by Evan Hahn
parent 184f7e1bf3
commit c408072576
20 changed files with 169 additions and 62 deletions

View file

@ -210,11 +210,15 @@
"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"
}, },
"archiveConversation": { "archiveConversation": {
"message": "Archive Conversation", "message": "Archive",
"description": "Shown in menu for conversation, and moves conversation out of main conversation list" "description": "Shown in menu for conversation, and moves conversation out of main conversation list"
}, },
"markUnread": {
"message": "Mark as unread",
"description": "Shown in menu for conversation, and marks conversation as unread"
},
"moveConversationToInbox": { "moveConversationToInbox": {
"message": "Move Conversation to Inbox", "message": "Unarchive",
"description": "Undoes Archive Conversation action, and moves archived conversation back to the main conversation list" "description": "Undoes Archive Conversation action, and moves archived conversation back to the main conversation list"
}, },
"pinConversation": { "pinConversation": {
@ -1039,7 +1043,7 @@
"description": "Shown on the drop-down menu for an individual message, deletes single message for everyone" "description": "Shown on the drop-down menu for an individual message, deletes single message for everyone"
}, },
"deleteMessages": { "deleteMessages": {
"message": "Delete messages", "message": "Delete",
"description": "Menu item for deleting messages, title case." "description": "Menu item for deleting messages, title case."
}, },
"deleteConversationConfirmation": { "deleteConversationConfirmation": {
@ -2361,6 +2365,10 @@
"message": "Conversation returned to inbox", "message": "Conversation returned to inbox",
"description": "A toast that shows up when the user unarchives a conversation" "description": "A toast that shows up when the user unarchives a conversation"
}, },
"conversationMarkedUnread": {
"message": "Conversation marked unread",
"description": "A toast that shows up when user marks a conversation as unread"
},
"StickerCreator--title": { "StickerCreator--title": {
"message": "Sticker pack creator", "message": "Sticker pack creator",
"description": "The title of the Sticker Pack Creator window" "description": "The title of the Sticker Pack Creator window"

View file

@ -205,6 +205,8 @@
'private' 'private'
); );
conversation.setMarkedUnread(false);
const { openConversationExternal } = window.reduxActions.conversations; const { openConversationExternal } = window.reduxActions.conversations;
if (openConversationExternal) { if (openConversationExternal) {
openConversationExternal(id, messageId); openConversationExternal(id, messageId);

View file

@ -73,20 +73,23 @@ message ContactRecord {
optional bool blocked = 9; optional bool blocked = 9;
optional bool whitelisted = 10; optional bool whitelisted = 10;
optional bool archived = 11; optional bool archived = 11;
optional bool markedUnread = 12;
} }
message GroupV1Record { message GroupV1Record {
optional bytes id = 1; optional bytes id = 1;
optional bool blocked = 2; optional bool blocked = 2;
optional bool whitelisted = 3; optional bool whitelisted = 3;
optional bool archived = 4; optional bool archived = 4;
optional bool markedUnread = 5;
} }
message GroupV2Record { message GroupV2Record {
optional bytes masterKey = 1; optional bytes masterKey = 1;
optional bool blocked = 2; optional bool blocked = 2;
optional bool whitelisted = 3; optional bool whitelisted = 3;
optional bool archived = 4; optional bool archived = 4;
optional bool markedUnread = 5;
} }
message AccountRecord { message AccountRecord {
@ -112,7 +115,7 @@ message AccountRecord {
optional bool sealedSenderIndicators = 7; optional bool sealedSenderIndicators = 7;
optional bool typingIndicators = 8; optional bool typingIndicators = 8;
optional bool proxiedLinkPreviews = 9; optional bool proxiedLinkPreviews = 9;
optional bool noteToSelfUnread = 10; optional bool noteToSelfMarkedUnread = 10;
optional bool linkPreviews = 11; optional bool linkPreviews = 11;
repeated PinnedConversation pinnedConversations = 14; repeated PinnedConversation pinnedConversations = 14;
} }

View file

@ -37,6 +37,7 @@ const createProps = (overrideProps: Partial<Props> = {}): Props => ({
name: overrideProps.name || 'Some Person', name: overrideProps.name || 'Some Person',
type: overrideProps.type || 'direct', type: overrideProps.type || 'direct',
onClick: action('onClick'), onClick: action('onClick'),
markedUnread: boolean('markedUnread', overrideProps.markedUnread || false),
lastMessage: overrideProps.lastMessage || { lastMessage: overrideProps.lastMessage || {
text: text('lastMessage.text', 'Hi there!'), text: text('lastMessage.text', 'Hi there!'),
status: select( status: select(
@ -137,18 +138,32 @@ story.add('Message Request', () => {
story.add('Unread', () => { story.add('Unread', () => {
const counts = [4, 10, 250]; const counts = [4, 10, 250];
const defaultProps = createProps({
lastMessage: {
text: 'Hey there!',
status: 'delivered',
},
});
return counts.map(unreadCount => { const items = counts.map(unreadCount => {
const props = createProps({ const props = {
lastMessage: { ...defaultProps,
text: 'Hey there!',
status: 'delivered',
},
unreadCount, unreadCount,
}); };
return <ConversationListItem key={unreadCount} {...props} />; return <ConversationListItem key={unreadCount} {...props} />;
}); });
const markedUnreadProps = {
...defaultProps,
markedUnread: true,
};
const markedUnreadItem = [
<ConversationListItem key={5} {...markedUnreadProps} />,
];
return [...items, ...markedUnreadItem];
}); });
story.add('Selected', () => { story.add('Selected', () => {

View file

@ -37,6 +37,7 @@ export type PropsData = {
lastUpdated: number; lastUpdated: number;
unreadCount?: number; unreadCount?: number;
markedUnread: boolean;
isSelected: boolean; isSelected: boolean;
acceptedMessageRequest?: boolean; acceptedMessageRequest?: boolean;
@ -93,13 +94,19 @@ export class ConversationListItem extends React.PureComponent<Props> {
); );
} }
isUnread(): boolean {
const { markedUnread, unreadCount } = this.props;
return (isNumber(unreadCount) && unreadCount > 0) || markedUnread;
}
public renderUnread(): JSX.Element | null { public renderUnread(): JSX.Element | null {
const { unreadCount } = this.props; const { unreadCount } = this.props;
if (isNumber(unreadCount) && unreadCount > 0) { if (this.isUnread()) {
return ( return (
<div className="module-conversation-list-item__unread-count"> <div className="module-conversation-list-item__unread-count">
{unreadCount} {unreadCount || ''}
</div> </div>
); );
} }
@ -109,7 +116,6 @@ export class ConversationListItem extends React.PureComponent<Props> {
public renderHeader(): JSX.Element { public renderHeader(): JSX.Element {
const { const {
unreadCount,
i18n, i18n,
isMe, isMe,
lastUpdated, lastUpdated,
@ -119,14 +125,12 @@ export class ConversationListItem extends React.PureComponent<Props> {
title, title,
} = this.props; } = this.props;
const withUnread = isNumber(unreadCount) && unreadCount > 0;
return ( return (
<div className="module-conversation-list-item__header"> <div className="module-conversation-list-item__header">
<div <div
className={classNames( className={classNames(
'module-conversation-list-item__header__name', 'module-conversation-list-item__header__name',
withUnread this.isUnread()
? 'module-conversation-list-item__header__name--with-unread' ? 'module-conversation-list-item__header__name--with-unread'
: null : null
)} )}
@ -146,7 +150,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
<div <div
className={classNames( className={classNames(
'module-conversation-list-item__header__date', 'module-conversation-list-item__header__date',
withUnread this.isUnread()
? 'module-conversation-list-item__header__date--has-unread' ? 'module-conversation-list-item__header__date--has-unread'
: null : null
)} )}
@ -155,7 +159,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
timestamp={lastUpdated} timestamp={lastUpdated}
extended={false} extended={false}
module="module-conversation-list-item__header__timestamp" module="module-conversation-list-item__header__timestamp"
withUnread={withUnread} withUnread={this.isUnread()}
i18n={i18n} i18n={i18n}
/> />
</div> </div>
@ -172,14 +176,12 @@ export class ConversationListItem extends React.PureComponent<Props> {
muteExpiresAt, muteExpiresAt,
shouldShowDraft, shouldShowDraft,
typingContact, typingContact,
unreadCount,
} = this.props; } = this.props;
if (!lastMessage && !typingContact) { if (!lastMessage && !typingContact) {
return null; return null;
} }
const messageBody = lastMessage ? lastMessage.text : ''; const messageBody = lastMessage ? lastMessage.text : '';
const withUnread = isNumber(unreadCount) && unreadCount > 0;
const showingDraft = shouldShowDraft && draftPreview; const showingDraft = shouldShowDraft && draftPreview;
const deletedForEveryone = Boolean( const deletedForEveryone = Boolean(
lastMessage && lastMessage.deletedForEveryone lastMessage && lastMessage.deletedForEveryone
@ -192,7 +194,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
dir="auto" dir="auto"
className={classNames( className={classNames(
'module-conversation-list-item__message__text', 'module-conversation-list-item__message__text',
withUnread this.isUnread()
? 'module-conversation-list-item__message__text--has-unread' ? 'module-conversation-list-item__message__text--has-unread'
: null : null
)} )}
@ -249,8 +251,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
/* eslint-enable no-nested-ternary */ /* eslint-enable no-nested-ternary */
public render(): JSX.Element { public render(): JSX.Element {
const { unreadCount, onClick, id, isSelected, style } = this.props; const { id, isSelected, onClick, style } = this.props;
const withUnread = isNumber(unreadCount) && unreadCount > 0;
return ( return (
<button <button
@ -263,7 +264,7 @@ export class ConversationListItem extends React.PureComponent<Props> {
style={style} style={style}
className={classNames( className={classNames(
'module-conversation-list-item', 'module-conversation-list-item',
withUnread ? 'module-conversation-list-item--has-unread' : null, this.isUnread() ? 'module-conversation-list-item--has-unread' : null,
isSelected ? 'module-conversation-list-item--is-selected' : null isSelected ? 'module-conversation-list-item--is-selected' : null
)} )}
data-id={cleanId(id)} data-id={cleanId(id)}

View file

@ -18,6 +18,7 @@ const defaultConversations: Array<PropsData> = [
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },
@ -25,6 +26,7 @@ const defaultConversations: Array<PropsData> = [
id: 'marc-convo', id: 'marc-convo',
isSelected: true, isSelected: true,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Marc Barraca', title: 'Marc Barraca',
type: 'direct', type: 'direct',
}, },
@ -35,6 +37,7 @@ const defaultArchivedConversations: Array<PropsData> = [
id: 'michelle-archive-convo', id: 'michelle-archive-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Michelle Mercure', title: 'Michelle Mercure',
type: 'direct', type: 'direct',
}, },
@ -46,6 +49,7 @@ const pinnedConversations: Array<PropsData> = [
isPinned: true, isPinned: true,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Philip Glass', title: 'Philip Glass',
type: 'direct', type: 'direct',
}, },
@ -54,6 +58,7 @@ const pinnedConversations: Array<PropsData> = [
isPinned: true, isPinned: true,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Robert Moog', title: 'Robert Moog',
type: 'direct', type: 'direct',
}, },
@ -129,6 +134,7 @@ story.add('Search Results', () => {
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'People Named Fred', title: 'People Named Fred',
type: 'group', type: 'group',
}, },
@ -147,6 +153,7 @@ story.add('Search Results', () => {
id: 'fred-contact', id: 'fred-contact',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },

View file

@ -157,6 +157,7 @@ const conversations = [
text: 'The rabbit hopped silently in the night.', text: 'The rabbit hopped silently in the night.',
status: SENT, status: SENT,
}, },
markedUnread: false,
}, },
}, },
{ {
@ -177,6 +178,7 @@ const conversations = [
text: "What's going on?", text: "What's going on?",
status: SENT, status: SENT,
}, },
markedUnread: false,
}, },
}, },
]; ];
@ -196,6 +198,7 @@ const contacts = [
lastUpdated: Date.now() - 10 * 60 * 1000, lastUpdated: Date.now() - 10 * 60 * 1000,
unreadCount: 0, unreadCount: 0,
isSelected: false, isSelected: false,
markedUnread: false,
}, },
}, },
{ {
@ -211,6 +214,7 @@ const contacts = [
lastUpdated: Date.now() - 11 * 60 * 1000, lastUpdated: Date.now() - 11 * 60 * 1000,
unreadCount: 0, unreadCount: 0,
isSelected: false, isSelected: false,
markedUnread: false,
}, },
}, },
]; ];

View file

@ -44,6 +44,7 @@ const actionProps: PropsActionsType = {
onGoBack: action('onGoBack'), onGoBack: action('onGoBack'),
onArchive: action('onArchive'), onArchive: action('onArchive'),
onMarkUnread: action('onMarkUnread'),
onMoveToInbox: action('onMoveToInbox'), onMoveToInbox: action('onMoveToInbox'),
onSetPin: action('onSetPin'), onSetPin: action('onSetPin'),
}; };

View file

@ -36,6 +36,7 @@ export interface PropsDataType {
isMe?: boolean; isMe?: boolean;
isArchived?: boolean; isArchived?: boolean;
isPinned?: boolean; isPinned?: boolean;
markedUnread?: boolean;
disableTimerChanges?: boolean; disableTimerChanges?: boolean;
expirationSettingName?: string; expirationSettingName?: string;
@ -60,6 +61,7 @@ export interface PropsActionsType {
onGoBack: () => void; onGoBack: () => void;
onArchive: () => void; onArchive: () => void;
onMarkUnread: () => void;
onMoveToInbox: () => void; onMoveToInbox: () => void;
} }
@ -310,6 +312,7 @@ export class ConversationHeader extends React.Component<PropsType> {
isPinned, isPinned,
type, type,
isArchived, isArchived,
markedUnread,
muteExpirationLabel, muteExpirationLabel,
onDeleteMessages, onDeleteMessages,
onResetSession, onResetSession,
@ -319,6 +322,7 @@ export class ConversationHeader extends React.Component<PropsType> {
onShowGroupMembers, onShowGroupMembers,
onShowSafetyNumber, onShowSafetyNumber,
onArchive, onArchive,
onMarkUnread,
onSetPin, onSetPin,
onMoveToInbox, onMoveToInbox,
timerOptions, timerOptions,
@ -350,28 +354,6 @@ export class ConversationHeader extends React.Component<PropsType> {
return ( return (
<ContextMenu id={triggerId}> <ContextMenu id={triggerId}>
<SubMenu title={muteTitle}>
{muteOptions.map(item => (
<MenuItem
key={item.name}
disabled={item.disabled}
onClick={() => {
onSetMuteNotifications(item.value);
}}
>
{item.name}
</MenuItem>
))}
</SubMenu>
{isPinned ? (
<MenuItem onClick={() => onSetPin(false)}>
{i18n('unpinConversation')}
</MenuItem>
) : (
<MenuItem onClick={() => onSetPin(true)}>
{i18n('pinConversation')}
</MenuItem>
)}
{disableTimerChanges ? null : ( {disableTimerChanges ? null : (
<SubMenu title={disappearingTitle}> <SubMenu title={disappearingTitle}>
{(timerOptions || []).map(item => ( {(timerOptions || []).map(item => (
@ -386,21 +368,37 @@ export class ConversationHeader extends React.Component<PropsType> {
))} ))}
</SubMenu> </SubMenu>
)} )}
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem> <SubMenu title={muteTitle}>
{muteOptions.map(item => (
<MenuItem
key={item.name}
disabled={item.disabled}
onClick={() => {
onSetMuteNotifications(item.value);
}}
>
{item.name}
</MenuItem>
))}
</SubMenu>
{isGroup ? ( {isGroup ? (
<MenuItem onClick={onShowGroupMembers}> <MenuItem onClick={onShowGroupMembers}>
{i18n('showMembers')} {i18n('showMembers')}
</MenuItem> </MenuItem>
) : null} ) : null}
<MenuItem onClick={onShowAllMedia}>{i18n('viewRecentMedia')}</MenuItem>
{!isGroup && !isMe ? ( {!isGroup && !isMe ? (
<MenuItem onClick={onShowSafetyNumber}> <MenuItem onClick={onShowSafetyNumber}>
{i18n('showSafetyNumber')} {i18n('showSafetyNumber')}
</MenuItem> </MenuItem>
) : null} ) : null}
<MenuItem divider />
{!isGroup && acceptedMessageRequest ? ( {!isGroup && acceptedMessageRequest ? (
<MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem> <MenuItem onClick={onResetSession}>{i18n('resetSession')}</MenuItem>
) : null} ) : null}
<MenuItem divider />
{!markedUnread ? (
<MenuItem onClick={onMarkUnread}>{i18n('markUnread')}</MenuItem>
) : null}
{isArchived ? ( {isArchived ? (
<MenuItem onClick={onMoveToInbox}> <MenuItem onClick={onMoveToInbox}>
{i18n('moveConversationToInbox')} {i18n('moveConversationToInbox')}
@ -409,6 +407,15 @@ export class ConversationHeader extends React.Component<PropsType> {
<MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem> <MenuItem onClick={onArchive}>{i18n('archiveConversation')}</MenuItem>
)} )}
<MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem> <MenuItem onClick={onDeleteMessages}>{i18n('deleteMessages')}</MenuItem>
{isPinned ? (
<MenuItem onClick={() => onSetPin(false)}>
{i18n('unpinConversation')}
</MenuItem>
) : (
<MenuItem onClick={() => onSetPin(true)}>
{i18n('pinConversation')}
</MenuItem>
)}
</ContextMenu> </ContextMenu>
); );
} }

View file

@ -19,6 +19,7 @@ storiesOf('Components/Conversation/ProfileChangeNotification', module)
title: 'Mr. Fire 🔥', title: 'Mr. Fire 🔥',
name: 'Mr. Fire 🔥', name: 'Mr. Fire 🔥',
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
}} }}
change={{ change={{
type: 'name', type: 'name',
@ -37,6 +38,7 @@ storiesOf('Components/Conversation/ProfileChangeNotification', module)
type: 'direct', type: 'direct',
title: 'Mr. Fire 🔥', title: 'Mr. Fire 🔥',
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
}} }}
change={{ change={{
type: 'name', type: 'name',

1
ts/model-types.d.ts vendored
View file

@ -149,6 +149,7 @@ export type ConversationAttributesType = {
isPinned: boolean; isPinned: boolean;
lastMessageDeletedForEveryone: boolean; lastMessageDeletedForEveryone: boolean;
lastMessageStatus: LastMessageStatus | null; lastMessageStatus: LastMessageStatus | null;
markedUnread: boolean;
messageCount: number; messageCount: number;
messageCountBeforeMessageRequests: number; messageCountBeforeMessageRequests: number;
messageRequestResponseType: number; messageRequestResponseType: number;

View file

@ -1137,7 +1137,7 @@ export class ConversationModel extends window.Backbone.Model<
deletedForEveryone: this.get('lastMessageDeletedForEveryone')!, deletedForEveryone: this.get('lastMessageDeletedForEveryone')!,
}, },
lastUpdated: this.get('timestamp')!, lastUpdated: this.get('timestamp')!,
markedUnread: this.get('markedUnread')!,
membersCount: this.isPrivate() membersCount: this.isPrivate()
? undefined ? undefined
: (this.get('membersV2')! || this.get('members')! || []).length, : (this.get('membersV2')! || this.get('members')! || []).length,
@ -3019,6 +3019,12 @@ export class ConversationModel extends window.Backbone.Model<
} }
} }
setMarkedUnread(markedUnread: boolean): void {
this.set({ markedUnread });
window.Signal.Data.updateConversation(this.attributes);
this.captureChange('markedUnread');
}
async updateExpirationTimer( async updateExpirationTimer(
providedExpireTimer: number | undefined, providedExpireTimer: number | undefined,
providedSource: unknown, providedSource: unknown,
@ -3998,6 +4004,7 @@ export class ConversationModel extends window.Backbone.Model<
// [X] blocked // [X] blocked
// [X] whitelisted // [X] whitelisted
// [X] archived // [X] archived
// [X] markedUnread
captureChange(property: string): void { captureChange(property: string): void {
if (!window.Signal.RemoteConfig.isEnabled('desktop.storageWrite')) { if (!window.Signal.RemoteConfig.isEnabled('desktop.storageWrite')) {
window.log.info( window.log.info(

View file

@ -113,6 +113,7 @@ export async function toContactRecord(
contactRecord.blocked = conversation.isBlocked(); contactRecord.blocked = conversation.isBlocked();
contactRecord.whitelisted = Boolean(conversation.get('profileSharing')); contactRecord.whitelisted = Boolean(conversation.get('profileSharing'));
contactRecord.archived = Boolean(conversation.get('isArchived')); contactRecord.archived = Boolean(conversation.get('isArchived'));
contactRecord.markedUnread = Boolean(conversation.get('markedUnread'));
applyUnknownFields(contactRecord, conversation); applyUnknownFields(contactRecord, conversation);
@ -137,6 +138,9 @@ export async function toAccountRecord(
} }
accountRecord.avatarUrl = window.storage.get('avatarUrl') || ''; accountRecord.avatarUrl = window.storage.get('avatarUrl') || '';
accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived')); accountRecord.noteToSelfArchived = Boolean(conversation.get('isArchived'));
accountRecord.noteToSelfMarkedUnread = Boolean(
conversation.get('markedUnread')
);
accountRecord.readReceipts = Boolean( accountRecord.readReceipts = Boolean(
window.storage.get('read-receipt-setting') window.storage.get('read-receipt-setting')
); );
@ -218,6 +222,7 @@ export async function toGroupV1Record(
groupV1Record.blocked = conversation.isBlocked(); groupV1Record.blocked = conversation.isBlocked();
groupV1Record.whitelisted = Boolean(conversation.get('profileSharing')); groupV1Record.whitelisted = Boolean(conversation.get('profileSharing'));
groupV1Record.archived = Boolean(conversation.get('isArchived')); groupV1Record.archived = Boolean(conversation.get('isArchived'));
groupV1Record.markedUnread = Boolean(conversation.get('markedUnread'));
applyUnknownFields(groupV1Record, conversation); applyUnknownFields(groupV1Record, conversation);
@ -236,6 +241,7 @@ export async function toGroupV2Record(
groupV2Record.blocked = conversation.isBlocked(); groupV2Record.blocked = conversation.isBlocked();
groupV2Record.whitelisted = Boolean(conversation.get('profileSharing')); groupV2Record.whitelisted = Boolean(conversation.get('profileSharing'));
groupV2Record.archived = Boolean(conversation.get('isArchived')); groupV2Record.archived = Boolean(conversation.get('isArchived'));
groupV2Record.markedUnread = Boolean(conversation.get('markedUnread'));
applyUnknownFields(groupV2Record, conversation); applyUnknownFields(groupV2Record, conversation);
@ -386,6 +392,7 @@ export async function mergeGroupV1Record(
conversation.set({ conversation.set({
isArchived: Boolean(groupV1Record.archived), isArchived: Boolean(groupV1Record.archived),
markedUnread: Boolean(groupV1Record.markedUnread),
storageID, storageID,
}); });
@ -446,6 +453,7 @@ export async function mergeGroupV2Record(
conversation.set({ conversation.set({
isArchived: Boolean(groupV2Record.archived), isArchived: Boolean(groupV2Record.archived),
markedUnread: Boolean(groupV2Record.markedUnread),
storageID, storageID,
}); });
@ -537,6 +545,7 @@ export async function mergeContactRecord(
conversation.set({ conversation.set({
isArchived: Boolean(contactRecord.archived), isArchived: Boolean(contactRecord.archived),
markedUnread: Boolean(contactRecord.markedUnread),
storageID, storageID,
}); });
@ -559,6 +568,7 @@ export async function mergeAccountRecord(
avatarUrl, avatarUrl,
linkPreviews, linkPreviews,
noteToSelfArchived, noteToSelfArchived,
noteToSelfMarkedUnread,
pinnedConversations: remotelyPinnedConversationClasses, pinnedConversations: remotelyPinnedConversationClasses,
profileKey, profileKey,
readReceipts, readReceipts,
@ -734,6 +744,7 @@ export async function mergeAccountRecord(
conversation.set({ conversation.set({
isArchived: Boolean(noteToSelfArchived), isArchived: Boolean(noteToSelfArchived),
markedUnread: Boolean(noteToSelfMarkedUnread),
storageID, storageID,
}); });

View file

@ -56,6 +56,7 @@ export type ConversationType = {
text: string; text: string;
deletedForEveryone?: boolean; deletedForEveryone?: boolean;
}; };
markedUnread: boolean;
phoneNumber?: string; phoneNumber?: string;
membersCount?: number; membersCount?: number;
muteExpiresAt?: number; muteExpiresAt?: number;

View file

@ -36,6 +36,7 @@ describe('LeftPane', () => {
isPinned: true, isPinned: true,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Philip Glass', title: 'Philip Glass',
type: 'direct', type: 'direct',
}, },
@ -44,6 +45,7 @@ describe('LeftPane', () => {
isPinned: true, isPinned: true,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Robert Moog', title: 'Robert Moog',
type: 'direct', type: 'direct',
}, },
@ -70,6 +72,7 @@ describe('LeftPane', () => {
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },
@ -78,6 +81,7 @@ describe('LeftPane', () => {
isPinned: false, isPinned: false,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Robert Moog', title: 'Robert Moog',
type: 'direct', type: 'direct',
}, },
@ -104,6 +108,7 @@ describe('LeftPane', () => {
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },
@ -114,6 +119,7 @@ describe('LeftPane', () => {
isPinned: true, isPinned: true,
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Philip Glass', title: 'Philip Glass',
type: 'direct', type: 'direct',
}, },
@ -148,6 +154,7 @@ describe('LeftPane', () => {
id: 'jerry-convo', id: 'jerry-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Jerry Jordan', title: 'Jerry Jordan',
type: 'direct', type: 'direct',
}, },
@ -157,6 +164,7 @@ describe('LeftPane', () => {
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },
@ -179,6 +187,7 @@ describe('LeftPane', () => {
id: 'fred-convo', id: 'fred-convo',
isSelected: false, isSelected: false,
lastUpdated: Date.now(), lastUpdated: Date.now(),
markedUnread: false,
title: 'Fred Willard', title: 'Fred Willard',
type: 'direct', type: 'direct',
}, },

View file

@ -41,6 +41,7 @@ describe('state/selectors/conversations', () => {
inboxPosition: 0, inboxPosition: 0,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -65,6 +66,7 @@ describe('state/selectors/conversations', () => {
inboxPosition: 21, inboxPosition: 21,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -89,6 +91,7 @@ describe('state/selectors/conversations', () => {
inboxPosition: 22, inboxPosition: 22,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -113,6 +116,7 @@ describe('state/selectors/conversations', () => {
inboxPosition: 20, inboxPosition: 20,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -137,6 +141,7 @@ describe('state/selectors/conversations', () => {
inboxPosition: 30, inboxPosition: 30,
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -185,6 +190,7 @@ describe('state/selectors/conversations', () => {
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
isPinned: true, isPinned: true,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -210,6 +216,7 @@ describe('state/selectors/conversations', () => {
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
isPinned: true, isPinned: true,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,
@ -235,6 +242,7 @@ describe('state/selectors/conversations', () => {
phoneNumber: 'notused', phoneNumber: 'notused',
isArchived: false, isArchived: false,
isPinned: true, isPinned: true,
markedUnread: false,
type: 'direct', type: 'direct',
isMe: false, isMe: false,

4
ts/textsecure.d.ts vendored
View file

@ -929,6 +929,7 @@ export declare class ContactRecordClass {
blocked?: boolean | null; blocked?: boolean | null;
whitelisted?: boolean | null; whitelisted?: boolean | null;
archived?: boolean | null; archived?: boolean | null;
markedUnread?: boolean;
__unknownFields?: ArrayBuffer; __unknownFields?: ArrayBuffer;
} }
@ -944,6 +945,7 @@ export declare class GroupV1RecordClass {
blocked?: boolean | null; blocked?: boolean | null;
whitelisted?: boolean | null; whitelisted?: boolean | null;
archived?: boolean | null; archived?: boolean | null;
markedUnread?: boolean;
__unknownFields?: ArrayBuffer; __unknownFields?: ArrayBuffer;
} }
@ -959,6 +961,7 @@ export declare class GroupV2RecordClass {
blocked?: boolean | null; blocked?: boolean | null;
whitelisted?: boolean | null; whitelisted?: boolean | null;
archived?: boolean | null; archived?: boolean | null;
markedUnread?: boolean;
__unknownFields?: ArrayBuffer; __unknownFields?: ArrayBuffer;
} }
@ -995,6 +998,7 @@ export declare class AccountRecordClass {
typingIndicators?: boolean | null; typingIndicators?: boolean | null;
linkPreviews?: boolean | null; linkPreviews?: boolean | null;
pinnedConversations?: PinnedConversationClass[]; pinnedConversations?: PinnedConversationClass[];
noteToSelfMarkedUnread?: boolean;
__unknownFields?: ArrayBuffer; __unknownFields?: ArrayBuffer;
} }

View file

@ -631,7 +631,7 @@
"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": 217, "lineNumber": 219,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z", "updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Known DOM elements" "reasonDetail": "Known DOM elements"
@ -640,7 +640,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": 220, "lineNumber": 222,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-29T18:29:18.234Z", "updated": "2020-05-29T18:29:18.234Z",
"reasonDetail": "Hardcoded selector" "reasonDetail": "Hardcoded selector"
@ -14703,7 +14703,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": 84, "lineNumber": 86,
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2020-05-20T20:10:43.540Z", "updated": "2020-05-20T20:10:43.540Z",
"reasonDetail": "Used to reference popup menu" "reasonDetail": "Used to reference popup menu"
@ -15116,4 +15116,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-09-08T23:07:22.682Z" "updated": "2020-09-08T23:07:22.682Z"
} }
] ]

View file

@ -115,6 +115,12 @@ Whisper.ConversationUnarchivedToast = Whisper.ToastView.extend({
}, },
}); });
Whisper.ConversationMarkedUnreadToast = Whisper.ToastView.extend({
render_attributes() {
return { toastMessage: window.i18n('conversationMarkedUnread') };
},
});
Whisper.TapToViewExpiredIncomingToast = Whisper.ToastView.extend({ Whisper.TapToViewExpiredIncomingToast = Whisper.ToastView.extend({
render_attributes() { render_attributes() {
return { return {
@ -544,6 +550,14 @@ Whisper.ConversationView = Whisper.View.extend({
document.body document.body
); );
}, },
onMarkUnread: () => {
this.model.setMarkedUnread(true);
Whisper.ToastView.show(
Whisper.ConversationMarkedUnreadToast,
document.body
);
},
onMoveToInbox: () => { onMoveToInbox: () => {
this.model.setArchived(false); this.model.setArchived(false);
@ -3057,6 +3071,7 @@ Whisper.ConversationView = Whisper.View.extend({
); );
this.compositionApi.current.reset(); this.compositionApi.current.reset();
this.model.setMarkedUnread(false);
this.setQuoteMessage(null); this.setQuoteMessage(null);
this.resetLinkPreview(); this.resetLinkPreview();
this.clearAttachments(); this.clearAttachments();

1
ts/window.d.ts vendored
View file

@ -545,6 +545,7 @@ export type WhisperType = {
}; };
ConversationArchivedToast: WhatIsThis; ConversationArchivedToast: WhatIsThis;
ConversationUnarchivedToast: WhatIsThis; ConversationUnarchivedToast: WhatIsThis;
ConversationMarkedUnreadToast: WhatIsThis;
AppView: WhatIsThis; AppView: WhatIsThis;
WallClockListener: WhatIsThis; WallClockListener: WhatIsThis;
MessageRequests: WhatIsThis; MessageRequests: WhatIsThis;