From b17312c13c2fa006d30141a335ae0b49ff9b0c50 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 17 Sep 2021 08:41:04 +0200 Subject: [PATCH] Cleanup redux store (#1925) * do not consider expire timer update unread messages #1881 * cleanup conversation props in redux to only have what cannot be derived * fix app not starting without the await on convo creation * cleanup props of message model --- stylesheets/_global.scss | 2 +- ts/components/Avatar.tsx | 4 +- ts/components/ContactListItem.tsx | 16 +- ts/components/ConversationListItem.tsx | 45 +++-- ts/components/UserSearchResults.tsx | 8 +- ts/components/conversation/ContactName.tsx | 14 +- .../conversation/ConversationHeader.tsx | 32 ++-- .../conversation/GroupNotification.tsx | 7 +- ts/components/conversation/MessageDetail.tsx | 14 +- ts/components/conversation/Quote.tsx | 2 +- .../conversation/TimerNotification.tsx | 6 +- ts/components/conversation/TypingBubble.tsx | 2 +- .../message/GenericReadableMessage.tsx | 11 +- .../message/MessageAuthorText.tsx | 2 +- .../conversation/message/MessageQuote.tsx | 9 +- ts/components/session/SessionInboxView.tsx | 22 +-- .../SessionMessagesListContainer.tsx | 10 +- .../conversation/SessionRightPanel.tsx | 3 +- .../session/menu/ConversationHeaderMenu.tsx | 11 +- .../menu/ConversationListItemContextMenu.tsx | 13 +- ts/components/session/menu/Menu.tsx | 37 ++-- .../usingClosedConversationDetails.tsx | 4 +- ts/hooks/useMembersAvatar.tsx | 4 +- ts/models/conversation.ts | 169 +++++++++++++----- ts/models/message.ts | 158 +++++++++++----- ts/receiver/queuedJob.ts | 3 +- ts/session/tslint.json | 3 +- ts/state/ducks/conversations.ts | 92 +++++----- ts/state/selectors/conversations.ts | 50 +++--- .../unit/selectors/conversations_test.ts | 32 ++-- ts/test/test-utils/utils/message.ts | 2 +- tslint.json | 1 + 32 files changed, 464 insertions(+), 324 deletions(-) diff --git a/stylesheets/_global.scss b/stylesheets/_global.scss index 589097abc..a07a267ac 100644 --- a/stylesheets/_global.scss +++ b/stylesheets/_global.scss @@ -100,7 +100,7 @@ button.grey { } a { - cursor: auto; + cursor: pointer; user-select: text; } diff --git a/ts/components/Avatar.tsx b/ts/components/Avatar.tsx index a06865576..acc5ead9f 100644 --- a/ts/components/Avatar.tsx +++ b/ts/components/Avatar.tsx @@ -17,7 +17,7 @@ export enum AvatarSize { type Props = { avatarPath?: string | null; - name?: string; // display name, profileName or phoneNumber, whatever is set first + name?: string; // display name, profileName or pubkey, whatever is set first pubkey?: string; size: AvatarSize; base64Data?: string; // if this is not empty, it will be used to render the avatar with base64 encoded data @@ -65,7 +65,7 @@ const NoImage = (props: { const AvatarImage = (props: { avatarPath?: string; base64Data?: string; - name?: string; // display name, profileName or phoneNumber, whatever is set first + name?: string; // display name, profileName or pubkey, whatever is set first imageBroken: boolean; handleImageError: () => any; }) => { diff --git a/ts/components/ContactListItem.tsx b/ts/components/ContactListItem.tsx index fb6853c9f..c7deceba9 100644 --- a/ts/components/ContactListItem.tsx +++ b/ts/components/ContactListItem.tsx @@ -5,7 +5,7 @@ import { Avatar, AvatarSize } from './Avatar'; import { Emojify } from './conversation/Emojify'; interface Props { - phoneNumber: string; + pubkey: string; isMe?: boolean; name?: string; profileName?: string; @@ -15,26 +15,24 @@ interface Props { export class ContactListItem extends React.Component { public renderAvatar() { - const { avatarPath, name, phoneNumber, profileName } = this.props; + const { avatarPath, name, pubkey, profileName } = this.props; - const userName = name || profileName || phoneNumber; + const userName = name || profileName || pubkey; - return ( - - ); + return ; } public render() { - const { name, onClick, isMe, phoneNumber, profileName } = this.props; + const { name, onClick, isMe, pubkey, profileName } = this.props; - const title = name ? name : phoneNumber; + const title = name ? name : pubkey; const displayName = isMe ? window.i18n('me') : title; const profileElement = !isMe && profileName && !name ? ( ~ - + ) : null; diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 0e80dc080..cdb1921f3 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -43,6 +43,7 @@ export const StyledConversationListItemIconWrapper = styled.div` type PropsHousekeeping = { style?: Object; }; +// tslint:disable: use-simple-attributes type Props = ConversationListItemProps & PropsHousekeeping; @@ -165,7 +166,7 @@ const UserItem = (props: { return (
{ type, isPublic, avatarPath, - notificationForConvo, + isPrivate, currentNotificationSetting, } = props; const triggerId = `conversation-item-${conversationId}-ctxmenu`; @@ -294,47 +295,53 @@ const ConversationListItem = (props: Props) => { style={style} className={classNames( 'module-conversation-list-item', - unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, - unreadCount > 0 && mentionedUs ? 'module-conversation-list-item--mentioned-us' : null, + unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, + unreadCount && unreadCount > 0 && mentionedUs + ? 'module-conversation-list-item--mentioned-us' + : null, isSelected ? 'module-conversation-list-item--is-selected' : null, isBlocked ? 'module-conversation-list-item--is-blocked' : null )} >
+ -
diff --git a/ts/components/UserSearchResults.tsx b/ts/components/UserSearchResults.tsx index 1cc4c8e13..295038706 100644 --- a/ts/components/UserSearchResults.tsx +++ b/ts/components/UserSearchResults.tsx @@ -43,10 +43,10 @@ export class UserSearchResults extends React.Component { } private renderContact(contact: ConversationListItemProps, index: Number) { - const { profileName, phoneNumber } = contact; + const { profileName, id } = contact; const { selectedContact } = this.props; - const shortenedPubkey = PubKey.shorten(phoneNumber); + const shortenedPubkey = PubKey.shorten(id); const rowContent = `${profileName} ${shortenedPubkey}`; return ( @@ -55,8 +55,8 @@ export class UserSearchResults extends React.Component { 'contacts-dropdown-row', selectedContact === index && 'contacts-dropdown-row-selected' )} - key={contact.phoneNumber} - onClick={() => this.props.onContactSelected(contact.phoneNumber)} + key={contact.id} + onClick={() => this.props.onContactSelected(contact.id)} role="button" > {rowContent} diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index ab2bdcc72..f906a91cf 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -4,7 +4,7 @@ import classNames from 'classnames'; import { Emojify } from './Emojify'; type Props = { - phoneNumber: string; + pubkey: string; name?: string | null; profileName?: string | null; module?: string; @@ -14,15 +14,7 @@ type Props = { }; export const ContactName = (props: Props) => { - const { - phoneNumber, - name, - profileName, - module, - boldProfileName, - compact, - shouldShowPubkey, - } = props; + const { pubkey, name, profileName, module, boldProfileName, compact, shouldShowPubkey } = props; const prefix = module ? module : 'module-contact-name'; const shouldShowProfile = Boolean(profileName || name); @@ -40,7 +32,7 @@ export const ContactName = (props: Props) => { const pubKeyElement = shouldShowPubkey ? ( - + ) : null; diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 4a0b622c5..f321a6a6a 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -27,7 +27,6 @@ import { deleteMessagesById } from '../../interactions/conversationInteractions' import { closeMessageDetailsView, closeRightPanel, - NotificationForConvoOption, openRightPanel, resetSelectedMessageIds, } from '../../state/ducks/conversations'; @@ -38,10 +37,9 @@ export interface TimerOption { } export type ConversationHeaderProps = { - id: string; + conversationKey: string; name?: string; - phoneNumber: string; profileName?: string; avatarPath: string | null; @@ -60,7 +58,6 @@ export type ConversationHeaderProps = { subscriberCount?: number; expirationSettingName?: string; - notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; hasNickname: boolean; @@ -138,13 +135,13 @@ const AvatarHeader = (props: { avatarPath: string | null; memberAvatars?: Array; name?: string; - phoneNumber: string; + pubkey: string; profileName?: string; showBackButton: boolean; onAvatarClick?: (pubkey: string) => void; }) => { - const { avatarPath, memberAvatars, name, phoneNumber, profileName } = props; - const userName = name || profileName || phoneNumber; + const { avatarPath, memberAvatars, name, pubkey, profileName } = props; + const userName = name || profileName || pubkey; return ( @@ -155,11 +152,11 @@ const AvatarHeader = (props: { onAvatarClick={() => { // do not allow right panel to appear if another button is shown on the SessionConversation if (props.onAvatarClick && !props.showBackButton) { - props.onAvatarClick(phoneNumber); + props.onAvatarClick(pubkey); } }} memberAvatars={memberAvatars} - pubkey={phoneNumber} + pubkey={pubkey} /> ); @@ -188,7 +185,7 @@ export const StyledSubtitleContainer = styled.div` `; export type ConversationHeaderTitleProps = { - phoneNumber: string; + conversationKey: string; profileName?: string; isMe: boolean; isGroup: boolean; @@ -210,7 +207,7 @@ const ConversationHeaderTitle = () => { } const { - phoneNumber, + conversationKey, profileName, isGroup, isPublic, @@ -249,7 +246,7 @@ const ConversationHeaderTitle = () => { ? `${memberCountText} ● ${notificationSubtitle}` : `${notificationSubtitle}`; - const title = profileName || name || phoneNumber; + const title = profileName || name || conversationKey; return (
{ const { isKickedFromGroup, expirationSettingName, - phoneNumber, avatarPath, name, profileName, - id, isMe, isPublic, - notificationForConvo, currentNotificationSetting, hasNickname, weAreAdmin, isBlocked, left, + conversationKey, isPrivate, isGroup, } = headerProps; @@ -343,7 +338,7 @@ export const ConversationHeaderWithDetails = () => { onAvatarClick={() => { dispatch(openRightPanel()); }} - phoneNumber={phoneNumber} + pubkey={conversationKey} showBackButton={isMessageDetailOpened} avatarPath={avatarPath} memberAvatars={memberDetails} @@ -353,7 +348,7 @@ export const ConversationHeaderWithDetails = () => { )} { isPrivate={isPrivate} left={left} hasNickname={hasNickname} - notificationForConvo={notificationForConvo} currentNotificationSetting={currentNotificationSetting} />
@@ -374,7 +368,7 @@ export const ConversationHeaderWithDetails = () => { isPublic={isPublic} onCloseOverlay={() => dispatch(resetSelectedMessageIds())} onDeleteSelectedMessages={() => { - void deleteMessagesById(selectedMessageIds, id, true); + void deleteMessagesById(selectedMessageIds, conversationKey, true); }} /> )} diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index 033a03b60..b74263483 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -29,11 +29,8 @@ function getPeople(change: TypeWithContacts) { flatten( (change.contacts || []).map((contact, index) => { const element = ( - - {contact.profileName || contact.phoneNumber} + + {contact.profileName || contact.pubkey} ); diff --git a/ts/components/conversation/MessageDetail.tsx b/ts/components/conversation/MessageDetail.tsx index 90179e062..183c4208c 100644 --- a/ts/components/conversation/MessageDetail.tsx +++ b/ts/components/conversation/MessageDetail.tsx @@ -14,12 +14,10 @@ import { } from '../../state/selectors/conversations'; const AvatarItem = (props: { contact: ContactPropsMessageDetail }) => { - const { avatarPath, phoneNumber, name, profileName } = props.contact; - const userName = name || profileName || phoneNumber; + const { avatarPath, pubkey, name, profileName } = props.contact; + const userName = name || profileName || pubkey; - return ( - - ); + return ; }; const DeleteButtonItem = (props: { messageId: string; convoId: string; isDeletable: boolean }) => { @@ -49,7 +47,7 @@ const ContactsItem = (props: { contacts: Array }) => return (
{contacts.map(contact => ( - + ))}
); @@ -69,12 +67,12 @@ const ContactItem = (props: { contact: ContactPropsMessageDetail }) => { ) : null; return ( -
+
{ window.i18n('you') ) : ( { - const { phoneNumber, profileName, timespan, type, disabled } = props; + const { pubkey, profileName, timespan, type, disabled } = props; const changeKey = disabled ? 'disabledDisappearingMessages' : 'theyChangedTheTimer'; const contact = ( - - {profileName || phoneNumber} + + {profileName || pubkey} ); diff --git a/ts/components/conversation/TypingBubble.tsx b/ts/components/conversation/TypingBubble.tsx index 9831c9746..d36f0d8e3 100644 --- a/ts/components/conversation/TypingBubble.tsx +++ b/ts/components/conversation/TypingBubble.tsx @@ -6,7 +6,7 @@ import { ConversationTypeEnum } from '../../models/conversation'; interface TypingBubbleProps { avatarPath?: string; - phoneNumber: string; + pubkey: string; displayedName: string | null; conversationType: ConversationTypeEnum; isTyping: boolean; diff --git a/ts/components/conversation/message/GenericReadableMessage.tsx b/ts/components/conversation/message/GenericReadableMessage.tsx index 1b555c345..ebf7c9202 100644 --- a/ts/components/conversation/message/GenericReadableMessage.tsx +++ b/ts/components/conversation/message/GenericReadableMessage.tsx @@ -94,6 +94,7 @@ type Props = { ctxMenuID: string; isDetailView?: boolean; }; +// tslint:disable: use-simple-attributes export const GenericReadableMessage = (props: Props) => { const msgProps = useSelector(state => @@ -167,14 +168,14 @@ export const GenericReadableMessage = (props: Props) => { )} onContextMenu={handleContextMenu} receivedAt={receivedAt} - isUnread={isUnread} + isUnread={!!isUnread} key={`readable-message-${messageId}`} > { /> ); diff --git a/ts/components/conversation/message/MessageAuthorText.tsx b/ts/components/conversation/message/MessageAuthorText.tsx index 98996f339..8a1104117 100644 --- a/ts/components/conversation/message/MessageAuthorText.tsx +++ b/ts/components/conversation/message/MessageAuthorText.tsx @@ -48,7 +48,7 @@ export const MessageAuthorText = (props: Props) => { return ( void; @@ -44,7 +45,7 @@ export const MessageQuote = (props: Props) => { scrollToQuote?.({ quoteAuthor: authorPhoneNumber, quoteId, - referencedMessageNotFound, + referencedMessageNotFound: referencedMessageNotFound || false, }); }, [scrollToQuote, selected?.quote, multiSelectMode, props.messageId] @@ -65,14 +66,14 @@ export const MessageQuote = (props: Props) => { return ( ); }; diff --git a/ts/components/session/SessionInboxView.tsx b/ts/components/session/SessionInboxView.tsx index 21fa0df6a..5a65908ad 100644 --- a/ts/components/session/SessionInboxView.tsx +++ b/ts/components/session/SessionInboxView.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Provider } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { ConversationModel } from '../../models/conversation'; import { getConversationController } from '../../session/conversations'; import { UserUtils } from '../../session/utils'; import { createStore } from '../../state/createStore'; @@ -43,8 +42,10 @@ export class SessionInboxView extends React.Component { this.state = { isInitialLoadComplete: false, }; + } - void this.setupLeftPane(); + public componentDidMount() { + this.setupLeftPane(); } public render() { @@ -72,18 +73,11 @@ export class SessionInboxView extends React.Component { return ; } - private async setupLeftPane() { + private setupLeftPane() { // Here we set up a full redux store with initial state for our LeftPane Root - const convoCollection = getConversationController().getConversations(); - const conversations = convoCollection.map((conversation: ConversationModel) => - conversation.getConversationModelProps() - ); - - const filledConversations = conversations.map((conv: any) => { - return { ...conv, messages: [] }; - }); - - const fullFilledConversations = await Promise.all(filledConversations); + const conversations = getConversationController() + .getConversations() + .map(conversation => conversation.getConversationModelProps()); const timerOptions: TimerOptionsArray = window.Whisper.ExpirationTimerOptions.map( (item: any) => ({ @@ -95,7 +89,7 @@ export class SessionInboxView extends React.Component { const initialState: StateType = { conversations: { ...getEmptyConversationState(), - conversationLookup: makeLookup(fullFilledConversations, 'id'), + conversationLookup: makeLookup(conversations, 'id'), }, user: { ourNumber: UserUtils.getOurPubKeyStrFromCache(), diff --git a/ts/components/session/conversation/SessionMessagesListContainer.tsx b/ts/components/session/conversation/SessionMessagesListContainer.tsx index 505cb4c32..dc5384345 100644 --- a/ts/components/session/conversation/SessionMessagesListContainer.tsx +++ b/ts/components/session/conversation/SessionMessagesListContainer.tsx @@ -140,10 +140,10 @@ class SessionMessagesListContainerInner extends React.Component { ref={this.props.messageContainerRef} > @@ -170,7 +170,11 @@ class SessionMessagesListContainerInner extends React.Component { return; } - if (conversation.unreadCount <= 0 || firstUnreadOnOpen === undefined) { + if ( + conversation.unreadCount || + (conversation.unreadCount && conversation.unreadCount <= 0) || + firstUnreadOnOpen === undefined + ) { this.scrollToBottom(); } else { // just assume that this need to be shown by default diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx index 2d4df6813..6fee96189 100644 --- a/ts/components/session/conversation/SessionRightPanel.tsx +++ b/ts/components/session/conversation/SessionRightPanel.tsx @@ -121,14 +121,13 @@ const HeaderItem = () => { isGroup, isKickedFromGroup, profileName, - phoneNumber, isBlocked, left, name, } = selectedConversation; const showInviteContacts = isGroup && !isKickedFromGroup && !isBlocked && !left; - const userName = name || profileName || phoneNumber; + const userName = name || profileName || id; return (
diff --git a/ts/components/session/menu/ConversationHeaderMenu.tsx b/ts/components/session/menu/ConversationHeaderMenu.tsx index ca49eb757..4581cfd00 100644 --- a/ts/components/session/menu/ConversationHeaderMenu.tsx +++ b/ts/components/session/menu/ConversationHeaderMenu.tsx @@ -19,7 +19,6 @@ import { } from './Menu'; import _ from 'lodash'; import { ConversationNotificationSettingType } from '../../../models/conversation'; -import { NotificationForConvoOption } from '../../../state/ducks/conversations'; export type PropsConversationHeaderMenu = { conversationId: string; @@ -30,7 +29,6 @@ export type PropsConversationHeaderMenu = { left: boolean; isGroup: boolean; weAreAdmin: boolean; - notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; isPrivate: boolean; isBlocked: boolean; @@ -50,21 +48,20 @@ const ConversationHeaderMenu = (props: PropsConversationHeaderMenu) => { isPrivate, left, hasNickname, - notificationForConvo, currentNotificationSetting, } = props; return ( {getDisappearingMenuItem(isPublic, isKickedFromGroup, left, isBlocked, conversationId)} - {getNotificationForConvoMenuItem( + {getNotificationForConvoMenuItem({ isKickedFromGroup, left, isBlocked, - notificationForConvo, + isPrivate, currentNotificationSetting, - conversationId - )} + conversationId, + })} {getPinConversationMenuItem(conversationId)} {getBlockMenuItem(isMe, isPrivate, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} diff --git a/ts/components/session/menu/ConversationListItemContextMenu.tsx b/ts/components/session/menu/ConversationListItemContextMenu.tsx index f42f557be..0796eb452 100644 --- a/ts/components/session/menu/ConversationListItemContextMenu.tsx +++ b/ts/components/session/menu/ConversationListItemContextMenu.tsx @@ -5,7 +5,6 @@ import { ConversationNotificationSettingType, ConversationTypeEnum, } from '../../../models/conversation'; -import { NotificationForConvoOption } from '../../../state/ducks/conversations'; import { getBlockMenuItem, @@ -27,12 +26,12 @@ export type PropsContextConversationItem = { type: ConversationTypeEnum; isMe: boolean; isPublic: boolean; + isPrivate: boolean; isBlocked: boolean; hasNickname: boolean; isKickedFromGroup: boolean; left: boolean; theme?: any; - notificationForConvo: Array; currentNotificationSetting: ConversationNotificationSettingType; }; @@ -47,21 +46,21 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) => type, left, isKickedFromGroup, - notificationForConvo, currentNotificationSetting, + isPrivate, } = props; const isGroup = type === 'group'; return ( - {getNotificationForConvoMenuItem( + {getNotificationForConvoMenuItem({ + isPrivate, isKickedFromGroup, left, isBlocked, - notificationForConvo, currentNotificationSetting, - conversationId - )} + conversationId, + })} {getPinConversationMenuItem(conversationId)} {getBlockMenuItem(isMe, type === ConversationTypeEnum.PRIVATE, isBlocked, conversationId)} {getCopyMenuItem(isPublic, isGroup, conversationId)} diff --git a/ts/components/session/menu/Menu.tsx b/ts/components/session/menu/Menu.tsx index 23f92ee09..29e3988f2 100644 --- a/ts/components/session/menu/Menu.tsx +++ b/ts/components/session/menu/Menu.tsx @@ -3,7 +3,10 @@ import React from 'react'; import { getNumberOfPinnedConversations } from '../../../state/selectors/conversations'; import { getFocusedSection } from '../../../state/selectors/section'; import { Item, Submenu } from 'react-contexify'; -import { ConversationNotificationSettingType } from '../../../models/conversation'; +import { + ConversationNotificationSetting, + ConversationNotificationSettingType, +} from '../../../models/conversation'; import { useDispatch, useSelector } from 'react-redux'; import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { SectionType } from '../../../state/ducks/section'; @@ -26,7 +29,6 @@ import { import { SessionButtonColor } from '../SessionButton'; import { getTimerOptions } from '../../../state/selectors/timerOptions'; import { ToastUtils } from '../../../session/utils'; -import { NotificationForConvoOption } from '../../../state/ducks/conversations'; const maxNumberOfPinnedConversations = 5; @@ -357,17 +359,32 @@ export function getDisappearingMenuItem( return null; } -export function getNotificationForConvoMenuItem( - isKickedFromGroup: boolean | undefined, - left: boolean | undefined, - isBlocked: boolean | undefined, - notificationForConvoOptions: Array, - currentNotificationSetting: ConversationNotificationSettingType, - conversationId: string -): JSX.Element | null { +export function getNotificationForConvoMenuItem({ + conversationId, + currentNotificationSetting, + isBlocked, + isKickedFromGroup, + left, + isPrivate, +}: { + isKickedFromGroup: boolean | undefined; + left: boolean | undefined; + isBlocked: boolean | undefined; + isPrivate: boolean | undefined; + currentNotificationSetting: ConversationNotificationSettingType; + conversationId: string; +}): JSX.Element | null { if (showNotificationConvo(Boolean(isKickedFromGroup), Boolean(left), Boolean(isBlocked))) { // const isRtlMode = isRtlBody();' + // exclude mentions_only settings for private chats as this does not make much sense + const notificationForConvoOptions = ConversationNotificationSetting.filter(n => + isPrivate ? n !== 'mentions_only' : true + ).map((n: ConversationNotificationSettingType) => { + // this link to the notificationForConvo_all, notificationForConvo_mentions_only, ... + return { value: n, name: window.i18n(`notificationForConvo_${n}`) }; + }); + return ( // Remove the && false to make context menu work with RTL support m !== ourPrimary); + const ourself = convoMembers?.find(m => m !== ourPrimary) || undefined; // add ourself back at the back, so it's shown only if only 1 member and we are still a member - let membersFiltered = convoMembers.filter(m => m !== ourPrimary); + let membersFiltered = convoMembers?.filter(m => m !== ourPrimary) || []; membersFiltered.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)); if (ourself) { membersFiltered.push(ourPrimary); diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index eb3c492a8..92da64a76 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -25,7 +25,6 @@ import { conversationChanged, LastMessageStatusType, MessageModelPropsWithoutConvoProps, - NotificationForConvoOption, ReduxConversationType, } from '../state/ducks/conversations'; import { ExpirationTimerUpdateMessage } from '../session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage'; @@ -413,57 +412,136 @@ export class ConversationModel extends Backbone.Model { return this.get('moderators'); } + // tslint:disable-next-line: cyclomatic-complexity public getConversationModelProps(): ReduxConversationType { const groupAdmins = this.getGroupAdmins(); - const members = this.isGroup() && !this.isPublic() ? this.get('members') : []; - const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + const isPublic = this.isPublic(); - // isSelected is overriden by redux - return { - isSelected: false, + const members = this.isGroup() && !isPublic ? this.get('members') : []; + const ourNumber = UserUtils.getOurPubKeyStrFromCache(); + const avatarPath = this.getAvatarPath(); + const isPrivate = this.isPrivate(); + const isGroup = !isPrivate; + const weAreAdmin = this.isAdmin(ourNumber); + const isMe = this.isMe(); + const isTyping = !!this.typingTimer; + const name = this.getName(); + const profileName = this.getProfileName(); + const unreadCount = this.get('unreadCount') || undefined; + const mentionedUs = this.get('mentionedUs') || undefined; + const isBlocked = this.isBlocked(); + const subscriberCount = this.get('subscriberCount'); + const isPinned = this.isPinned(); + const hasNickname = !!this.getNickname(); + const isKickedFromGroup = !!this.get('isKickedFromGroup'); + const left = !!this.get('left'); + const expireTimer = this.get('expireTimer'); + const currentNotificationSetting = this.get('triggerNotificationsFor'); + + // to reduce the redux store size, only set fields which cannot be undefined + // for instance, a boolean can usually be not set if false, etc + const toRet: ReduxConversationType = { id: this.id as string, activeAt: this.get('active_at'), - avatarPath: this.getAvatarPath() || null, - type: this.isPrivate() ? ConversationTypeEnum.PRIVATE : ConversationTypeEnum.GROUP, - weAreAdmin: this.isAdmin(ourNumber), - isGroup: !this.isPrivate(), - - isPrivate: this.isPrivate(), - isMe: this.isMe(), - isPublic: this.isPublic(), - isTyping: !!this.typingTimer, - name: this.getName(), - profileName: this.getProfileName(), - // title: this.getTitle(), - unreadCount: this.get('unreadCount') || 0, - mentionedUs: this.get('mentionedUs') || false, - isBlocked: this.isBlocked(), - phoneNumber: this.getNumber(), - lastMessage: { - status: this.get('lastMessageStatus'), - text: this.get('lastMessage'), - }, - hasNickname: !!this.getNickname(), - isKickedFromGroup: !!this.get('isKickedFromGroup'), - left: !!this.get('left'), - groupAdmins, - members, - expireTimer: this.get('expireTimer') || 0, - subscriberCount: this.get('subscriberCount') || 0, - isPinned: this.isPinned(), - notificationForConvo: this.getConversationNotificationSettingType(), - currentNotificationSetting: this.get('triggerNotificationsFor'), + type: isPrivate ? ConversationTypeEnum.PRIVATE : ConversationTypeEnum.GROUP, }; - } - public getConversationNotificationSettingType(): Array { - // exclude mentions_only settings for private chats as this does not make much sense - return ConversationNotificationSetting.filter(n => - this.isPrivate() ? n !== 'mentions_only' : true - ).map((n: ConversationNotificationSettingType) => { - // this link to the notificationForConvo_all, notificationForConvo_mentions_only, ... - return { value: n, name: window.i18n(`notificationForConvo_${n}`) }; - }); + if (isPrivate) { + toRet.isPrivate = true; + } + + if (isGroup) { + toRet.isGroup = true; + } + + if (weAreAdmin) { + toRet.weAreAdmin = true; + } + + if (isMe) { + toRet.isMe = true; + } + if (isPublic) { + toRet.isPublic = true; + } + if (isTyping) { + toRet.isTyping = true; + } + + if (isTyping) { + toRet.isTyping = true; + } + + if (avatarPath) { + toRet.avatarPath = avatarPath; + } + + if (name) { + toRet.name = name; + } + + if (profileName) { + toRet.profileName = profileName; + } + + if (unreadCount) { + toRet.unreadCount = unreadCount; + } + + if (mentionedUs) { + toRet.mentionedUs = mentionedUs; + } + + if (isBlocked) { + toRet.isBlocked = isBlocked; + } + if (hasNickname) { + toRet.hasNickname = hasNickname; + } + if (isKickedFromGroup) { + toRet.isKickedFromGroup = isKickedFromGroup; + } + if (left) { + toRet.left = left; + } + if (isPinned) { + toRet.isPinned = isPinned; + } + if (subscriberCount) { + toRet.subscriberCount = subscriberCount; + } + if (groupAdmins && groupAdmins.length) { + toRet.groupAdmins = groupAdmins; + } + if (members && members.length) { + toRet.members = members; + } + + if (members && members.length) { + toRet.members = members; + } + + if (expireTimer) { + toRet.expireTimer = expireTimer; + } + + if ( + currentNotificationSetting && + currentNotificationSetting !== ConversationNotificationSetting[0] + ) { + toRet.currentNotificationSetting = currentNotificationSetting; + } + + const lastMessageText = this.get('lastMessage'); + if (lastMessageText && lastMessageText.length) { + const lastMessageStatus = this.get('lastMessageStatus'); + + toRet.lastMessage = { + status: lastMessageStatus, + text: lastMessageText, + }; + } + return toRet; } public async updateGroupAdmins(groupAdmins: Array) { @@ -921,7 +999,6 @@ export class ConversationModel extends Backbone.Model { id: this.id, data: { ...this.getConversationModelProps(), - isSelected: false, }, }) ); diff --git a/ts/models/message.ts b/ts/models/message.ts index 202728928..fb732b1bc 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -84,13 +84,25 @@ export class MessageModel extends Backbone.Model { public getMessageModelProps(): MessageModelPropsWithoutConvoProps { perfStart(`getPropsMessage-${this.id}`); + const propsForDataExtractionNotification = this.getPropsForDataExtractionNotification(); + const propsForGroupInvitation = this.getPropsForGroupInvitation(); + const propsForGroupNotification = this.getPropsForGroupNotification(); + const propsForTimerNotification = this.getPropsForTimerNotification(); const messageProps: MessageModelPropsWithoutConvoProps = { propsForMessage: this.getPropsForMessage(), - propsForDataExtractionNotification: this.getPropsForDataExtractionNotification(), - propsForGroupInvitation: this.getPropsForGroupInvitation(), - propsForGroupNotification: this.getPropsForGroupNotification(), - propsForTimerNotification: this.getPropsForTimerNotification(), }; + if (propsForDataExtractionNotification) { + messageProps.propsForDataExtractionNotification = propsForDataExtractionNotification; + } + if (propsForGroupInvitation) { + messageProps.propsForGroupInvitation = propsForGroupInvitation; + } + if (propsForGroupNotification) { + messageProps.propsForGroupNotification = propsForGroupNotification; + } + if (propsForTimerNotification) { + messageProps.propsForTimerNotification = propsForTimerNotification; + } perfEnd(`getPropsMessage-${this.id}`, 'getPropsMessage'); return messageProps; } @@ -365,8 +377,8 @@ export class MessageModel extends Backbone.Model { } return { - phoneNumber: pubkey as string, - avatarPath: (contactModel ? contactModel.getAvatarPath() : null) as string | null, + pubkey: pubkey, + avatarPath: contactModel ? contactModel.getAvatarPath() : null, name: (contactModel ? contactModel.getName() : null) as string | null, profileName: profileName as string | null, title: (contactModel ? contactModel.getTitle() : null) as string | null, @@ -393,7 +405,7 @@ export class MessageModel extends Backbone.Model { type: 'add', contacts: _.map( Array.isArray(groupUpdate.joined) ? groupUpdate.joined : [groupUpdate.joined], - phoneNumber => this.findAndFormatContact(phoneNumber) + pubkey => this.findAndFormatContact(pubkey) ), }; changes.push(change); @@ -411,7 +423,7 @@ export class MessageModel extends Backbone.Model { isMe: false, contacts: _.map( Array.isArray(groupUpdate.kicked) ? groupUpdate.kicked : [groupUpdate.kicked], - phoneNumber => this.findAndFormatContact(phoneNumber) + pubkey => this.findAndFormatContact(pubkey) ), }; changes.push(change); @@ -443,7 +455,7 @@ export class MessageModel extends Backbone.Model { isMe: false, contacts: _.map( Array.isArray(groupUpdate.left) ? groupUpdate.left : [groupUpdate.left], - phoneNumber => this.findAndFormatContact(phoneNumber) + pubkey => this.findAndFormatContact(pubkey) ), }; changes.push(change); @@ -473,11 +485,11 @@ export class MessageModel extends Backbone.Model { // Only return the status on outgoing messages if (!this.isOutgoing()) { - return null; + return undefined; } if (this.isDataExtractionNotification()) { - return null; + return undefined; } const readBy = this.get('read_by') || []; @@ -493,6 +505,7 @@ export class MessageModel extends Backbone.Model { return 'sending'; } + // tslint:disable-next-line: cyclomatic-complexity public getPropsForMessage(options: any = {}): PropsForMessageWithoutConvoProps { const sender = this.getSource(); const expirationLength = this.get('expireTimer') * 1000; @@ -502,38 +515,67 @@ export class MessageModel extends Backbone.Model { const attachments = this.get('attachments') || []; const isTrustedForAttachmentDownload = this.isTrustedForAttachmentDownload(); - + const body = this.get('body'); const props: PropsForMessageWithoutConvoProps = { - text: this.createNonBreakingLastSeparator(this.get('body') || null), - id: this.id as string, + id: this.id, direction: (this.isIncoming() ? 'incoming' : 'outgoing') as MessageModelType, timestamp: this.get('sent_at') || 0, - receivedAt: this.get('received_at'), - serverTimestamp: this.get('serverTimestamp'), - serverId: this.get('serverId'), - status: this.getMessagePropStatus(), authorPhoneNumber: sender, convoId: this.get('conversationId'), - attachments: attachments - .filter((attachment: any) => !attachment.error) - .map((attachment: any) => this.getPropsForAttachment(attachment)), - previews: this.getPropsForPreview(), - quote: this.getPropsForQuote(options), - isUnread: this.isUnread(), - expirationLength, - expirationTimestamp, - isExpired: this.isExpired(), - isTrustedForAttachmentDownload, }; + if (body) { + props.text = this.createNonBreakingLastSeparator(body); + } + + if (this.get('received_at')) { + props.receivedAt = this.get('received_at'); + } + if (this.get('serverTimestamp')) { + props.serverTimestamp = this.get('serverTimestamp'); + } + if (this.get('serverId')) { + props.serverId = this.get('serverId'); + } + if (expirationLength) { + props.expirationLength = expirationLength; + } + if (expirationTimestamp) { + props.expirationTimestamp = expirationTimestamp; + } + if (isTrustedForAttachmentDownload) { + props.isTrustedForAttachmentDownload = isTrustedForAttachmentDownload; + } + const isUnread = this.isUnread(); + if (isUnread) { + props.isUnread = isUnread; + } + const isExpired = this.isExpired(); + if (isExpired) { + props.isExpired = isExpired; + } + const previews = this.getPropsForPreview(); + if (previews && previews.length) { + props.previews = previews; + } + const quote = this.getPropsForQuote(options); + if (quote) { + props.quote = quote; + } + const status = this.getMessagePropStatus(); + if (status) { + props.status = status; + } + const attachmentsProps = attachments + .filter((attachment: any) => !attachment.error) + .map((attachment: any) => this.getPropsForAttachment(attachment)); + if (attachmentsProps && attachmentsProps.length) { + props.attachments = attachmentsProps; + } return props; } - public createNonBreakingLastSeparator(text: string | null) { - if (!text) { - return null; - } - + public createNonBreakingLastSeparator(text: string) { const nbsp = '\xa0'; const regex = /(\S)( +)(\S+\s*)$/; return text.replace(regex, (_match, start, spaces, end) => { @@ -566,8 +608,12 @@ export class MessageModel extends Backbone.Model { // tslint:enable: prefer-object-spread } - public getPropsForPreview() { - const previews = this.get('preview') || []; + public getPropsForPreview(): Array | null { + const previews = this.get('preview') || null; + + if (!previews || previews.length === 0) { + return null; + } return previews.map((preview: any) => { let image: PropsForAttachment | null = null; @@ -602,16 +648,44 @@ export class MessageModel extends Backbone.Model { const isFromMe = contact ? contact.id === UserUtils.getOurPubKeyStrFromCache() : false; const firstAttachment = quote.attachments && quote.attachments[0]; - - return { - text: this.createNonBreakingLastSeparator(quote.text), - attachment: firstAttachment ? this.processQuoteAttachment(firstAttachment) : null, - isFromMe, + const quoteProps: { + referencedMessageNotFound?: boolean; + authorPhoneNumber: string; + messageId: string; + authorName: string; + text?: string; + attachment?: any; + isFromMe?: boolean; + } = { authorPhoneNumber: author, messageId: id, authorName, - referencedMessageNotFound, }; + + if (referencedMessageNotFound) { + quoteProps.referencedMessageNotFound = true; + } + + if (!referencedMessageNotFound) { + if (quote.text) { + // do not show text of not found messages. + // if the message was deleted better not show it's text content in the message + quoteProps.text = this.createNonBreakingLastSeparator(quote.text); + } + + const quoteAttachment = firstAttachment + ? this.processQuoteAttachment(firstAttachment) + : undefined; + if (quoteAttachment) { + // only set attachment if referencedMessageNotFound is false and we have one + quoteProps.attachment = quoteAttachment; + } + } + if (isFromMe) { + quoteProps.isFromMe = true; + } + + return quoteProps; } public getPropsForAttachment(attachment: AttachmentTypeWithPath): PropsForAttachment | null { @@ -708,7 +782,7 @@ export class MessageModel extends Backbone.Model { // first; otherwise it's alphabetical const sortedContacts = _.sortBy( finalContacts, - contact => `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}` + contact => `${contact.isPrimaryDevice ? '0' : '1'}${contact.pubkey}` ); const toRet: MessagePropsDetails = { sentAt: this.get('sent_at') || 0, diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 1b3b6fd31..904857875 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -354,13 +354,12 @@ async function handleExpirationTimerUpdate( source: string, expireTimer: number ) { - // TODO: if the message is an expiration timer update, it - // shouldn't be responsible for anything else!!! message.set({ expirationTimerUpdate: { source, expireTimer, }, + unread: 0, // mark the message as read. }); conversation.set({ expireTimer }); diff --git a/ts/session/tslint.json b/ts/session/tslint.json index 0644121c7..6d990c28d 100644 --- a/ts/session/tslint.json +++ b/ts/session/tslint.json @@ -1,6 +1,7 @@ { "extends": ["../../tslint.json"], "rules": { - "no-unused-variable": false + "no-unused-variable": false, + "use-simple-attributes": false } } diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index 2bf46bf3e..130b2a81b 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -19,10 +19,10 @@ import { omit } from 'lodash'; export type MessageModelPropsWithoutConvoProps = { propsForMessage: PropsForMessageWithoutConvoProps; - propsForGroupInvitation: PropsForGroupInvitation | null; - propsForTimerNotification: PropsForExpirationTimer | null; - propsForDataExtractionNotification: PropsForDataExtractionNotification | null; - propsForGroupNotification: PropsForGroupUpdate | null; + propsForGroupInvitation?: PropsForGroupInvitation; + propsForTimerNotification?: PropsForExpirationTimer; + propsForDataExtractionNotification?: PropsForDataExtractionNotification; + propsForGroupNotification?: PropsForGroupUpdate; }; export type MessageModelPropsWithConvoProps = SortedMessageModelProps & { @@ -30,8 +30,8 @@ export type MessageModelPropsWithConvoProps = SortedMessageModelProps & { }; export type ContactPropsMessageDetail = { - status: string | null; - phoneNumber: string; + status: string | undefined; + pubkey: string; name?: string | null; profileName?: string | null; avatarPath?: string | null; @@ -50,10 +50,10 @@ export type MessagePropsDetails = { direction: MessageModelType; }; -export type LastMessageStatusType = MessageDeliveryStatus | null; +export type LastMessageStatusType = MessageDeliveryStatus | undefined; export type FindAndFormatContactType = { - phoneNumber: string; + pubkey: string; avatarPath: string | null; name: string | null; profileName: string | null; @@ -64,7 +64,7 @@ export type FindAndFormatContactType = { export type PropsForExpirationTimer = { timespan: string; disabled: boolean; - phoneNumber: string; + pubkey: string; avatarPath: string | null; name: string | null; profileName: string | null; @@ -157,33 +157,34 @@ export type PropsForAttachment = { }; export type PropsForMessageWithoutConvoProps = { - text: string | null; id: string; // messageId direction: MessageModelType; timestamp: number; - receivedAt: number | undefined; - serverTimestamp: number | undefined; - serverId: number | undefined; - status: LastMessageStatusType | null; authorPhoneNumber: string; // this is the sender convoId: string; // this is the conversation in which this message was sent - attachments: Array; - previews: Array; + text?: string; + + receivedAt?: number; + serverTimestamp?: number; + serverId?: number; + status?: LastMessageStatusType; + attachments?: Array; + previews?: Array; quote?: { - text: string | null; + text?: string; attachment?: QuotedAttachmentType; - isFromMe: boolean; + isFromMe?: boolean; authorPhoneNumber: string; authorProfileName?: string; authorName?: string; messageId?: string; - referencedMessageNotFound: boolean; + referencedMessageNotFound?: boolean; } | null; - isUnread: boolean; - expirationLength: number; - expirationTimestamp: number | null; - isExpired: boolean; - isTrustedForAttachmentDownload: boolean; + isUnread?: boolean; + expirationLength?: number; + expirationTimestamp?: number | null; + isExpired?: boolean; + isTrustedForAttachmentDownload?: boolean; }; export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & { @@ -209,35 +210,36 @@ export interface ReduxConversationType { id: string; name?: string; profileName?: string; - hasNickname: boolean; + hasNickname?: boolean; activeAt?: number; lastMessage?: LastMessageType; - phoneNumber: string; type: ConversationTypeEnum; - isMe: boolean; - isPublic: boolean; - isGroup: boolean; - isPrivate: boolean; - weAreAdmin: boolean; - unreadCount: number; - mentionedUs: boolean; - isSelected: boolean; - expireTimer: number; + isMe?: boolean; + isPublic?: boolean; + isGroup?: boolean; + isPrivate?: boolean; + weAreAdmin?: boolean; + unreadCount?: number; + mentionedUs?: boolean; + isSelected?: boolean; + expireTimer?: number; - isTyping: boolean; - isBlocked: boolean; - isKickedFromGroup: boolean; - subscriberCount: number; - left: boolean; - avatarPath: string | null; // absolute filepath to the avatar + isTyping?: boolean; + isBlocked?: boolean; + isKickedFromGroup?: boolean; + subscriberCount?: number; + left?: boolean; + avatarPath?: string | null; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups - members: Array; // members for closed groups only + members?: Array; // members for closed groups only - currentNotificationSetting: ConversationNotificationSettingType; - notificationForConvo: Array; + /** + * If this is undefined, it means all notification are enabled + */ + currentNotificationSetting?: ConversationNotificationSettingType; - isPinned: boolean; + isPinned?: boolean; } export interface NotificationForConvoOption { diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index e7da54030..ba9de7397 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -14,7 +14,7 @@ import { import { getIntl, getOurNumber } from './user'; import { BlockedNumberController } from '../../util'; -import { ConversationTypeEnum } from '../../models/conversation'; +import { ConversationNotificationSetting, ConversationTypeEnum } from '../../models/conversation'; import { LocalizerType } from '../../types/Util'; import { ConversationHeaderProps, @@ -319,6 +319,7 @@ export const _getLeftPaneLists = ( if ( unreadCount < 9 && + conversation.unreadCount && conversation.unreadCount > 0 && conversation.currentNotificationSetting !== 'disabled' ) { @@ -369,11 +370,11 @@ export const getConversationHeaderTitleProps = createSelector(getSelectedConvers return undefined; } return { - isKickedFromGroup: state.isKickedFromGroup, - phoneNumber: state.phoneNumber, - isMe: state.isMe, + isKickedFromGroup: !!state.isKickedFromGroup, + conversationKey: state.id, + isMe: !!state.isMe, members: state.members || [], - isPublic: state.isPublic, + isPublic: !!state.isPublic, profileName: state.profileName, name: state.name, subscriberCount: state.subscriberCount, @@ -415,25 +416,24 @@ export const getConversationHeaderProps = createSelector(getSelectedConversation : null; return { - id: state.id, - isPrivate: state.isPrivate, - notificationForConvo: state.notificationForConvo, - currentNotificationSetting: state.currentNotificationSetting, - isBlocked: state.isBlocked, - left: state.left, - avatarPath: state.avatarPath, + conversationKey: state.id, + isPrivate: !!state.isPrivate, + currentNotificationSetting: + state.currentNotificationSetting || ConversationNotificationSetting[0], // if undefined, it is 'all' + isBlocked: !!state.isBlocked, + left: !!state.left, + avatarPath: state.avatarPath || null, expirationSettingName: expirationSettingName, - hasNickname: state.hasNickname, - weAreAdmin: state.weAreAdmin, - isKickedFromGroup: state.isKickedFromGroup, - phoneNumber: state.phoneNumber, - isMe: state.isMe, + hasNickname: !!state.hasNickname, + weAreAdmin: !!state.weAreAdmin, + isKickedFromGroup: !!state.isKickedFromGroup, + isMe: !!state.isMe, members: state.members || [], - isPublic: state.isPublic, + isPublic: !!state.isPublic, profileName: state.profileName, name: state.name, subscriberCount: state.subscriberCount, - isGroup: state.isGroup, + isGroup: !!state.isGroup, }; }); @@ -657,16 +657,16 @@ export const getMessagePropsByMessageId = createSelector( ...foundMessageProps, propsForMessage: { ...foundMessageProps.propsForMessage, - isBlocked: foundMessageConversation.isBlocked, - isPublic, - isOpenGroupV2: isPublic, + isBlocked: !!foundMessageConversation.isBlocked, + isPublic: !!isPublic, + isOpenGroupV2: !!isPublic, isSenderAdmin, isDeletable, weAreAdmin, conversationType: foundMessageConversation.type, authorPhoneNumber, - authorAvatarPath: foundSenderConversation.avatarPath, - isKickedFromGroup: foundMessageConversation.isKickedFromGroup, + authorAvatarPath: foundSenderConversation.avatarPath || null, + isKickedFromGroup: foundMessageConversation.isKickedFromGroup || false, authorProfileName, authorName, }, @@ -875,7 +875,7 @@ export const getMessageAttachmentProps = createSelector(getMessagePropsByMessage convoId, } = props.propsForMessage; const msgProps: MessageAttachmentSelectorProps = { - attachments, + attachments: attachments || [], direction, isTrustedForAttachmentDownload, timestamp, diff --git a/ts/test/session/unit/selectors/conversations_test.ts b/ts/test/session/unit/selectors/conversations_test.ts index 7fb39d563..30e5bfb89 100644 --- a/ts/test/session/unit/selectors/conversations_test.ts +++ b/ts/test/session/unit/selectors/conversations_test.ts @@ -17,8 +17,6 @@ describe('state/selectors/conversations', () => { id: 'id1', activeAt: 0, name: 'No timestamp', - phoneNumber: 'notused', - type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -35,7 +33,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -48,8 +46,6 @@ describe('state/selectors/conversations', () => { id: 'id2', activeAt: 20, name: 'B', - phoneNumber: 'notused', - type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -66,7 +62,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -79,7 +75,6 @@ describe('state/selectors/conversations', () => { id: 'id3', activeAt: 20, name: 'C', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -96,7 +91,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -109,7 +104,6 @@ describe('state/selectors/conversations', () => { id: 'id4', activeAt: 20, name: 'Á', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -126,7 +120,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], expireTimer: 0, @@ -139,7 +133,6 @@ describe('state/selectors/conversations', () => { id: 'id5', activeAt: 30, name: 'First!', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -157,7 +150,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -185,7 +178,6 @@ describe('state/selectors/conversations', () => { id: 'id1', activeAt: 0, name: 'No timestamp', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, @@ -202,7 +194,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -216,7 +208,6 @@ describe('state/selectors/conversations', () => { id: 'id2', activeAt: 20, name: 'B', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, @@ -233,7 +224,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -247,7 +238,6 @@ describe('state/selectors/conversations', () => { id: 'id3', activeAt: 20, name: 'C', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, @@ -264,7 +254,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -278,7 +268,6 @@ describe('state/selectors/conversations', () => { id: 'id4', activeAt: 20, name: 'Á', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -294,7 +283,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, @@ -308,7 +297,6 @@ describe('state/selectors/conversations', () => { id: 'id5', activeAt: 30, name: 'First!', - phoneNumber: 'notused', type: ConversationTypeEnum.PRIVATE, isMe: false, unreadCount: 1, @@ -325,7 +313,7 @@ describe('state/selectors/conversations', () => { weAreAdmin: false, isGroup: false, isPrivate: false, - notificationForConvo: [{ value: 'all', name: 'all' }], + avatarPath: '', groupAdmins: [], lastMessage: undefined, diff --git a/ts/test/test-utils/utils/message.ts b/ts/test/test-utils/utils/message.ts index eec4c75fe..b45ac39ef 100644 --- a/ts/test/test-utils/utils/message.ts +++ b/ts/test/test-utils/utils/message.ts @@ -89,7 +89,7 @@ export class MockConversation { isKickedFromGroup: false, active_at: Date.now(), lastJoinedTimestamp: Date.now(), - lastMessageStatus: null, + lastMessageStatus: undefined, lastMessage: null, zombies: [], triggerNotificationsFor: 'all', diff --git a/tslint.json b/tslint.json index 3fae74307..c1fd80ff8 100644 --- a/tslint.json +++ b/tslint.json @@ -3,6 +3,7 @@ "extends": ["tslint:recommended", "tslint-react", "tslint-microsoft-contrib"], "jsRules": {}, "rules": { + "use-simple-attributes": false, "align": false, "newline-per-chained-call": false, "array-type": [true, "generic"],