add types for message props and remove props function calls msg

pull/1783/head
Audric Ackermann 4 years ago
parent 6deb97dbc0
commit 672eb91975
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -7,31 +7,7 @@ import { Timestamp } from './conversation/Timestamp';
import { ContactName } from './conversation/ContactName';
import { DefaultTheme, withTheme } from 'styled-components';
export type MessageSearchResultProps = {
id: string;
conversationId: string;
receivedAt: number;
snippet: string;
from: {
phoneNumber: string;
isMe?: boolean;
name?: string;
color?: string;
profileName?: string;
avatarPath?: string;
};
to: {
groupName?: string;
phoneNumber: string;
isMe?: boolean;
name?: string;
profileName?: string;
};
};
import { PropsForSearchResults } from '../state/ducks/conversations';
type PropsHousekeeping = {
isSelected?: boolean;
@ -39,7 +15,7 @@ type PropsHousekeeping = {
onClick: (conversationId: string, messageId?: string) => void;
};
type Props = MessageSearchResultProps & PropsHousekeeping;
type Props = PropsForSearchResults & PropsHousekeeping;
class MessageSearchResultInner extends React.PureComponent<Props> {
public renderFromName() {
@ -59,10 +35,11 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
}
return (
// tslint:disable: use-simple-attributes
<ContactName
phoneNumber={from.phoneNumber}
name={from.name}
profileName={from.profileName}
name={from.name || ''}
profileName={from.profileName || ''}
module="module-message-search-result__header__name"
shouldShowPubkey={false}
/>
@ -80,8 +57,8 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
<span className="module-mesages-search-result__header__group">
<ContactName
phoneNumber={to.phoneNumber}
name={to.name}
profileName={to.profileName}
name={to.name || ''}
profileName={to.profileName || ''}
shouldShowPubkey={false}
/>
</span>
@ -98,7 +75,7 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
return (
<Avatar
avatarPath={from.avatarPath}
avatarPath={from.avatarPath || ''}
name={userName}
size={AvatarSize.S}
pubkey={from.phoneNumber}
@ -135,7 +112,7 @@ class MessageSearchResultInner extends React.PureComponent<Props> {
</div>
</div>
<div className="module-message-search-result__body">
<MessageBodyHighlight text={snippet} />
<MessageBodyHighlight text={snippet || ''} />
</div>
</div>
</div>

@ -1,12 +1,13 @@
import React from 'react';
import { PropsForSearchResults } from '../state/ducks/conversations';
import { ConversationListItemProps, ConversationListItemWithDetails } from './ConversationListItem';
import { MessageSearchResult, MessageSearchResultProps } from './MessageSearchResult';
import { MessageSearchResult } from './MessageSearchResult';
export type SearchResultsProps = {
contacts: Array<ConversationListItemProps>;
conversations: Array<ConversationListItemProps>;
hideMessagesHeader: boolean;
messages: Array<MessageSearchResultProps>;
messages: Array<PropsForSearchResults>;
searchTerm: string;
};

@ -1,14 +1,12 @@
import React from 'react';
import { useTheme } from 'styled-components';
import { DataExtractionNotificationProps } from '../../models/messageType';
import { PropsForDataExtractionNotification } from '../../models/messageType';
import { SignalService } from '../../protobuf';
import { Flex } from '../basic/Flex';
import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import { SpacerXS, Text } from '../basic/Text';
type Props = DataExtractionNotificationProps;
export const DataExtractionNotification = (props: Props) => {
export const DataExtractionNotification = (props: PropsForDataExtractionNotification) => {
const theme = useTheme();
const { name, type, source } = props;

@ -2,15 +2,10 @@ import React from 'react';
import classNames from 'classnames';
import { SessionIconButton, SessionIconSize, SessionIconType } from '../session/icon';
import { useTheme } from 'styled-components';
import { PropsForGroupInvitation } from '../../state/ducks/conversations';
import { acceptOpenGroupInvitation } from '../../interactions/messageInteractions';
type Props = {
name: string;
url: string;
direction: string;
onJoinClick: () => void;
};
export const GroupInvitation = (props: Props) => {
export const GroupInvitation = (props: PropsForGroupInvitation) => {
const theme = useTheme();
const classes = ['group-invitation'];
@ -28,10 +23,12 @@ export const GroupInvitation = (props: Props) => {
iconColor={theme.colors.accent}
theme={theme}
iconSize={SessionIconSize.Large}
onClick={props.onJoinClick}
onClick={() => {
acceptOpenGroupInvitation(props.acceptUrl, props.serverName);
}}
/>
<span className="group-details">
<span className="group-name">{props.name}</span>
<span className="group-name">{props.serverName}</span>
<span className="group-type">{openGroupInvitation}</span>
<span className="group-address">{props.url}</span>
</span>

@ -3,89 +3,92 @@ import { compact, flatten } from 'lodash';
import { Intl } from '../Intl';
import { missingCaseError } from '../../util/missingCaseError';
import {
PropsForGroupUpdate,
PropsForGroupUpdateAdd,
PropsForGroupUpdateKicked,
PropsForGroupUpdateRemove,
PropsForGroupUpdateType,
} from '../../state/ducks/conversations';
import _ from 'underscore';
interface Contact {
phoneNumber: string;
profileName?: string;
name?: string;
}
// This component is used to display group updates in the conversation view.
// This is a not a "notification" as the name suggests, but a message inside the conversation
interface Change {
type: 'add' | 'remove' | 'name' | 'general' | 'kicked';
isMe: boolean;
newName?: string;
contacts?: Array<Contact>;
type TypeWithContacts =
| PropsForGroupUpdateAdd
| PropsForGroupUpdateKicked
| PropsForGroupUpdateRemove;
function isTypeWithContact(change: PropsForGroupUpdateType): change is TypeWithContacts {
return (change as TypeWithContacts).contacts !== undefined;
}
type Props = {
changes: Array<Change>;
};
function getPeople(change: TypeWithContacts) {
return _.compact(
flatten(
(change.contacts || []).map((contact, index) => {
const element = (
<span
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
{contact.profileName || contact.phoneNumber}
</span>
);
return [index > 0 ? ', ' : null, element];
})
)
);
}
// This component is used to display group updates in the conversation view.
// This is a not a "notification" as the name suggests, but a message inside the conversation
export const GroupNotification = (props: Props) => {
function renderChange(change: Change) {
const { isMe, contacts, type, newName } = change;
const people = compact(
flatten(
(contacts || []).map((contact, index) => {
const element = (
<span
key={`external-${contact.phoneNumber}`}
className="module-group-notification__contact"
>
{contact.profileName || contact.phoneNumber}
</span>
);
return [index > 0 ? ', ' : null, element];
})
)
);
switch (type) {
case 'name':
return `${window.i18n('titleIsNow', [newName || ''])}.`;
case 'add':
if (!contacts || !contacts.length) {
throw new Error('Group update add is missing contacts');
}
const joinKey = contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
return <Intl id={joinKey} components={[people]} />;
case 'remove':
if (isMe) {
return window.i18n('youLeftTheGroup');
}
if (!contacts || !contacts.length) {
throw new Error('Group update remove is missing contacts');
}
const leftKey = contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
return <Intl id={leftKey} components={[people]} />;
case 'kicked':
if (isMe) {
return window.i18n('youGotKickedFromGroup');
}
if (!contacts || !contacts.length) {
throw new Error('Group update kicked is missing contacts');
}
const kickedKey = contacts.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup';
return <Intl id={kickedKey} components={[people]} />;
case 'general':
return window.i18n('updatedTheGroup');
default:
throw missingCaseError(type);
}
function renderChange(change: PropsForGroupUpdateType) {
const people = isTypeWithContact(change) ? getPeople(change) : [];
switch (change.type) {
case 'name':
return `${window.i18n('titleIsNow', [change.newName || ''])}.`;
case 'add':
if (!change.contacts || !change.contacts.length) {
throw new Error('Group update add is missing contacts');
}
const joinKey = change.contacts.length > 1 ? 'multipleJoinedTheGroup' : 'joinedTheGroup';
return <Intl id={joinKey} components={[people]} />;
case 'remove':
if (change.isMe) {
return window.i18n('youLeftTheGroup');
}
if (!change.contacts || !change.contacts.length) {
throw new Error('Group update remove is missing contacts');
}
const leftKey = change.contacts.length > 1 ? 'multipleLeftTheGroup' : 'leftTheGroup';
return <Intl id={leftKey} components={[people]} />;
case 'kicked':
if (change.isMe) {
return window.i18n('youGotKickedFromGroup');
}
if (!change.contacts || !change.contacts.length) {
throw new Error('Group update kicked is missing contacts');
}
const kickedKey =
change.contacts.length > 1 ? 'multipleKickedFromTheGroup' : 'kickedFromTheGroup';
return <Intl id={kickedKey} components={[people]} />;
case 'general':
return window.i18n('updatedTheGroup');
default:
window.log.error('Missing case error');
}
}
export const GroupNotification = (props: PropsForGroupUpdate) => {
const { changes } = props;
return (
<div className="module-group-notification">

@ -42,6 +42,7 @@ import { MessageInteraction } from '../../interactions';
import autoBind from 'auto-bind';
import { AudioPlayerWithEncryptedFile } from './H5AudioPlayer';
import { ClickToTrustSender } from './message/ClickToTrustSender';
import { getMessageById } from '../../data/data';
// Same as MIN_WIDTH in ImageGrid.tsx
const MINIMUM_LINK_PREVIEW_IMAGE_WIDTH = 200;
@ -526,7 +527,6 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
onSelectMessage,
onDeleteMessage,
onDownload,
onRetrySend,
onShowDetail,
isPublic,
isOpenGroupV2,
@ -582,7 +582,18 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
</Item>
<Item onClick={this.onReplyPrivate}>{window.i18n('replyToMessage')}</Item>
<Item onClick={onShowDetail}>{window.i18n('moreInformation')}</Item>
{showRetry ? <Item onClick={onRetrySend}>{window.i18n('resend')}</Item> : null}
{showRetry ? (
<Item
onClick={async () => {
const found = await getMessageById(id);
if (found) {
await found.retrySend();
}
}}
>
{window.i18n('resend')}
</Item>
) : null}
{isDeletable ? (
<>
<Item
@ -692,17 +703,7 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
// tslint:disable-next-line: cyclomatic-complexity
public render() {
const {
direction,
id,
selected,
multiSelectMode,
conversationType,
isPublic,
text,
isUnread,
markRead,
} = this.props;
const { direction, id, selected, multiSelectMode, conversationType, isUnread } = this.props;
const { expired, expiring } = this.state;
if (expired) {
@ -728,11 +729,12 @@ class MessageInner extends React.PureComponent<MessageRegularProps, State> {
divClasses.push('flash-green-once');
}
const onVisible = (inView: boolean) => {
const onVisible = async (inView: boolean) => {
if (inView && shouldMarkReadWhenVisible) {
const found = await getMessageById(id);
// mark the message as read.
// this will trigger the expire timer.
void markRead(Date.now());
void found?.markRead(Date.now());
}
};

@ -5,17 +5,9 @@ import { Intl } from '../Intl';
import { missingCaseError } from '../../util/missingCaseError';
import { SessionIcon, SessionIconSize, SessionIconType } from '../session/icon';
import { ThemeContext } from 'styled-components';
import { PropsForExpirationTimer } from '../../state/ducks/conversations';
type Props = {
type: 'fromOther' | 'fromMe' | 'fromSync';
phoneNumber: string;
profileName?: string;
name?: string;
disabled: boolean;
timespan: string;
};
export const TimerNotification = (props: Props) => {
export const TimerNotification = (props: PropsForExpirationTimer) => {
function renderContents() {
const { phoneNumber, profileName, timespan, type, disabled } = props;
const changeKey = disabled ? 'disabledDisappearingMessages' : 'theyChangedTheTimer';

@ -28,11 +28,11 @@ const StyledCountContainer = styled.div<{ shouldRender: boolean }>`
/* cursor: */
`;
const StyledCount = styled.div<{ overflow: boolean }>`
const StyledCount = styled.div<{ countOverflow: boolean }>`
position: relative;
font-size: ${props => (props.overflow ? '0.5em' : '0.6em')};
margin-top: ${props => (props.overflow ? '0.35em' : '0em')};
margin-left: ${props => (props.overflow ? '-0.45em' : '0em')};
font-size: ${props => (props.countOverflow ? '0.5em' : '0.6em')};
margin-top: ${props => (props.countOverflow ? '0.35em' : '0em')};
margin-left: ${props => (props.countOverflow ? '-0.45em' : '0em')};
`;
const StyledCountSup = styled.div`
@ -50,7 +50,7 @@ export const SessionNotificationCount = (props: Props) => {
if (overflow) {
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount overflow={overflow}>
<StyledCount countOverflow={overflow}>
{9}
<StyledCountSup>+</StyledCountSup>
</StyledCount>
@ -59,7 +59,7 @@ export const SessionNotificationCount = (props: Props) => {
}
return (
<StyledCountContainer shouldRender={shouldRender}>
<StyledCount overflow={overflow}>{count}</StyledCount>
<StyledCount countOverflow={overflow}>{count}</StyledCount>
</StyledCountContainer>
);
};

@ -715,7 +715,7 @@ export class SessionConversation extends React.Component<Props, State> {
}
private onClickAttachment(attachment: any, message: any) {
// message is MessageTypeInConvo.propsForMessage I think
// message is MessageModelProps.propsForMessage I think
const media = (message.attachments || []).map((attachmentForMedia: any) => {
return {
objectURL: attachmentForMedia.url,

@ -10,14 +10,18 @@ import { contextMenu } from 'react-contexify';
import { AttachmentType } from '../../../types/Attachment';
import { GroupNotification } from '../../conversation/GroupNotification';
import { GroupInvitation } from '../../conversation/GroupInvitation';
import { ConversationType } from '../../../state/ducks/conversations';
import {
ConversationType,
MessageModelProps,
SortedMessageModelProps,
} from '../../../state/ducks/conversations';
import { SessionLastSeenIndicator } from './SessionLastSeenIndicator';
import { ToastUtils } from '../../../session/utils';
import { TypingBubble } from '../../conversation/TypingBubble';
import { getConversationController } from '../../../session/conversations';
import { MessageModel } from '../../../models/message';
import { MessageRegularProps } from '../../../models/messageType';
import { getMessagesBySentAt } from '../../../data/data';
import { getMessageById, getMessagesBySentAt } from '../../../data/data';
import autoBind from 'auto-bind';
import { ConversationTypeEnum } from '../../../models/conversation';
import { DataExtractionNotification } from '../../conversation/DataExtractionNotification';
@ -25,13 +29,13 @@ import { DataExtractionNotification } from '../../conversation/DataExtractionNot
interface State {
showScrollButton: boolean;
animateQuotedMessageId?: string;
nextMessageToPlay: number | null;
nextMessageToPlay: number | undefined;
}
interface Props {
selectedMessages: Array<string>;
conversationKey: string;
messages: Array<MessageModel>;
messages: Array<SortedMessageModelProps>;
conversation: ConversationType;
ourPrimary: string;
messageContainerRef: React.RefObject<any>;
@ -52,7 +56,7 @@ interface Props {
messageTimestamp,
}: {
attachment: any;
messageTimestamp: number;
messageTimestamp?: number;
messageSender: string;
}) => void;
onDeleteSelectedMessages: () => Promise<void>;
@ -69,7 +73,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
this.state = {
showScrollButton: false,
nextMessageToPlay: null,
nextMessageToPlay: undefined,
};
autoBind(this);
@ -150,16 +154,21 @@ export class SessionMessagesList extends React.Component<Props, State> {
conversationType={conversation.type}
displayedName={displayedName}
isTyping={conversation.isTyping}
key="typing-bubble"
/>
{this.renderMessages(messages)}
<SessionScrollButton show={showScrollButton} onClick={this.scrollToBottom} />
<SessionScrollButton
show={showScrollButton}
onClick={this.scrollToBottom}
key="scroll-down-button"
/>
</div>
);
}
private displayUnreadBannerIndex(messages: Array<MessageModel>) {
private displayUnreadBannerIndex(messages: Array<SortedMessageModelProps>) {
const { conversation } = this.props;
if (conversation.unreadCount === 0) {
return -1;
@ -177,10 +186,13 @@ export class SessionMessagesList extends React.Component<Props, State> {
// Basically, count the number of incoming messages from the most recent one.
for (let index = 0; index <= messages.length - 1; index++) {
const message = messages[index];
if (message.attributes.type === 'incoming') {
if (message.propsForMessage.direction === 'incoming') {
incomingMessagesSoFar++;
// message.attributes.unread is !== undefined if the message is unread.
if (message.attributes.unread !== undefined && incomingMessagesSoFar >= unreadCount) {
if (
message.propsForMessage.isUnread !== undefined &&
incomingMessagesSoFar >= unreadCount
) {
findFirstUnreadIndex = index;
break;
}
@ -194,23 +206,22 @@ export class SessionMessagesList extends React.Component<Props, State> {
return findFirstUnreadIndex;
}
private renderMessages(messages: Array<MessageModel>) {
const { conversation, ourPrimary, selectedMessages } = this.props;
private renderMessages(messagesProps: Array<SortedMessageModelProps>) {
const { selectedMessages } = this.props;
const multiSelectMode = Boolean(selectedMessages.length);
let currentMessageIndex = 0;
let playableMessageIndex = 0;
const displayUnreadBannerIndex = this.displayUnreadBannerIndex(messages);
const displayUnreadBannerIndex = this.displayUnreadBannerIndex(messagesProps);
return (
<>
{messages.map((message: MessageModel) => {
const messageProps = message.propsForMessage;
const timerProps = message.propsForTimerNotification;
const propsForGroupInvitation = message.propsForGroupInvitation;
const propsForDataExtractionNotification = message.propsForDataExtractionNotification;
{messagesProps.map((messageProps: SortedMessageModelProps) => {
const timerProps = messageProps.propsForTimerNotification;
const propsForGroupInvitation = messageProps.propsForGroupInvitation;
const propsForDataExtractionNotification =
messageProps.propsForDataExtractionNotification;
const groupNotificationProps = message.propsForGroupNotification;
const groupNotificationProps = messageProps.propsForGroupNotification;
// IF there are some unread messages
// AND we found the last read message
@ -224,88 +235,71 @@ export class SessionMessagesList extends React.Component<Props, State> {
<SessionLastSeenIndicator
count={displayUnreadBannerIndex + 1} // count is used for the 118n of the string
show={showUnreadIndicator}
key={`unread-indicator-${message.id}`}
key={`unread-indicator-${messageProps.propsForMessage.id}`}
/>
);
console.warn('key', messageProps.propsForMessage.id);
currentMessageIndex = currentMessageIndex + 1;
if (groupNotificationProps) {
return (
<>
<GroupNotification {...groupNotificationProps} key={message.id} />
<React.Fragment key={messageProps.propsForMessage.id}>
<GroupNotification {...groupNotificationProps} />
{unreadIndicator}
</>
</React.Fragment>
);
}
if (propsForGroupInvitation) {
return (
<>
<GroupInvitation {...propsForGroupInvitation} key={message.id} />
<React.Fragment key={messageProps.propsForMessage.id}>
<GroupInvitation
{...propsForGroupInvitation}
key={messageProps.propsForMessage.id}
/>
{unreadIndicator}
</>
</React.Fragment>
);
}
if (propsForDataExtractionNotification) {
return (
<>
<React.Fragment key={messageProps.propsForMessage.id}>
<DataExtractionNotification
{...propsForDataExtractionNotification}
key={message.id}
key={messageProps.propsForMessage.id}
/>
{unreadIndicator}
</>
</React.Fragment>
);
}
if (timerProps) {
return (
<>
<TimerNotification {...timerProps} key={message.id} />
<React.Fragment key={messageProps.propsForMessage.id}>
<TimerNotification {...timerProps} key={messageProps.propsForMessage.id} />
{unreadIndicator}
</>
</React.Fragment>
);
}
if (!messageProps) {
return;
}
if (messageProps) {
messageProps.nextMessageToPlay = this.state.nextMessageToPlay;
messageProps.playableMessageIndex = playableMessageIndex;
messageProps.playNextMessage = this.playNextMessage;
}
playableMessageIndex++;
if (messageProps.conversationType === ConversationTypeEnum.GROUP) {
messageProps.weAreAdmin = conversation.groupAdmins?.includes(ourPrimary);
}
// a message is deletable if
// either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us)
// or the convo is public and we are an admin
const isDeletable =
messageProps.authorPhoneNumber === this.props.ourPrimary ||
!conversation.isPublic ||
(conversation.isPublic && !!messageProps.weAreAdmin);
messageProps.isDeletable = isDeletable;
messageProps.isAdmin = conversation.groupAdmins?.includes(messageProps.authorPhoneNumber);
// firstMessageOfSeries tells us to render the avatar only for the first message
// in a series of messages from the same user
return (
<>
<React.Fragment key={messageProps.propsForMessage.id}>
{this.renderMessage(
messageProps,
messageProps.firstMessageOfSeries,
multiSelectMode,
message
playableMessageIndex
)}
{unreadIndicator}
</>
</React.Fragment>
);
})}
</>
@ -313,40 +307,50 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
private renderMessage(
messageProps: MessageRegularProps,
messageProps: SortedMessageModelProps,
firstMessageOfSeries: boolean,
multiSelectMode: boolean,
message: MessageModel
playableMessageIndex: number
) {
const selected = !!messageProps?.id && this.props.selectedMessages.includes(messageProps.id);
messageProps.selected = selected;
messageProps.firstMessageOfSeries = firstMessageOfSeries;
messageProps.multiSelectMode = multiSelectMode;
messageProps.onSelectMessage = this.props.selectMessage;
messageProps.onDeleteMessage = this.props.deleteMessage;
messageProps.onReply = this.props.replyToMessage;
messageProps.onShowDetail = async () => {
const messageDetailsProps = await message.getPropsForMessageDetail();
this.props.showMessageDetails(messageDetailsProps);
const regularProps: MessageRegularProps = { ...messageProps.propsForMessage };
const messageId = messageProps.propsForMessage.id;
const selected =
!!messageProps?.propsForMessage.id && this.props.selectedMessages.includes(messageId);
regularProps.selected = selected;
regularProps.firstMessageOfSeries = firstMessageOfSeries;
regularProps.multiSelectMode = multiSelectMode;
regularProps.onSelectMessage = this.props.selectMessage;
regularProps.onDeleteMessage = this.props.deleteMessage;
regularProps.onReply = this.props.replyToMessage;
regularProps.onShowDetail = async () => {
const found = await getMessageById(messageId);
if (found) {
const messageDetailsProps = await found.getPropsForMessageDetail();
this.props.showMessageDetails(messageDetailsProps);
} else {
window.log.warn(`Message ${messageId} not found in db`);
}
};
messageProps.onClickAttachment = (attachment: AttachmentType) => {
regularProps.onClickAttachment = (attachment: AttachmentType) => {
this.props.onClickAttachment(attachment, messageProps);
};
messageProps.onDownload = (attachment: AttachmentType) => {
regularProps.onDownload = (attachment: AttachmentType) => {
this.props.onDownloadAttachment({
attachment,
messageTimestamp: messageProps.timestamp,
messageSender: messageProps.authorPhoneNumber,
messageTimestamp: messageProps.propsForMessage.timestamp,
messageSender: messageProps.propsForMessage.authorPhoneNumber,
});
};
messageProps.isQuotedMessageToAnimate = messageProps.id === this.state.animateQuotedMessageId;
regularProps.isQuotedMessageToAnimate = messageId === this.state.animateQuotedMessageId;
if (messageProps.quote) {
messageProps.quote.onClick = (options: {
if (regularProps.quote) {
regularProps.quote.onClick = (options: {
quoteAuthor: string;
quoteId: any;
referencedMessageNotFound: boolean;
@ -355,7 +359,11 @@ export class SessionMessagesList extends React.Component<Props, State> {
};
}
return <Message {...messageProps} key={messageProps.id} />;
regularProps.nextMessageToPlay = this.state.nextMessageToPlay;
regularProps.playableMessageIndex = playableMessageIndex;
regularProps.playNextMessage = this.playNextMessage;
return <Message {...regularProps} key={messageId} />;
}
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -379,7 +387,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
if (this.getScrollOffsetBottomPx() === 0) {
void conversation.markRead(messages[0].attributes.received_at);
void conversation.markRead(messages[0].propsForMessage.receivedAt);
}
}
@ -389,12 +397,12 @@ export class SessionMessagesList extends React.Component<Props, State> {
*/
private readonly playNextMessage = (index: any) => {
const { messages } = this.props;
let nextIndex: number | null = index - 1;
let nextIndex: number | undefined = index - 1;
// to prevent autoplaying as soon as a message is received.
const latestMessagePlayed = index <= 0 || messages.length < index - 1;
if (latestMessagePlayed) {
nextIndex = null;
nextIndex = undefined;
this.setState({
nextMessageToPlay: nextIndex,
});
@ -406,7 +414,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
const nextAuthorNumber = messages[index - 1].propsForMessage.authorPhoneNumber;
const differentAuthor = prevAuthorNumber !== nextAuthorNumber;
if (differentAuthor) {
nextIndex = null;
nextIndex = undefined;
}
this.setState({
@ -463,7 +471,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
const numMessages =
this.props.messages.length + Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT;
const oldLen = messages.length;
const previousTopMessage = messages[oldLen - 1]?.id;
const previousTopMessage = messages[oldLen - 1]?.propsForMessage.id;
fetchMessagesForConversation({ conversationKey, count: numMessages });
if (previousTopMessage && oldLen !== messages.length) {
@ -484,7 +492,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
}
if (message) {
this.scrollToMessage(message.id);
this.scrollToMessage(message.propsForMessage.id);
}
}
@ -584,9 +592,9 @@ export class SessionMessagesList extends React.Component<Props, State> {
const collection = await getMessagesBySentAt(quoteId);
const found = Boolean(
collection.find((item: MessageModel) => {
const messageAuthor = item.propsForMessage?.authorPhoneNumber;
const messageAuthor = item.getSource();
return messageAuthor && quoteAuthor === messageAuthor;
return Boolean(messageAuthor && quoteAuthor === messageAuthor);
})
);
@ -598,7 +606,7 @@ export class SessionMessagesList extends React.Component<Props, State> {
return;
}
const databaseId = targetMessage.id;
const databaseId = targetMessage.propsForMessage.id;
this.scrollToMessage(databaseId, true);
}

@ -4,11 +4,7 @@ import { NotificationForConvoOption, TimerOption } from '../../conversation/Conv
import { Item, Submenu } from 'react-contexify';
import { ConversationNotificationSettingType } from '../../../models/conversation';
import { useDispatch } from 'react-redux';
import {
adminLeaveClosedGroup,
changeNickNameModal,
updateConfirmModal,
} from '../../../state/ducks/modalDialog';
import { changeNickNameModal, updateConfirmModal } from '../../../state/ducks/modalDialog';
import { getConversationController } from '../../../session/conversations';
import {
blockConvoById,
@ -134,6 +130,8 @@ export function getDeleteContactMenuItem(
isKickedFromGroup: boolean | undefined,
conversationId: string
): JSX.Element | null {
const dispatch = useDispatch();
if (
showDeleteContact(
Boolean(isMe),
@ -150,7 +148,6 @@ export function getDeleteContactMenuItem(
menuItemText = window.i18n('delete');
}
const dispatch = useDispatch();
const onClickClose = () => {
dispatch(updateConfirmModal(null));
};

@ -26,6 +26,7 @@ import {
actions as conversationActions,
ConversationType as ReduxConversationType,
LastMessageStatusType,
MessageModelProps,
} from '../state/ducks/conversations';
import { ExpirationTimerUpdateMessage } from '../session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage';
import { TypingMessage } from '../session/messages/outgoing/controlMessage/TypingMessage';
@ -885,7 +886,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
window.inboxStore?.dispatch(
conversationActions.messageAdded({
conversationKey: this.id,
messageModel: model,
messageModelProps: model.getProps(),
})
);
const unreadCount = await this.getUnreadCount();
@ -939,11 +940,12 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
const oldUnreadNowReadAttrs = oldUnreadNowRead.map(m => m.attributes);
await saveMessages(oldUnreadNowReadAttrs);
const allProps: Array<MessageModelProps> = [];
for (const nowRead of oldUnreadNowRead) {
nowRead.generateProps(false);
allProps.push(nowRead.generateProps(false));
}
window.inboxStore?.dispatch(conversationActions.messagesChanged(oldUnreadNowRead));
window.inboxStore?.dispatch(conversationActions.messagesChanged(allProps));
// Some messages we're marking read are local notifications with no sender
read = _.filter(read, m => Boolean(m.sender));

@ -11,10 +11,11 @@ import { PubKey } from '../../ts/session/types';
import { UserUtils } from '../../ts/session/utils';
import {
DataExtractionNotificationMsg,
DataExtractionNotificationProps,
fillMessageAttributesWithDefaults,
MessageAttributes,
MessageAttributesOptionals,
MessageModelType,
PropsForDataExtractionNotification,
} from './messageType';
import autoBind from 'auto-bind';
@ -23,7 +24,11 @@ import { ConversationModel, ConversationTypeEnum } from './conversation';
import {
actions as conversationActions,
FindAndFormatContactType,
LastMessageStatusType,
MessageModelProps,
MessagePropsDetails,
PropsForExpirationTimer,
PropsForGroupInvitation,
PropsForGroupUpdate,
PropsForGroupUpdateAdd,
PropsForGroupUpdateArray,
@ -31,6 +36,8 @@ import {
PropsForGroupUpdateKicked,
PropsForGroupUpdateName,
PropsForGroupUpdateRemove,
PropsForMessage,
PropsForSearchResults,
} from '../state/ducks/conversations';
import { VisibleMessage } from '../session/messages/outgoing/visibleMessage/VisibleMessage';
import { buildSyncMessage } from '../session/utils/syncUtils';
@ -49,13 +56,6 @@ import { isUsFromCache } from '../session/utils/User';
import { perfEnd, perfStart } from '../session/utils/Performance';
export class MessageModel extends Backbone.Model<MessageAttributes> {
public propsForTimerNotification: any;
public propsForGroupNotification: any;
public propsForGroupInvitation: any;
public propsForDataExtractionNotification?: DataExtractionNotificationProps;
public propsForSearchResult: any;
public propsForMessage: any;
constructor(attributes: MessageAttributesOptionals) {
const filledAttrs = fillMessageAttributesWithDefaults(attributes);
super(filledAttrs);
@ -80,21 +80,18 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
window.contextMenuShown = false;
this.generateProps(false);
// this.generateProps(false);
}
// Keep props ready
public generateProps(triggerEvent = true) {
public getProps(): MessageModelProps {
const propsForTimerNotification = this.getPropsForTimerNotification();
const propsForGroupNotification = this.getPropsForGroupNotification();
const propsForGroupInvitation = this.getPropsForGroupInvitation();
const propsForDataExtractionNotification = this.isDataExtractionNotification()
? this.getPropsForDataExtractionNotification()
: null;
const propsForDataExtractionNotification = this.getPropsForDataExtractionNotification();
const propsForSearchResult = this.getPropsForSearchResult();
const propsForMessage = this.getPropsForMessage();
const messageProps = {
const messageProps: MessageModelProps = {
propsForMessage,
propsForSearchResult,
propsForDataExtractionNotification,
@ -102,10 +99,16 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
propsForGroupNotification,
propsForTimerNotification,
};
return messageProps;
}
// Keep props ready
public generateProps(triggerEvent = true): MessageModelProps {
const messageProps = this.getProps();
if (triggerEvent) {
window.inboxStore?.dispatch(conversationActions.messageChanged(messageProps));
}
return messageProps;
}
public idForLogging() {
@ -284,7 +287,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
await window.Signal.Migrations.deleteExternalMessageFiles(this.attributes);
}
public getPropsForTimerNotification(): PropsForExpirationTimer {
public getPropsForTimerNotification(): PropsForExpirationTimer | null {
if (!this.isExpirationTimerUpdate()) {
return null;
}
@ -294,9 +297,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
const { expireTimer, fromSync, source } = timerUpdate;
const timespan = window.Whisper.ExpirationTimerOptions.getName(expireTimer || 0) as
| string
| null;
const timespan = window.Whisper.ExpirationTimerOptions.getName(expireTimer || 0) as string;
const disabled = !expireTimer;
const basicProps: PropsForExpirationTimer = {
@ -309,7 +310,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return basicProps;
}
public getPropsForGroupInvitation() {
public getPropsForGroupInvitation(): PropsForGroupInvitation | null {
if (!this.isGroupInvitation()) {
return null;
}
@ -332,18 +333,20 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
serverName: invitation.name,
url: serverAddress,
direction,
onJoinClick: () => {
acceptOpenGroupInvitation(invitation.url, invitation.name);
},
acceptUrl: invitation.url,
messageId: this.id as string,
};
}
public getPropsForDataExtractionNotification(): DataExtractionNotificationProps | undefined {
public getPropsForDataExtractionNotification(): PropsForDataExtractionNotification | null {
if (!this.isDataExtractionNotification()) {
return null;
}
const dataExtractionNotification = this.get('dataExtractionNotification');
if (!dataExtractionNotification) {
window.log.warn('dataExtractionNotification should not happen');
return;
return null;
}
const contact = this.findAndFormatContact(dataExtractionNotification.source);
@ -361,8 +364,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
public findAndFormatContact(pubkey: string): FindAndFormatContactType {
const contactModel = this.findContact(pubkey);
let profileName;
if (pubkey === window.storage.get('primaryDevicePubKey')) {
let isMe = false;
UserUtils.getOurPubKeyStrFromCache();
if (pubkey === UserUtils.getOurPubKeyStrFromCache()) {
profileName = window.i18n('you');
isMe = true;
} else {
profileName = contactModel ? contactModel.getProfileName() : null;
}
@ -373,10 +379,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
name: (contactModel ? contactModel.getName() : null) as string | null,
profileName: profileName as string | null,
title: (contactModel ? contactModel.getTitle() : null) as string | null,
isMe,
};
}
public getPropsForGroupNotification(): PropsForGroupUpdate {
public getPropsForGroupNotification(): PropsForGroupUpdate | null {
if (!this.isGroupUpdate()) {
return null;
}
@ -465,7 +472,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
};
}
public getMessagePropStatus() {
public getMessagePropStatus(): LastMessageStatusType {
if (this.hasErrors()) {
return 'error';
}
@ -492,42 +499,31 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
return 'sending';
}
public getPropsForSearchResult() {
public getPropsForSearchResult(): PropsForSearchResults {
const fromNumber = this.getSource();
const from = this.findAndFormatContact(fromNumber);
if (fromNumber === UserUtils.getOurPubKeyStrFromCache()) {
(from as any).isMe = true;
}
const toNumber = this.get('conversationId');
let to = this.findAndFormatContact(toNumber) as any;
if (toNumber === UserUtils.getOurPubKeyStrFromCache()) {
to.isMe = true;
} else if (fromNumber === toNumber) {
to = {
isMe: true,
};
}
const to = this.findAndFormatContact(toNumber);
return {
from,
to,
// isSelected: this.isSelected,
id: this.id,
id: this.id as string,
conversationId: this.get('conversationId'),
receivedAt: this.get('received_at'),
snippet: this.get('snippet'),
};
}
public getPropsForMessage(options: any = {}) {
const phoneNumber = this.getSource();
const contact = this.findAndFormatContact(phoneNumber);
const contactModel = this.findContact(phoneNumber);
public getPropsForMessage(options: any = {}): PropsForMessage {
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
const sender = this.getSource();
const senderContact = this.findAndFormatContact(sender);
const senderContactModel = this.findContact(sender);
const authorAvatarPath = contactModel ? contactModel.getAvatarPath() : null;
const authorAvatarPath = senderContactModel ? senderContactModel.getAvatarPath() : null;
const expirationLength = this.get('expireTimer') * 1000;
const expireTimerStart = this.get('expirationStartTimestamp');
@ -536,26 +532,35 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const conversation = this.getConversation();
const convoId = conversation ? conversation.id : undefined;
const isGroup = !!conversation && !conversation.isPrivate();
const isPublic = !!this.get('isPublic');
const isPublicOpenGroupV2 = isOpenGroupV2(this.getConversation()?.id || '');
const attachments = this.get('attachments') || [];
const isTrustedForAttachmentDownload = this.isTrustedForAttachmentDownload();
const groupAdmins = (isGroup && conversation?.get('groupAdmins')) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
// a message is deletable if
// either we sent it,
// or the convo is not a public one (in this case, we will only be able to delete for us)
// or the convo is public and we are an admin
const isDeletable = sender === ourPubkey || !isPublic || (isPublic && !!weAreAdmin);
return {
const isSenderAdmin = groupAdmins.includes(sender);
const props: PropsForMessage = {
text: this.createNonBreakingLastSeparator(this.get('body')),
id: this.id,
direction: this.isIncoming() ? 'incoming' : 'outgoing',
id: this.id as string,
direction: (this.isIncoming() ? 'incoming' : 'outgoing') as MessageModelType,
timestamp: this.get('sent_at'),
receivedAt: this.get('received_at'),
serverTimestamp: this.get('serverTimestamp'),
status: this.getMessagePropStatus(),
authorName: contact.name,
authorProfileName: contact.profileName,
authorPhoneNumber: contact.phoneNumber,
authorName: senderContact.name,
authorProfileName: senderContact.profileName,
authorPhoneNumber: senderContact.phoneNumber,
conversationType: isGroup ? ConversationTypeEnum.GROUP : ConversationTypeEnum.PRIVATE,
convoId,
convoId: this.get('conversationId'),
attachments: attachments
.filter((attachment: any) => !attachment.error)
.map((attachment: any) => this.getPropsForAttachment(attachment)),
@ -567,12 +572,14 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
expirationTimestamp,
isPublic,
isOpenGroupV2: isPublicOpenGroupV2,
isKickedFromGroup: conversation && conversation.get('isKickedFromGroup'),
isKickedFromGroup: conversation?.get('isKickedFromGroup'),
isTrustedForAttachmentDownload,
onRetrySend: this.retrySend,
markRead: this.markRead,
weAreAdmin,
isDeletable,
isSenderAdmin,
};
return props;
}
public createNonBreakingLastSeparator(text?: string) {
@ -639,7 +646,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
public getPropsForQuote(options: any = {}) {
const { noClick } = options;
const quote = this.get('quote');
if (!quote) {
@ -652,16 +658,6 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
const authorName = contact ? contact.getContactProfileNameOrShortenedPubKey() : null;
const isFromMe = contact ? contact.id === UserUtils.getOurPubKeyStrFromCache() : false;
const onClick = noClick
? null
: (event: any) => {
event.stopPropagation();
this.trigger('scroll-to-message', {
author,
id,
referencedMessageNotFound,
});
};
const firstAttachment = quote.attachments && quote.attachments[0];
@ -672,12 +668,18 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
authorPhoneNumber: author,
messageId: id,
authorName,
onClick,
referencedMessageNotFound,
};
}
public getPropsForAttachment(attachment: any) {
public getPropsForAttachment(attachment: {
path?: string;
pending?: boolean;
flags: number;
size: number;
screenshot: any;
thumbnail: any;
}) {
if (!attachment) {
return null;
}
@ -709,7 +711,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
};
}
public async getPropsForMessageDetail() {
public async getPropsForMessageDetail(): Promise<MessagePropsDetails> {
// We include numbers we didn't successfully send to so we can display errors.
// Older messages don't have the recipients included on the message, so we fall
// back to the conversation's current recipients
@ -754,12 +756,11 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
finalContacts,
contact => `${contact.isPrimaryDevice ? '0' : '1'}${contact.phoneNumber}`
);
return {
const toRet: MessagePropsDetails = {
sentAt: this.get('sent_at'),
receivedAt: this.get('received_at'),
message: {
...this.propsForMessage,
...this.getPropsForMessage(),
disableMenu: true,
// To ensure that group avatar doesn't show up
conversationType: ConversationTypeEnum.PRIVATE,
@ -767,6 +768,8 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
errors,
contacts: sortedContacts,
};
return toRet;
}
public copyPubKey() {

@ -109,7 +109,7 @@ export interface DataExtractionNotificationMsg {
referencedAttachmentTimestamp: number; // the attachment timestamp he screenshot
}
export type DataExtractionNotificationProps = DataExtractionNotificationMsg & {
export type PropsForDataExtractionNotification = DataExtractionNotificationMsg & {
name: string;
};
@ -247,7 +247,6 @@ export interface MessageRegularProps {
onClickLinkPreview?: (url: string) => void;
onSelectMessage: (messageId: string) => void;
onReply?: (messagId: number) => void;
onRetrySend?: () => void;
onDownload?: (attachment: AttachmentType) => void;
onDeleteMessage: (messageId: string) => void;
onShowDetail: () => void;

@ -38,7 +38,7 @@ export async function onError(ev: any) {
window.inboxStore?.dispatch(
conversationActions.messageAdded({
conversationKey: conversation.id,
messageModel: message,
messageModelProps: message.getProps(),
})
);

@ -440,7 +440,7 @@ export async function handleMessageJob(
window.inboxStore?.dispatch(
conversationActions.messageAdded({
conversationKey: conversation.id,
messageModel: message,
messageModelProps: message.getProps(),
})
);
getMessageController().register(message.id, message);

@ -6,50 +6,22 @@ import { getConversationController } from '../../session/conversations';
import { MessageModel } from '../../models/message';
import { getMessagesByConversation } from '../../data/data';
import { ConversationTypeEnum } from '../../models/conversation';
import { MessageDeliveryStatus } from '../../models/messageType';
// State
export type MessageType = {
id: string;
conversationId: string;
receivedAt: number;
snippet: string;
from: {
phoneNumber: string;
isMe?: boolean;
name?: string;
color?: string;
profileName?: string;
avatarPath?: string;
};
to: {
groupName?: string;
phoneNumber: string;
isMe?: boolean;
name?: string;
profileName?: string;
};
isSelected?: boolean;
import {
MessageDeliveryStatus,
MessageModelType,
PropsForDataExtractionNotification,
} from '../../models/messageType';
export type MessageModelProps = {
propsForMessage: PropsForMessage;
propsForSearchResult: PropsForSearchResults | null;
propsForGroupInvitation: PropsForGroupInvitation | null;
propsForTimerNotification: PropsForExpirationTimer | null;
propsForDataExtractionNotification: PropsForDataExtractionNotification | null;
propsForGroupNotification: PropsForGroupUpdate | null;
};
export type MessageTypeInConvo = {
id: string;
conversationId: string;
attributes: any;
propsForMessage: Object;
propsForSearchResult: Object;
propsForGroupInvitation: Object;
propsForTimerNotification: Object;
propsForGroupNotification: Object;
firstMessageOfSeries: boolean;
receivedAt: number;
getPropsForMessageDetail(): Promise<any>;
};
export type MessagePropsDetails = {};
export type LastMessageStatusType = MessageDeliveryStatus | null;
@ -59,10 +31,11 @@ export type FindAndFormatContactType = {
name: string | null;
profileName: string | null;
title: string | null;
isMe: boolean;
};
export type PropsForExpirationTimer = {
timespan: string | null;
timespan: string;
disabled: boolean;
phoneNumber: string;
avatarPath: string | null;
@ -70,7 +43,7 @@ export type PropsForExpirationTimer = {
profileName: string | null;
title: string | null;
type: 'fromMe' | 'fromSync' | 'fromOther';
} | null;
};
export type PropsForGroupUpdateGeneral = {
type: 'general';
@ -98,17 +71,64 @@ export type PropsForGroupUpdateName = {
newName: string;
};
export type PropsForGroupUpdateArray = Array<
export type PropsForGroupUpdateType =
| PropsForGroupUpdateGeneral
| PropsForGroupUpdateAdd
| PropsForGroupUpdateKicked
| PropsForGroupUpdateName
| PropsForGroupUpdateRemove
>;
| PropsForGroupUpdateRemove;
export type PropsForGroupUpdateArray = Array<PropsForGroupUpdateType>;
export type PropsForGroupUpdate = {
changes: PropsForGroupUpdateArray;
} | null;
};
export type PropsForGroupInvitation = {
serverName: string;
url: string;
direction: MessageModelType;
acceptUrl: string;
messageId: string;
};
export type PropsForSearchResults = {
from: FindAndFormatContactType;
to: FindAndFormatContactType;
id: string;
conversationId: string;
receivedAt: number | undefined;
snippet?: string; //not sure about the type of snippet
};
export type PropsForMessage = {
text: string | null;
id: string;
direction: MessageModelType;
timestamp: number | undefined;
receivedAt: number | undefined;
serverTimestamp: number | undefined;
status: LastMessageStatusType;
authorName: string | null;
authorProfileName: string | null;
authorPhoneNumber: string;
conversationType: ConversationTypeEnum;
convoId: string;
attachments: any;
previews: any;
quote: any;
authorAvatarPath: string | null;
isUnread: boolean;
expirationLength: number;
expirationTimestamp: number | null;
isPublic: boolean;
isOpenGroupV2: boolean;
isKickedFromGroup: boolean | undefined;
isTrustedForAttachmentDownload: boolean;
weAreAdmin: boolean;
isSenderAdmin: boolean;
isDeletable: boolean;
};
export type LastMessageType = {
status: LastMessageStatusType;
@ -148,13 +168,13 @@ export type ConversationLookupType = {
export type ConversationsStateType = {
conversationLookup: ConversationLookupType;
selectedConversation?: string;
messages: Array<MessageTypeInConvo>;
messages: Array<SortedMessageModelProps>;
};
async function getMessages(
conversationKey: string,
numMessages: number
): Promise<Array<MessageTypeInConvo>> {
): Promise<Array<SortedMessageModelProps>> {
const conversation = getConversationController().get(conversationKey);
if (!conversation) {
// no valid conversation, early return
@ -178,55 +198,78 @@ async function getMessages(
});
// Set first member of series here.
const messageModels = messageSet.models;
const messageModelsProps: Array<SortedMessageModelProps> = messageSet.models.map(m => {
return { ...m.getProps(), firstMessageOfSeries: true };
});
const isPublic = conversation.isPublic();
const messagesPickedUp = messageModels.map(makeMessageTypeFromMessageModel);
const sortedMessage = sortMessages(messagesPickedUp, isPublic);
const sortedMessageProps = sortMessages(messageModelsProps, isPublic);
// no need to do that `firstMessageOfSeries` on a private chat
if (conversation.isPrivate()) {
return sortedMessage;
return sortedMessageProps;
}
return updateFirstMessageOfSeries(sortedMessage);
return updateFirstMessageOfSeries(sortedMessageProps);
}
const updateFirstMessageOfSeries = (messageModels: Array<any>) => {
export type SortedMessageModelProps = MessageModelProps & {
firstMessageOfSeries: boolean;
};
const updateFirstMessageOfSeries = (
messageModelsProps: Array<MessageModelProps>
): Array<SortedMessageModelProps> => {
// messages are got from the more recent to the oldest, so we need to check if
// the next messages in the list is still the same author.
// The message is the first of the series if the next message is not from the same author
for (let i = 0; i < messageModels.length; i++) {
const sortedMessageProps: Array<SortedMessageModelProps> = [];
for (let i = 0; i < messageModelsProps.length; i++) {
// Handle firstMessageOfSeries for conditional avatar rendering
let firstMessageOfSeries = true;
const currentSender = messageModels[i].propsForMessage?.authorPhoneNumber;
const currentSender = messageModelsProps[i].propsForMessage?.authorPhoneNumber;
const nextSender =
i < messageModels.length - 1
? messageModels[i + 1].propsForMessage?.authorPhoneNumber
i < messageModelsProps.length - 1
? messageModelsProps[i + 1].propsForMessage?.authorPhoneNumber
: undefined;
if (i >= 0 && currentSender === nextSender) {
firstMessageOfSeries = false;
}
if (messageModels[i].propsForMessage) {
messageModels[i].propsForMessage.firstMessageOfSeries = firstMessageOfSeries;
}
sortedMessageProps.push({ ...messageModelsProps[i], firstMessageOfSeries });
}
return messageModels;
return sortedMessageProps;
};
type FetchedMessageResults = {
conversationKey: string;
messagesProps: Array<SortedMessageModelProps>;
};
const fetchMessagesForConversation = createAsyncThunk(
'messages/fetchByConversationKey',
async ({ conversationKey, count }: { conversationKey: string; count: number }) => {
async ({
conversationKey,
count,
}: {
conversationKey: string;
count: number;
}): Promise<FetchedMessageResults> => {
const beforeTimestamp = Date.now();
const messages = await getMessages(conversationKey, count);
const messagesProps = await getMessages(conversationKey, count);
const afterTimestamp = Date.now();
const time = afterTimestamp - beforeTimestamp;
window?.log?.info(`Loading ${messages.length} messages took ${time}ms to load.`);
window?.log?.info(`Loading ${messagesProps.length} messages took ${time}ms to load.`);
return {
conversationKey,
messages,
messagesProps: messagesProps.map(m => {
return {
...m,
firstMessageOfSeries: true,
};
}),
};
}
);
@ -266,17 +309,17 @@ export type MessageExpiredActionType = {
};
export type MessageChangedActionType = {
type: 'MESSAGE_CHANGED';
payload: MessageModel;
payload: MessageModelProps;
};
export type MessagesChangedActionType = {
type: 'MESSAGES_CHANGED';
payload: Array<MessageModel>;
payload: Array<MessageModelProps>;
};
export type MessageAddedActionType = {
type: 'MESSAGE_ADDED';
payload: {
conversationKey: string;
messageModel: MessageModel;
messageModelProps: MessageModelProps;
};
};
export type MessageDeletedActionType = {
@ -304,7 +347,7 @@ export type FetchMessagesForConversationType = {
type: 'messages/fetchByConversationKey/fulfilled';
payload: {
conversationKey: string;
messages: Array<MessageModel>;
messages: Array<MessageModelProps>;
};
};
@ -389,32 +432,32 @@ function messageExpired({
};
}
function messageChanged(messageModel: MessageModel): MessageChangedActionType {
function messageChanged(messageModelProps: MessageModelProps): MessageChangedActionType {
return {
type: 'MESSAGE_CHANGED',
payload: messageModel,
payload: messageModelProps,
};
}
function messagesChanged(messageModels: Array<MessageModel>): MessagesChangedActionType {
function messagesChanged(messageModelsProps: Array<MessageModelProps>): MessagesChangedActionType {
return {
type: 'MESSAGES_CHANGED',
payload: messageModels,
payload: messageModelsProps,
};
}
function messageAdded({
conversationKey,
messageModel,
messageModelProps,
}: {
conversationKey: string;
messageModel: MessageModel;
messageModelProps: MessageModelProps;
}): MessageAddedActionType {
return {
type: 'MESSAGE_ADDED',
payload: {
conversationKey,
messageModel,
messageModelProps,
},
};
}
@ -464,31 +507,6 @@ export function openConversationExternal(
// Reducer
const toPickFromMessageModel = [
'attributes',
'id',
'propsForSearchResult',
'propsForMessage',
'receivedAt',
'conversationId',
'firstMessageOfSeries',
'propsForGroupInvitation',
'propsForTimerNotification',
'propsForGroupNotification',
'propsForDataExtractionNotification',
// FIXME below are what is needed to fetch on the fly messageDetails. This is not the react way
'getPropsForMessageDetail',
'get',
'getConversation',
'isIncoming',
'findAndFormatContact',
'findContact',
'getStatus',
'getMessagePropStatus',
'hasErrors',
'isOutgoing',
];
function getEmptyState(): ConversationsStateType {
return {
conversationLookup: {},
@ -496,14 +514,10 @@ function getEmptyState(): ConversationsStateType {
};
}
const makeMessageTypeFromMessageModel = (message: MessageModel) => {
return _.pick(message as any, toPickFromMessageModel) as MessageTypeInConvo;
};
function sortMessages(
messages: Array<MessageTypeInConvo>,
messages: Array<SortedMessageModelProps>,
isPublic: boolean
): Array<MessageTypeInConvo> {
): Array<SortedMessageModelProps> {
// we order by serverTimestamp for public convos
// be sure to update the sorting order to fetch messages from the DB too at getMessagesByConversation
if (isPublic) {
@ -511,7 +525,7 @@ function sortMessages(
(a: any, b: any) => b.attributes.serverTimestamp - a.attributes.serverTimestamp
);
}
if (messages.some(n => !n.attributes.sent_at && !n.attributes.received_at)) {
if (messages.some(n => !n.propsForMessage.timestamp && !n.propsForMessage.receivedAt)) {
throw new Error('Found some messages without any timestamp set');
}
@ -528,10 +542,12 @@ function sortMessages(
function handleMessageAdded(state: ConversationsStateType, action: MessageAddedActionType) {
const { messages } = state;
const { conversationKey, messageModel } = action.payload;
const { conversationKey, messageModelProps: addedMessageProps } = action.payload;
if (conversationKey === state.selectedConversation) {
const addedMessage = makeMessageTypeFromMessageModel(messageModel);
const messagesWithNewMessage = [...messages, addedMessage];
const messagesWithNewMessage = [
...messages,
{ ...addedMessageProps, firstMessageOfSeries: true },
];
const convo = state.conversationLookup[state.selectedConversation];
const isPublic = convo?.isPublic || false;
@ -551,9 +567,11 @@ function handleMessageAdded(state: ConversationsStateType, action: MessageAddedA
function handleMessageChanged(state: ConversationsStateType, action: MessageChangedActionType) {
const { payload } = action;
const messageInStoreIndex = state?.messages?.findIndex(m => m.id === payload.id);
const messageInStoreIndex = state?.messages?.findIndex(
m => m.propsForMessage.id === payload.propsForMessage.id
);
if (messageInStoreIndex >= 0) {
const changedMessage = _.pick(payload as any, toPickFromMessageModel) as MessageTypeInConvo;
const changedMessage = { ...payload, firstMessageOfSeries: true };
// we cannot edit the array directly, so slice the first part, insert our edited message, and slice the second part
const editedMessages = [
...state.messages.slice(0, messageInStoreIndex),
@ -561,7 +579,7 @@ function handleMessageChanged(state: ConversationsStateType, action: MessageChan
...state.messages.slice(messageInStoreIndex + 1),
];
const convo = state.conversationLookup[payload.get('conversationId')];
const convo = state.conversationLookup[payload.propsForMessage.convoId];
const isPublic = convo?.isPublic || false;
// reorder the messages depending on the timestamp (we might have an updated serverTimestamp now)
const sortedMessage = sortMessages(editedMessages, isPublic);
@ -598,7 +616,7 @@ function handleMessageExpiredOrDeleted(
if (conversationKey === state.selectedConversation) {
// search if we find this message id.
// we might have not loaded yet, so this case might not happen
const messageInStoreIndex = state?.messages.findIndex(m => m.id === messageId);
const messageInStoreIndex = state?.messages.findIndex(m => m.propsForMessage.id === messageId);
if (messageInStoreIndex >= 0) {
// we cannot edit the array directly, so slice the first part, and slice the second part,
// keeping the index removed out
@ -714,15 +732,12 @@ export function reducer(
// this is called once the messages are loaded from the db for the currently selected conversation
if (action.type === fetchMessagesForConversation.fulfilled.type) {
const { messages, conversationKey } = action.payload as any;
const { messagesProps, conversationKey } = action.payload as FetchedMessageResults;
// double check that this update is for the shown convo
if (conversationKey === state.selectedConversation) {
const lightMessages = messages.map((m: any) => _.pick(m, toPickFromMessageModel)) as Array<
MessageTypeInConvo
>;
return {
...state,
messages: lightMessages,
messages: { ...messagesProps },
};
}
return state;

@ -8,7 +8,7 @@ import { makeLookup } from '../../util/makeLookup';
import {
ConversationType,
MessageExpiredActionType,
MessageType,
PropsForSearchResults,
RemoveAllConversationsActionType,
SelectedConversationChangedActionType,
} from './conversations';
@ -23,10 +23,10 @@ export type SearchStateType = {
query: string;
normalizedPhoneNumber?: string;
// We need to store messages here, because they aren't anywhere else in state
messages: Array<MessageType>;
messages: Array<PropsForSearchResults>;
selectedMessage?: string;
messageLookup: {
[key: string]: MessageType;
[key: string]: PropsForSearchResults;
};
// For conversations we store just the id, and pull conversation props in the selector
conversations: Array<string>;
@ -37,7 +37,7 @@ export type SearchStateType = {
type SearchResultsPayloadType = {
query: string;
normalizedPhoneNumber?: string;
messages: Array<MessageType>;
messages: Array<PropsForSearchResults>;
conversations: Array<string>;
contacts: Array<string>;
};
@ -211,7 +211,7 @@ function getAdvancedSearchOptionsFromQuery(query: string): AdvancedSearchOptions
return filters;
}
const getMessageProps = (messages: Array<MessageType>) => {
const getMessageProps = (messages: Array<PropsForSearchResults>) => {
if (!messages || !messages.length) {
return [];
}
@ -224,7 +224,7 @@ const getMessageProps = (messages: Array<MessageType>) => {
const model = new MessageModel(overridenProps);
return model.propsForSearchResult;
return model.getPropsForSearchResult();
});
};

@ -5,7 +5,8 @@ import {
ConversationLookupType,
ConversationsStateType,
ConversationType,
MessageTypeInConvo,
MessageModelProps,
SortedMessageModelProps,
} from '../ducks/conversations';
import { getIntl, getOurNumber } from './user';
@ -46,7 +47,7 @@ export const getOurPrimaryConversation = createSelector(
export const getMessagesOfSelectedConversation = createSelector(
getConversations,
(state: ConversationsStateType): Array<MessageTypeInConvo> => state.messages
(state: ConversationsStateType): Array<SortedMessageModelProps> => state.messages
);
function getConversationTitle(conversation: ConversationType, testingi18n?: LocalizerType): string {

Loading…
Cancel
Save