chore: broke apart big Message selectors into smaller ones

pull/2793/head
Audric Ackermann 2 years ago
parent 461b192f37
commit f2cddb83c8

@ -1,19 +1,18 @@
import React, { useState } from 'react';
import classNames from 'classnames';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { useDisableDrag } from '../../hooks/useDisableDrag';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import { isEqual } from 'lodash';
import {
useAvatarPath,
useConversationUsername,
useIsClosedGroup,
} from '../../hooks/useParamSelector';
import { isMessageSelectionMode } from '../../state/selectors/conversations';
import { SessionIcon } from '../icon';
import { AvatarPlaceHolder } from './AvatarPlaceHolder/AvatarPlaceHolder';
import { ClosedGroupAvatar } from './AvatarPlaceHolder/ClosedGroupAvatar';
import { useDisableDrag } from '../../hooks/useDisableDrag';
import styled from 'styled-components';
import { SessionIcon } from '../icon';
import { useSelector } from 'react-redux';
import { isMessageSelectionMode } from '../../state/selectors/conversations';
export enum AvatarSize {
XS = 28,
@ -90,7 +89,7 @@ const AvatarImage = (props: {
datatestId?: string;
handleImageError: () => any;
}) => {
const { avatarPath, base64Data, name, imageBroken, datatestId, handleImageError } = props;
const { avatarPath, base64Data, imageBroken, datatestId, handleImageError } = props;
const disableDrag = useDisableDrag();
@ -103,7 +102,6 @@ const AvatarImage = (props: {
<img
onError={handleImageError}
onDragStart={disableDrag}
alt={window.i18n('contactAvatarAlt', [name || 'avatar'])}
src={dataToDisplay}
data-testid={datatestId}
/>
@ -173,4 +171,4 @@ const AvatarInner = (props: Props) => {
);
};
export const Avatar = React.memo(AvatarInner, isEqual);
export const Avatar = AvatarInner;

@ -1,9 +1,13 @@
import React from 'react';
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import { MessageRenderingProps } from '../../../../models/messageType';
import { PubKey } from '../../../../session/types';
import { getMessageAuthorProps } from '../../../../state/selectors/conversations';
import {
useAuthorName,
useAuthorProfileName,
useFirstMessageOfSeries,
useMessageAuthor,
useMessageDirection,
} from '../../../../state/selectors';
import {
useSelectedIsGroup,
useSelectedIsPublic,
@ -11,11 +15,6 @@ import {
import { Flex } from '../../../basic/Flex';
import { ContactName } from '../../ContactName';
export type MessageAuthorSelectorProps = Pick<
MessageRenderingProps,
'authorName' | 'authorProfileName' | 'sender' | 'direction' | 'firstMessageOfSeries'
>;
type Props = {
messageId: string;
};
@ -25,15 +24,17 @@ const StyledAuthorContainer = styled(Flex)`
`;
export const MessageAuthorText = (props: Props) => {
const selected = useSelector(state => getMessageAuthorProps(state as any, props.messageId));
const isPublic = useSelectedIsPublic();
const isGroup = useSelectedIsGroup();
const authorProfileName = useAuthorProfileName(props.messageId);
const authorName = useAuthorName(props.messageId);
const sender = useMessageAuthor(props.messageId);
const direction = useMessageDirection(props.messageId);
const firstMessageOfSeries = useFirstMessageOfSeries(props.messageId);
if (!selected) {
if (!props.messageId || !sender || !direction) {
return null;
}
const { authorName, sender, authorProfileName, direction, firstMessageOfSeries } = selected;
const title = authorName ? authorName : sender;

@ -9,10 +9,18 @@ import { getSodiumRenderer } from '../../../../session/crypto';
import { PubKey } from '../../../../session/types';
import { openConversationWithMessages } from '../../../../state/ducks/conversations';
import { updateUserDetailsModal } from '../../../../state/ducks/modalDialog';
import { getMessageAvatarProps } from '../../../../state/selectors/conversations';
import {
useAuthorAvatarPath,
useAuthorName,
useAuthorProfileName,
useLastMessageOfSeries,
useMessageAuthor,
useMessageSenderIsAdmin,
} from '../../../../state/selectors/';
import {
getSelectedCanWrite,
useSelectedConversationKey,
useSelectedIsPublic,
} from '../../../../state/selectors/selectedConversation';
import { Avatar, AvatarSize, CrownIcon } from '../../../avatar/Avatar';
// tslint:disable: use-simple-attributes
@ -26,13 +34,7 @@ const StyledAvatar = styled.div`
export type MessageAvatarSelectorProps = Pick<
MessageRenderingProps,
| 'authorAvatarPath'
| 'authorName'
| 'sender'
| 'authorProfileName'
| 'isSenderAdmin'
| 'isPublic'
| 'lastMessageOfSeries'
'sender' | 'isSenderAdmin' | 'lastMessageOfSeries'
>;
type Props = { messageId: string; noAvatar: boolean };
@ -41,26 +43,18 @@ export const MessageAvatar = (props: Props) => {
const { messageId, noAvatar } = props;
const dispatch = useDispatch();
const avatarProps = useSelector(state => getMessageAvatarProps(state as any, messageId));
const selectedConvoKey = useSelectedConversationKey();
const isTypingEnabled = useSelector(getSelectedCanWrite);
if (!avatarProps) {
return null;
}
const {
authorAvatarPath,
authorName,
sender,
authorProfileName,
isSenderAdmin,
lastMessageOfSeries,
isPublic,
} = avatarProps;
if (noAvatar) {
const isPublic = useSelectedIsPublic();
const authorName = useAuthorName(messageId);
const authorProfileName = useAuthorProfileName(messageId);
const authorAvatarPath = useAuthorAvatarPath(messageId);
const sender = useMessageAuthor(messageId);
const lastMessageOfSeries = useLastMessageOfSeries(messageId);
const isSenderAdmin = useMessageSenderIsAdmin(messageId);
if (noAvatar || !sender) {
return null;
}

@ -10,8 +10,8 @@ import {
getMessageContentSelectorProps,
getQuotedMessageToAnimate,
getShouldHighlightMessage,
useMessageIsDeleted,
} from '../../../../state/selectors/conversations';
import { useMessageIsDeleted } from '../../../../state/selectors';
import { ScrollToLoadedMessageContext } from '../../SessionMessagesListContainer';
import { MessageAttachment } from './MessageAttachment';
import { MessageLinkPreview } from './MessageLinkPreview';

@ -4,12 +4,14 @@ import { isImageAttachment } from '../../../../types/Attachment';
import { Image } from '../../Image';
import { MessageRenderingProps } from '../../../../models/messageType';
import { useDispatch, useSelector } from 'react-redux';
import {
getIsMessageSelectionMode,
getMessageLinkPreviewProps,
} from '../../../../state/selectors/conversations';
import { getIsMessageSelectionMode } from '../../../../state/selectors/conversations';
import { SessionIcon } from '../../../icon';
import { showLinkVisitWarningDialog } from '../../../dialog/SessionConfirm';
import {
useMessageAttachments,
useMessageDirection,
useMessageLinkPreview,
} from '../../../../state/selectors';
export type MessageLinkPreviewSelectorProps = Pick<
MessageRenderingProps,
@ -24,14 +26,15 @@ type Props = {
const linkPreviewsImageSize = 100;
export const MessageLinkPreview = (props: Props) => {
const selected = useSelector(state => getMessageLinkPreviewProps(state as any, props.messageId));
const dispatch = useDispatch();
const direction = useMessageDirection(props.messageId);
const attachments = useMessageAttachments(props.messageId);
const previews = useMessageLinkPreview(props.messageId);
const isMessageSelectionMode = useSelector(getIsMessageSelectionMode);
if (!selected) {
if (!props.messageId) {
return null;
}
const { direction, attachments, previews } = selected;
// Attachments take precedence over Link Previews
if (attachments && attachments.length) {

@ -5,7 +5,6 @@ import { MessageRenderingProps } from '../../../../models/messageType';
import { PubKey } from '../../../../session/types';
import { openConversationToSpecificMessage } from '../../../../state/ducks/conversations';
import {
getMessageQuoteProps,
isMessageDetailView,
isMessageSelectionMode,
} from '../../../../state/selectors/conversations';
@ -13,6 +12,7 @@ import { Quote } from './Quote';
import { ToastUtils } from '../../../../session/utils';
import { Data } from '../../../../data/data';
import { MessageModel } from '../../../../models/message';
import { useMessageDirection, useMessageQuote } from '../../../../state/selectors';
// tslint:disable: use-simple-attributes
@ -23,13 +23,11 @@ type Props = {
export type MessageQuoteSelectorProps = Pick<MessageRenderingProps, 'quote' | 'direction'>;
export const MessageQuote = (props: Props) => {
const selected = useSelector(state => getMessageQuoteProps(state as any, props.messageId));
const quote = useMessageQuote(props.messageId);
const direction = useMessageDirection(props.messageId);
const multiSelectMode = useSelector(isMessageSelectionMode);
const isMessageDetailViewMode = useSelector(isMessageDetailView);
const quote = selected ? selected.quote : undefined;
const direction = selected ? selected.direction : undefined;
const onQuoteClick = useCallback(
async (event: React.MouseEvent<HTMLDivElement>) => {
event.preventDefault();
@ -76,7 +74,7 @@ export const MessageQuote = (props: Props) => {
},
[quote, multiSelectMode, props.messageId]
);
if (!selected) {
if (!props.messageId) {
return null;
}

@ -1,8 +1,7 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { MessageRenderingProps } from '../../../../models/messageType';
import { getMessageStatusProps } from '../../../../state/selectors/conversations';
import { OutgoingMessageStatus } from './OutgoingMessageStatus';
import { useMessageDirection, useMessageStatus } from '../../../../state/selectors';
type Props = {
isCorrectSide: boolean;
@ -14,12 +13,12 @@ export type MessageStatusSelectorProps = Pick<MessageRenderingProps, 'direction'
export const MessageStatus = (props: Props) => {
const { isCorrectSide, dataTestId } = props;
const direction = useMessageDirection(props.messageId);
const status = useMessageStatus(props.messageId);
const selected = useSelector(state => getMessageStatusProps(state as any, props.messageId));
if (!selected) {
if (!props.messageId) {
return null;
}
const { status, direction } = selected;
if (!isCorrectSide) {
return null;

@ -10,14 +10,12 @@ import {
closeMessageDetailsView,
ContactPropsMessageDetail,
} from '../../../../state/ducks/conversations';
import {
getMessageDetailsViewProps,
getMessageIsDeletable,
} from '../../../../state/selectors/conversations';
import { getMessageDetailsViewProps } from '../../../../state/selectors/conversations';
import { ContactName } from '../../ContactName';
// tslint:disable-next-line: no-submodule-imports
import useKey from 'react-use/lib/useKey';
import { SessionButton, SessionButtonColor, SessionButtonType } from '../../../basic/SessionButton';
import { useMessageIsDeletable } from '../../../../state/selectors';
const AvatarItem = (props: { pubkey: string }) => {
const { pubkey } = props;
@ -98,9 +96,7 @@ export const MessageDetail = () => {
const { i18n } = window;
const messageDetailProps = useSelector(getMessageDetailsViewProps);
const isDeletable = useSelector(state =>
getMessageIsDeletable(state as any, messageDetailProps?.messageId || '')
);
const isDeletable = useMessageIsDeletable(messageDetailProps?.messageId);
const dispatch = useDispatch();

@ -122,7 +122,7 @@ const animation = (props: {
};
//tslint:disable no-unnecessary-callback-wrapper
const Svg = React.memo(styled.svg<StyledSvgProps>`
const Svg = styled.svg<StyledSvgProps>`
width: ${props => props.width};
transform: ${props => `rotate(${props.iconRotation}deg)`};
animation: ${props => animation(props)};
@ -134,7 +134,7 @@ const Svg = React.memo(styled.svg<StyledSvgProps>`
fill: ${props => (props.iconColor ? props.iconColor : '--button-icon-stroke-color')};
padding: ${props => (props.iconPadding ? props.iconPadding : '')};
transition: inherit;
`);
`;
// tslint:enable no-unnecessary-callback-wrapper
const SessionSvg = (props: {

@ -140,4 +140,4 @@ const ConversationListItem = (props: Props) => {
);
};
export const MemoConversationListItemWithDetails = React.memo(ConversationListItem, _.isEqual);
export const MemoConversationListItemWithDetails = ConversationListItem;

@ -77,13 +77,7 @@ const ConversationListItemContextMenu = (props: PropsContextConversationItem) =>
);
};
function propsAreEqual(prev: PropsContextConversationItem, next: PropsContextConversationItem) {
return _.isEqual(prev, next);
}
export const MemoConversationListItemContextMenu = React.memo(
ConversationListItemContextMenu,
propsAreEqual
);
export const MemoConversationListItemContextMenu = ConversationListItemContextMenu;
export const PinConversationMenuItem = (): JSX.Element | null => {
const conversationId = useConvoIdFromContext();

@ -12,6 +12,7 @@ import {
sortBy,
throttle,
uniq,
xor,
} from 'lodash';
import { SignalService } from '../protobuf';
import { getMessageQueue } from '../session';
@ -41,7 +42,6 @@ import { toHex } from '../session/utils/String';
import { createTaskWithTimeout } from '../session/utils/TaskWithTimeout';
import {
actions as conversationActions,
conversationChanged,
conversationsChanged,
markConversationFullyRead,
MessageModelPropsWithoutConvoProps,
@ -163,9 +163,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
this.typingRefreshTimer = null;
this.typingPauseTimer = null;
window.inboxStore?.dispatch(
conversationChanged({ id: this.id, data: this.getConversationModelProps() })
);
window.inboxStore?.dispatch(conversationsChanged([this.getConversationModelProps()]));
}
public idForLogging() {
@ -394,13 +392,13 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
* @returns true if the groupAdmins where not the same (and thus updated)
*/
public async updateGroupAdmins(groupAdmins: Array<string>, shouldCommit: boolean) {
const sortedExistingAdmins = uniq(sortBy(this.getGroupAdmins()));
const sortedNewAdmins = uniq(sortBy(groupAdmins));
if (isEqual(sortedExistingAdmins, sortedNewAdmins)) {
// check if there is any difference betwewen the two, if yes, override it with what we got.
if (!xor(this.getGroupAdmins(), groupAdmins).length) {
return false;
}
this.set({ groupAdmins });
this.set({ groupAdmins: sortedNewAdmins });
if (shouldCommit) {
await this.commit();
}

@ -77,6 +77,7 @@ import {
PropsForGroupUpdateLeft,
PropsForGroupUpdateName,
PropsForMessageWithoutConvoProps,
ReduxQuoteType,
} from '../state/ducks/conversations';
import { AttachmentTypeWithPath, isVoiceMessage } from '../types/Attachment';
import { getAttachmentMetadata } from '../types/message/initializeAttachmentMetadata';
@ -615,15 +616,7 @@ export class MessageModel extends Backbone.Model<MessageAttributes> {
}
const firstAttachment = quote.attachments && quote.attachments[0];
const quoteProps: {
referencedMessageNotFound?: boolean;
sender: string;
messageId: string;
authorName: string;
text?: string;
attachment?: any;
isFromMe?: boolean;
} = {
const quoteProps: ReduxQuoteType = {
sender: author,
messageId: id,
authorName: authorName || 'Unknown',

@ -432,10 +432,7 @@ export class ConversationController {
this.conversations.remove(conversation);
window?.inboxStore?.dispatch(
conversationActions.conversationChanged({
id: convoId,
data: conversation.getConversationModelProps(),
})
conversationActions.conversationsChanged([conversation.getConversationModelProps()])
);
}
window.inboxStore?.dispatch(conversationActions.conversationRemoved(convoId));

@ -161,6 +161,17 @@ export type PropsForAttachment = {
} | null;
};
export type ReduxQuoteType = {
text?: string;
attachment?: QuotedAttachmentType;
isFromMe?: boolean;
sender: string;
authorProfileName?: string;
authorName?: string;
messageId?: string;
referencedMessageNotFound?: boolean;
} | null;
export type PropsForMessageWithoutConvoProps = {
id: string; // messageId
direction: MessageModelType;
@ -177,16 +188,7 @@ export type PropsForMessageWithoutConvoProps = {
reacts?: ReactionList;
reactsIndex?: number;
previews?: Array<any>;
quote?: {
text?: string;
attachment?: QuotedAttachmentType;
isFromMe?: boolean;
sender: string;
authorProfileName?: string;
authorName?: string;
messageId?: string;
referencedMessageNotFound?: boolean;
} | null;
quote?: ReduxQuoteType;
messageHash?: string;
isDeleted?: boolean;
isUnread?: boolean;
@ -197,12 +199,8 @@ export type PropsForMessageWithoutConvoProps = {
};
export type PropsForMessageWithConvoProps = PropsForMessageWithoutConvoProps & {
authorName: string | null;
authorProfileName: string | null;
conversationType: ConversationTypeEnum;
authorAvatarPath: string | null;
isPublic: boolean;
isOpenGroupV2: boolean;
isKickedFromGroup: boolean;
weAreAdmin: boolean;
isSenderAdmin: boolean;
@ -634,16 +632,7 @@ const conversationsSlice = createSlice({
},
};
},
conversationChanged(
state: ConversationsStateType,
action: PayloadAction<{
id: string;
data: ReduxConversationType;
}>
) {
const { payload } = action;
return applyConversationChanged(state, payload);
},
conversationsChanged(
state: ConversationsStateType,
action: PayloadAction<Array<ReduxConversationType>>
@ -652,12 +641,7 @@ const conversationsSlice = createSlice({
let updatedState = state;
if (payload.length) {
payload.forEach(convoProps => {
updatedState = applyConversationChanged(updatedState, {
id: convoProps.id,
data: convoProps,
});
});
updatedState = applyConversationsChanged(updatedState, payload);
}
return updatedState;
@ -972,47 +956,47 @@ const conversationsSlice = createSlice({
},
});
function applyConversationChanged(
function applyConversationsChanged(
state: ConversationsStateType,
payload: { id: string; data: ReduxConversationType }
payload: Array<ReduxConversationType>
) {
const { id, data } = payload;
const { conversationLookup, selectedConversation } = state;
const existing = conversationLookup[id];
// In the change case we only modify the lookup if we already had that conversation
if (!existing) {
return state;
}
for (let index = 0; index < payload.length; index++) {
const convoProps = payload[index];
const { id } = convoProps;
// In the `change` case we only modify the lookup if we already had that conversation
const existing = conversationLookup[id];
if (!existing) {
continue;
}
if (
state.selectedConversation &&
convoProps.isPrivate &&
convoProps.id === selectedConversation &&
convoProps.priority &&
convoProps.priority < CONVERSATION_PRIORITIES.default
) {
// A private conversation hidden cannot be a selected.
// When opening a hidden conversation, we unhide it so it can be selected again.
state.selectedConversation = undefined;
}
let selected = selectedConversation;
if (
data &&
data.isPrivate &&
data.id === selectedConversation &&
data.priority &&
data.priority < CONVERSATION_PRIORITIES.default
) {
// A private conversation hidden cannot be a selected.
// When opening a hidden conversation, we unhide it so it can be selected again.
selected = undefined;
state.conversationLookup[id] = {
...convoProps,
isInitialFetchingInProgress: existing.isInitialFetchingInProgress,
};
}
return {
...state,
selectedConversation: selected,
conversationLookup: {
...conversationLookup,
[id]: { ...data, isInitialFetchingInProgress: existing.isInitialFetchingInProgress },
},
};
return state;
}
export const { actions, reducer } = conversationsSlice;
export const {
// conversation and messages list
conversationAdded,
conversationChanged,
conversationsChanged,
conversationRemoved,
removeAllConversations,

@ -1,9 +1,8 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { sortBy, uniq } from 'lodash';
import { sortBy, uniq, xor } from 'lodash';
import {
getCanWriteOutsideRedux,
getCurrentSubscriberCountOutsideRedux,
getModeratorsOutsideRedux,
} from '../selectors/sogsRoomInfo';
type RoomInfo = {
@ -53,8 +52,16 @@ const sogsRoomInfosSlice = createSlice({
},
setModerators(state, action: PayloadAction<{ convoId: string; moderators: Array<string> }>) {
addEmptyEntryIfNeeded(state, action.payload.convoId);
const existing = state.rooms[action.payload.convoId].moderators;
const newMods = sortBy(uniq(action.payload.moderators));
state.rooms[action.payload.convoId].moderators = sortBy(uniq(action.payload.moderators));
// check if there is any changes (order excluded) between those two arrays
const xord = xor(existing, newMods);
if (!xord.length) {
return state;
}
state.rooms[action.payload.convoId].moderators = newMods;
return state;
},
@ -93,10 +100,6 @@ function setCanWriteOutsideRedux(convoId: string, canWrite: boolean) {
* @param moderators the updated list of moderators
*/
function setModeratorsOutsideRedux(convoId: string, moderators: Array<string>) {
const currentMods = getModeratorsOutsideRedux(convoId);
if (sortBy(uniq(currentMods)) === sortBy(uniq(moderators))) {
return;
}
window.inboxStore?.dispatch(
setModerators({
convoId,

@ -14,14 +14,9 @@ import { StateType } from '../reducer';
import { ReplyingToMessageProps } from '../../components/conversation/composition/CompositionBox';
import { MessageAttachmentSelectorProps } from '../../components/conversation/message/message-content/MessageAttachment';
import { MessageAuthorSelectorProps } from '../../components/conversation/message/message-content/MessageAuthorText';
import { MessageAvatarSelectorProps } from '../../components/conversation/message/message-content/MessageAvatar';
import { MessageContentSelectorProps } from '../../components/conversation/message/message-content/MessageContent';
import { MessageContentWithStatusSelectorProps } from '../../components/conversation/message/message-content/MessageContentWithStatus';
import { MessageContextMenuSelectorProps } from '../../components/conversation/message/message-content/MessageContextMenu';
import { MessageLinkPreviewSelectorProps } from '../../components/conversation/message/message-content/MessageLinkPreview';
import { MessageQuoteSelectorProps } from '../../components/conversation/message/message-content/MessageQuote';
import { MessageStatusSelectorProps } from '../../components/conversation/message/message-content/MessageStatus';
import { MessageTextSelectorProps } from '../../components/conversation/message/message-content/MessageText';
import { GenericReadableMessageSelectorProps } from '../../components/conversation/message/message-item/GenericReadableMessage';
import { LightBoxOptions } from '../../components/conversation/SessionConversation';
@ -40,21 +35,17 @@ import { getIntl } from './user';
import { filter, isEmpty, isNumber, pick, sortBy } from 'lodash';
import { MessageReactsSelectorProps } from '../../components/conversation/message/message-content/MessageReactions';
import { getModeratorsOutsideRedux } from './sogsRoomInfo';
import { getSelectedConversation, getSelectedConversationKey } from './selectedConversation';
import { useSelector } from 'react-redux';
import { getModeratorsOutsideRedux } from './sogsRoomInfo';
export const getConversations = (state: StateType): ConversationsStateType => state.conversations;
export const getConversationLookup = createSelector(
getConversations,
(state: ConversationsStateType): ConversationLookupType => {
return state.conversationLookup;
}
);
export const getConversationLookup = (state: StateType): ConversationLookupType => {
return state.conversations.conversationLookup;
};
export const getConversationsCount = createSelector(getConversationLookup, (state): number => {
return Object.values(state).length;
return Object.keys(state).length;
});
export const getOurPrimaryConversation = createSelector(
@ -63,10 +54,9 @@ export const getOurPrimaryConversation = createSelector(
state.conversationLookup[Storage.get('primaryDevicePubKey') as string]
);
const getMessagesOfSelectedConversation = createSelector(
getConversations,
(state: ConversationsStateType): Array<MessageModelPropsWithoutConvoProps> => state.messages
);
const getMessagesOfSelectedConversation = (
state: StateType
): Array<MessageModelPropsWithoutConvoProps> => state.conversations.messages;
// Redux recommends to do filtered and deriving state in a selector rather than ourself
export const getSortedMessagesOfSelectedConversation = createSelector(
@ -97,12 +87,9 @@ export const hasSelectedConversationIncomingMessages = createSelector(
}
);
export const getFirstUnreadMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => {
return state.firstUnreadMessageId;
}
);
export const getFirstUnreadMessageId = (state: StateType): string | undefined => {
return state.conversations.firstUnreadMessageId;
};
export type MessagePropsType =
| 'group-notification'
@ -489,76 +476,47 @@ export const getGlobalUnreadMessageCount = createSelector(getLeftPaneLists, (sta
return state.globalUnreadCount;
});
export const isMessageDetailView = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.messageDetailProps !== undefined
);
export const isMessageDetailView = (state: StateType): boolean =>
state.conversations.messageDetailProps !== undefined;
export const getMessageDetailsViewProps = createSelector(
getConversations,
(state: ConversationsStateType): MessagePropsDetails | undefined => state.messageDetailProps
);
export const getMessageDetailsViewProps = (state: StateType): MessagePropsDetails | undefined =>
state.conversations.messageDetailProps;
export const isRightPanelShowing = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.showRightPanel
);
export const isRightPanelShowing = (state: StateType): boolean =>
state.conversations.showRightPanel;
export const isMessageSelectionMode = createSelector(
getConversations,
(state: ConversationsStateType): boolean => Boolean(state.selectedMessageIds.length > 0)
);
export const isMessageSelectionMode = (state: StateType): boolean =>
state.conversations.selectedMessageIds.length > 0;
export const getSelectedMessageIds = createSelector(
getConversations,
(state: ConversationsStateType): Array<string> => state.selectedMessageIds
);
export const getSelectedMessageIds = (state: StateType): Array<string> =>
state.conversations.selectedMessageIds;
export const getIsMessageSelectionMode = createSelector(
getSelectedMessageIds,
(state: Array<string>): boolean => Boolean(state.length)
);
export const getIsMessageSelectionMode = (state: StateType): boolean =>
Boolean(getSelectedMessageIds(state).length);
export const getLightBoxOptions = createSelector(
getConversations,
(state: ConversationsStateType): LightBoxOptions | undefined => state.lightBox
);
export const getLightBoxOptions = (state: StateType): LightBoxOptions | undefined =>
state.conversations.lightBox;
export const getQuotedMessage = createSelector(
getConversations,
(state: ConversationsStateType): ReplyingToMessageProps | undefined => state.quotedMessage
);
export const getQuotedMessage = (state: StateType): ReplyingToMessageProps | undefined =>
state.conversations.quotedMessage;
export const areMoreMessagesBeingFetched = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.areMoreMessagesBeingFetched || false
);
export const areMoreMessagesBeingFetched = (state: StateType): boolean =>
state.conversations.areMoreMessagesBeingFetched || false;
export const getShowScrollButton = createSelector(
getConversations,
(state: ConversationsStateType): boolean => state.showScrollButton || false
);
export const getShowScrollButton = (state: StateType): boolean =>
state.conversations.showScrollButton || false;
export const getQuotedMessageToAnimate = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => state.animateQuotedMessageId || undefined
);
export const getQuotedMessageToAnimate = (state: StateType): string | undefined =>
state.conversations.animateQuotedMessageId || undefined;
export const getShouldHighlightMessage = createSelector(
getConversations,
(state: ConversationsStateType): boolean =>
Boolean(state.animateQuotedMessageId && state.shouldHighlightMessage)
);
export const getShouldHighlightMessage = (state: StateType): boolean =>
Boolean(state.conversations.animateQuotedMessageId && state.conversations.shouldHighlightMessage);
export const getNextMessageToPlayId = createSelector(
getConversations,
(state: ConversationsStateType): string | undefined => state.nextMessageToPlayId || undefined
);
export const getNextMessageToPlayId = (state: StateType): string | undefined =>
state.conversations.nextMessageToPlayId || undefined;
export const getMentionsInput = createSelector(
getConversations,
(state: ConversationsStateType): MentionsMembersType => state.mentionMembers
);
export const getMentionsInput = (state: StateType): MentionsMembersType =>
state.conversations.mentionMembers;
/// Those calls are just related to ordering messages in the redux store.
@ -621,12 +579,9 @@ function sortMessages(
* This returns the most recent message id in the database. This is not the most recent message shown,
* but the most recent one, which could still not be loaded.
*/
export const getMostRecentMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | null => {
return state.mostRecentMessageId;
}
);
export const getMostRecentMessageId = (state: StateType): string | null => {
return state.conversations.mostRecentMessageId;
};
export const getOldestMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
@ -674,20 +629,22 @@ export const isFirstUnreadMessageIdAbove = createSelector(
}
);
const getMessageId = (_whatever: any, id: string) => id;
const getMessageId = (_whatever: any, id: string | undefined) => id;
// tslint:disable: cyclomatic-complexity
export const getMessagePropsByMessageId = createSelector(
getSortedMessagesOfSelectedConversation,
getConversationLookup,
getSelectedConversation,
getMessageId,
(
messages: Array<SortedMessageModelProps>,
conversations,
selectedConvo,
id
): MessageModelPropsWithConvoProps | undefined => {
if (!id) {
return undefined;
}
const foundMessageProps: SortedMessageModelProps | undefined = messages?.find(
m => m?.propsForMessage?.id === id
);
@ -697,27 +654,20 @@ export const getMessagePropsByMessageId = createSelector(
}
const sender = foundMessageProps?.propsForMessage?.sender;
// foundMessageConversation is the conversation this message is
const foundMessageConversation = conversations[foundMessageProps.propsForMessage.convoId];
if (!foundMessageConversation || !sender) {
return undefined;
}
const foundSenderConversation = conversations[sender];
if (!foundSenderConversation) {
// we can only show messages when the convo is selected.
if (!selectedConvo || !sender) {
return undefined;
}
const ourPubkey = UserUtils.getOurPubKeyStrFromCache();
const isGroup = !foundMessageConversation.isPrivate;
const isPublic = foundMessageConversation.isPublic;
const isGroup = !selectedConvo.isPrivate;
const isPublic = selectedConvo.isPublic;
const groupAdmins = (isGroup && foundMessageConversation.groupAdmins) || [];
const groupAdmins = (isGroup && selectedConvo.groupAdmins) || [];
const weAreAdmin = groupAdmins.includes(ourPubkey) || false;
const weAreModerator =
(isPublic && getModeratorsOutsideRedux(foundMessageConversation.id).includes(ourPubkey)) ||
false;
(isPublic && getModeratorsOutsideRedux(selectedConvo.id).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)
@ -732,33 +682,20 @@ export const getMessagePropsByMessageId = createSelector(
sender === ourPubkey || (isPublic && (weAreAdmin || weAreModerator)) || false;
const isSenderAdmin = groupAdmins.includes(sender);
const senderIsUs = sender === ourPubkey;
const authorName =
foundSenderConversation.nickname || foundSenderConversation.displayNameInProfile || null;
const authorProfileName = senderIsUs
? window.i18n('you')
: foundSenderConversation.nickname ||
foundSenderConversation.displayNameInProfile ||
window.i18n('anonymous');
const messageProps: MessageModelPropsWithConvoProps = {
...foundMessageProps,
propsForMessage: {
...foundMessageProps.propsForMessage,
isBlocked: !!foundMessageConversation.isBlocked,
isBlocked: !!selectedConvo.isBlocked,
isPublic: !!isPublic,
isOpenGroupV2: !!isPublic,
isSenderAdmin,
isDeletable,
isDeletableForEveryone,
weAreAdmin,
conversationType: foundMessageConversation.type,
conversationType: selectedConvo.type,
sender,
authorAvatarPath: foundSenderConversation.avatarPath || null,
isKickedFromGroup: foundMessageConversation.isKickedFromGroup || false,
authorProfileName: authorProfileName || 'Unknown',
authorName,
isKickedFromGroup: selectedConvo.isKickedFromGroup || false,
},
};
@ -766,30 +703,6 @@ export const getMessagePropsByMessageId = createSelector(
}
);
export const getMessageAvatarProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAvatarSelectorProps
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const messageAvatarProps: MessageAvatarSelectorProps = {
lastMessageOfSeries: props.lastMessageOfSeries,
...pick(props.propsForMessage, [
'authorAvatarPath',
'authorName',
'sender',
'authorProfileName',
'conversationType',
'direction',
'isPublic',
'isSenderAdmin',
]),
};
return messageAvatarProps;
});
export const getMessageReactsProps = createSelector(getMessagePropsByMessageId, (props):
| MessageReactsSelectorProps
| undefined => {
@ -800,7 +713,6 @@ export const getMessageReactsProps = createSelector(getMessagePropsByMessageId,
const msgProps: MessageReactsSelectorProps = pick(props.propsForMessage, [
'convoId',
'conversationType',
'isPublic',
'reacts',
'serverId',
]);
@ -825,46 +737,6 @@ export const getMessageReactsProps = createSelector(getMessagePropsByMessageId,
return msgProps;
});
export const getMessageLinkPreviewProps = createSelector(getMessagePropsByMessageId, (props):
| MessageLinkPreviewSelectorProps
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const msgProps: MessageLinkPreviewSelectorProps = pick(props.propsForMessage, [
'direction',
'attachments',
'previews',
]);
return msgProps;
});
export const getMessageQuoteProps = createSelector(getMessagePropsByMessageId, (props):
| MessageQuoteSelectorProps
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const msgProps: MessageQuoteSelectorProps = pick(props.propsForMessage, ['direction', 'quote']);
return msgProps;
});
export const getMessageStatusProps = createSelector(getMessagePropsByMessageId, (props):
| MessageStatusSelectorProps
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const msgProps: MessageStatusSelectorProps = pick(props.propsForMessage, ['direction', 'status']);
return msgProps;
});
export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (props):
| MessageTextSelectorProps
| undefined => {
@ -883,11 +755,6 @@ export const getMessageTextProps = createSelector(getMessagePropsByMessageId, (p
return msgProps;
});
export const useMessageIsDeleted = (messageId: string): boolean => {
const props = useSelector((state: StateType) => getMessagePropsByMessageId(state, messageId));
return props?.propsForMessage.isDeleted || false;
};
/**
* TODO probably not something which should be memoized with createSelector as we rememoize it for each message (and override the previous one). Not sure what is the right way to do a lookup. But maybe something like having the messages as a record<id, message> and do a simple lookup to grab the details.
* And the the sorting would be done in a memoized selector
@ -915,32 +782,6 @@ export const getMessageContextMenuProps = createSelector(getMessagePropsByMessag
return msgProps;
});
export const getMessageAuthorProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAuthorSelectorProps
| undefined => {
if (!props || isEmpty(props)) {
return undefined;
}
const msgProps: MessageAuthorSelectorProps = {
firstMessageOfSeries: props.firstMessageOfSeries,
...pick(props.propsForMessage, ['authorName', 'sender', 'authorProfileName', 'direction']),
};
return msgProps;
});
export const getMessageIsDeletable = createSelector(
getMessagePropsByMessageId,
(props): boolean => {
if (!props || isEmpty(props)) {
return false;
}
return props.propsForMessage.isDeletable;
}
);
export const getMessageAttachmentProps = createSelector(getMessagePropsByMessageId, (props):
| MessageAttachmentSelectorProps
| undefined => {
@ -1038,22 +879,14 @@ export const getGenericReadableMessageSelectorProps = createSelector(
}
);
export const getOldTopMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | null => state.oldTopMessageId || null
);
export const getOldTopMessageId = (state: StateType): string | null =>
state.conversations.oldTopMessageId || null;
// TODOLATER get rid of all the unneeded createSelector calls
export const getOldBottomMessageId = (state: StateType): string | null =>
state.conversations.oldBottomMessageId || null;
export const getOldBottomMessageId = createSelector(
getConversations,
(state: ConversationsStateType): string | null => state.oldBottomMessageId || null
);
export const getIsSelectedConvoInitialLoadingInProgress = createSelector(
getSelectedConversation,
(convo: ReduxConversationType | undefined): boolean => Boolean(convo?.isInitialFetchingInProgress)
);
export const getIsSelectedConvoInitialLoadingInProgress = (state: StateType): boolean =>
Boolean(getSelectedConversation(state)?.isInitialFetchingInProgress);
export function getCurrentlySelectedConversationOutsideRedux() {
return window?.inboxStore?.getState().conversations.selectedConversation as string | undefined;

@ -25,3 +25,5 @@ export {
UserConfigSelectors,
UserSelectors,
};
export * from './messages';

@ -0,0 +1,112 @@
import { useSelector } from 'react-redux';
import { UserUtils } from '../../session/utils';
import {
MessageModelPropsWithConvoProps,
ReduxConversationType,
PropsForAttachment,
ReduxQuoteType,
LastMessageStatusType,
} from '../ducks/conversations';
import { StateType } from '../reducer';
import { getMessagePropsByMessageId } from './conversations';
const useMessageIdProps = (messageId: string | undefined) => {
return useSelector((state: StateType) => getMessagePropsByMessageId(state, messageId));
};
const useSenderConvoProps = (
msgProps: MessageModelPropsWithConvoProps | undefined
): ReduxConversationType | undefined => {
return useSelector((state: StateType) => {
const sender = msgProps?.propsForMessage.sender;
if (!sender) {
return undefined;
}
return state.conversations.conversationLookup[sender] || undefined;
});
};
export const useAuthorProfileName = (messageId: string): string | null => {
const msg = useMessageIdProps(messageId);
const senderProps = useSenderConvoProps(msg);
if (!msg || !senderProps) {
return null;
}
const senderIsUs = msg.propsForMessage.sender === UserUtils.getOurPubKeyStrFromCache();
const authorProfileName = senderIsUs
? window.i18n('you')
: senderProps.nickname || senderProps.displayNameInProfile || window.i18n('anonymous');
return authorProfileName || window.i18n('unknown');
};
export const useAuthorName = (messageId: string): string | null => {
const msg = useMessageIdProps(messageId);
const senderProps = useSenderConvoProps(msg);
if (!msg || !senderProps) {
return null;
}
const authorName = senderProps.nickname || senderProps.displayNameInProfile || null;
return authorName;
};
export const useAuthorAvatarPath = (messageId: string): string | null => {
const msg = useMessageIdProps(messageId);
const senderProps = useSenderConvoProps(msg);
if (!msg || !senderProps) {
return null;
}
return senderProps.avatarPath || null;
};
export const useMessageIsDeleted = (messageId: string): boolean => {
const props = useMessageIdProps(messageId);
return props?.propsForMessage.isDeleted || false;
};
export const useFirstMessageOfSeries = (messageId: string | undefined): boolean => {
return useMessageIdProps(messageId)?.firstMessageOfSeries || false;
};
export const useLastMessageOfSeries = (messageId: string | undefined): boolean => {
return useMessageIdProps(messageId)?.lastMessageOfSeries || false;
};
export const useMessageAuthor = (messageId: string | undefined): string | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.sender;
};
export const useMessageDirection = (messageId: string | undefined): string | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.direction;
};
export const useMessageLinkPreview = (messageId: string | undefined): any[] | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.previews;
};
export const useMessageAttachments = (
messageId: string | undefined
): Array<PropsForAttachment> | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.attachments;
};
export const useMessageSenderIsAdmin = (messageId: string | undefined): boolean => {
return useMessageIdProps(messageId)?.propsForMessage.isSenderAdmin || false;
};
export const useMessageIsDeletable = (messageId: string | undefined): boolean => {
return useMessageIdProps(messageId)?.propsForMessage.isDeletable || false;
};
export const useMessageQuote = (messageId: string | undefined): ReduxQuoteType | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.quote;
};
export const useMessageStatus = (
messageId: string | undefined
): LastMessageStatusType | undefined => {
return useMessageIdProps(messageId)?.propsForMessage.status;
};

@ -10,13 +10,11 @@ import { getSelectedConversationKey } from './selectedConversation';
export const getSearch = (state: StateType): SearchStateType => state.search;
export const getQuery = createSelector(getSearch, (state: SearchStateType): string => state.query);
export const getQuery = (state: StateType): string => getSearch(state).query;
export const isSearching = createSelector(getSearch, (state: SearchStateType) => {
const { query } = state;
return Boolean(query && query.trim().length > 1);
});
export const isSearching = (state: StateType) => {
return !!getSearch(state)?.query?.trim();
};
export const getSearchResults = createSelector(
[getSearch, getConversationLookup, getSelectedConversationKey],

Loading…
Cancel
Save