Don't mutate state in TimelineItem

This commit is contained in:
Fedor Indutny 2021-08-19 13:14:41 -07:00 committed by GitHub
parent 1cc7c5dc2d
commit 80c1ad6ee3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 61 deletions

View file

@ -99,6 +99,7 @@ import {
getActiveCall, getActiveCall,
} from '../state/selectors/calling'; } from '../state/selectors/calling';
import { getAccountSelector } from '../state/selectors/accounts'; import { getAccountSelector } from '../state/selectors/accounts';
import { getContactNameColorSelector } from '../state/selectors/conversations';
import { import {
MessageReceipts, MessageReceipts,
MessageReceiptType, MessageReceiptType,
@ -378,6 +379,14 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const accountSelector = getAccountSelector(state); const accountSelector = getAccountSelector(state);
return accountSelector(identifier); return accountSelector(identifier);
}, },
contactNameColorSelector: (
conversationId: string,
contactId: string
) => {
const state = window.reduxStore.getState();
const contactNameColorSelector = getContactNameColorSelector(state);
return contactNameColorSelector(conversationId, contactId);
},
}), }),
errors, errors,
contacts, contacts,

View file

@ -680,6 +680,71 @@ export const getCachedSelectorForMessage = createSelector(
} }
); );
const getCachedConversationMemberColorsSelector = createSelector(
getConversationSelector,
getUserConversationId,
(
conversationSelector: GetConversationByIdType,
ourConversationId: string
) => {
return memoizee(
(conversationId: string) => {
const contactNameColors: Map<string, ContactNameColorType> = new Map();
const {
sortedGroupMembers = [],
type,
id: theirId,
} = conversationSelector(conversationId);
if (type === 'direct') {
contactNameColors.set(ourConversationId, ContactNameColors[0]);
contactNameColors.set(theirId, ContactNameColors[0]);
return contactNameColors;
}
[...sortedGroupMembers]
.sort((left, right) =>
String(left.uuid) > String(right.uuid) ? 1 : -1
)
.forEach((member, i) => {
contactNameColors.set(
member.id,
ContactNameColors[i % ContactNameColors.length]
);
});
return contactNameColors;
},
{ max: 100 }
);
}
);
export type ContactNameColorSelectorType = (
conversationId: string,
contactId: string
) => ContactNameColorType;
export const getContactNameColorSelector = createSelector(
getCachedConversationMemberColorsSelector,
conversationMemberColorsSelector => {
return (
conversationId: string,
contactId: string
): ContactNameColorType => {
const contactNameColors = conversationMemberColorsSelector(
conversationId
);
const color = contactNameColors.get(contactId);
if (!color) {
window.log.warn(`No color generated for contact ${contactId}`);
return ContactNameColors[0];
}
return color;
};
}
);
type GetMessageByIdType = (id: string) => TimelineItemType | undefined; type GetMessageByIdType = (id: string) => TimelineItemType | undefined;
export const getMessageSelector = createSelector( export const getMessageSelector = createSelector(
getCachedSelectorForMessage, getCachedSelectorForMessage,
@ -693,6 +758,7 @@ export const getMessageSelector = createSelector(
getCallSelector, getCallSelector,
getActiveCall, getActiveCall,
getAccountSelector, getAccountSelector,
getContactNameColorSelector,
( (
messageSelector: typeof getPropsForBubble, messageSelector: typeof getPropsForBubble,
messageLookup: MessageLookupType, messageLookup: MessageLookupType,
@ -704,7 +770,8 @@ export const getMessageSelector = createSelector(
ourConversationId: string, ourConversationId: string,
callSelector: CallSelectorType, callSelector: CallSelectorType,
activeCall: undefined | CallStateType, activeCall: undefined | CallStateType,
accountSelector: AccountSelectorType accountSelector: AccountSelectorType,
contactNameColorSelector: ContactNameColorSelectorType
): GetMessageByIdType => { ): GetMessageByIdType => {
return (id: string) => { return (id: string) => {
const message = messageLookup[id]; const message = messageLookup[id];
@ -720,6 +787,7 @@ export const getMessageSelector = createSelector(
regionCode, regionCode,
selectedMessageId: selectedMessage?.id, selectedMessageId: selectedMessage?.id,
selectedMessageCounter: selectedMessage?.counter, selectedMessageCounter: selectedMessage?.counter,
contactNameColorSelector,
callSelector, callSelector,
activeCall, activeCall,
accountSelector, accountSelector,
@ -838,54 +906,6 @@ export const getInvitedContactsForNewlyCreatedGroup = createSelector(
) )
); );
const getCachedConversationMemberColorsSelector = createSelector(
getConversationSelector,
(conversationSelector: GetConversationByIdType) => {
return memoizee(
(conversationId: string) => {
const contactNameColors: Map<string, ContactNameColorType> = new Map();
const { sortedGroupMembers = [] } = conversationSelector(
conversationId
);
[...sortedGroupMembers]
.sort((left, right) =>
String(left.uuid) > String(right.uuid) ? 1 : -1
)
.forEach((member, i) => {
contactNameColors.set(
member.id,
ContactNameColors[i % ContactNameColors.length]
);
});
return contactNameColors;
},
{ max: 100 }
);
}
);
export const getContactNameColorSelector = createSelector(
getCachedConversationMemberColorsSelector,
conversationMemberColorsSelector => {
return (
conversationId: string,
contactId: string
): ContactNameColorType => {
const contactNameColors = conversationMemberColorsSelector(
conversationId
);
const color = contactNameColors.get(contactId);
if (!color) {
window.log.warn(`No color generated for contact ${contactId}`);
return ContactNameColors[0];
}
return color;
};
}
);
export const getConversationsWithCustomColorSelector = createSelector( export const getConversationsWithCustomColorSelector = createSelector(
getAllConversations, getAllConversations,
conversations => { conversations => {

View file

@ -55,10 +55,12 @@ import { isMoreRecentThan } from '../../util/timestamp';
import { ConversationType } from '../ducks/conversations'; import { ConversationType } from '../ducks/conversations';
import { AccountSelectorType } from './accounts';
import { CallSelectorType, CallStateType } from './calling'; import { CallSelectorType, CallStateType } from './calling';
import { import {
GetConversationByIdType, GetConversationByIdType,
isMissingRequiredProfileSharing, isMissingRequiredProfileSharing,
ContactNameColorSelectorType,
} from './conversations'; } from './conversations';
import { import {
SendStatus, SendStatus,
@ -100,7 +102,8 @@ export type GetPropsForBubbleOptions = Readonly<{
regionCode: string; regionCode: string;
callSelector: CallSelectorType; callSelector: CallSelectorType;
activeCall?: CallStateType; activeCall?: CallStateType;
accountSelector: (identifier?: string) => boolean; accountSelector: AccountSelectorType;
contactNameColorSelector: ContactNameColorSelectorType;
}>; }>;
export function isIncoming( export function isIncoming(
@ -450,10 +453,13 @@ export type GetPropsForMessageOptions = Pick<
GetPropsForBubbleOptions, GetPropsForBubbleOptions,
| 'conversationSelector' | 'conversationSelector'
| 'ourConversationId' | 'ourConversationId'
| 'ourUuid'
| 'ourNumber'
| 'selectedMessageId' | 'selectedMessageId'
| 'selectedMessageCounter' | 'selectedMessageCounter'
| 'regionCode' | 'regionCode'
| 'accountSelector' | 'accountSelector'
| 'contactNameColorSelector'
>; >;
type ShallowPropsType = Pick< type ShallowPropsType = Pick<
@ -462,6 +468,7 @@ type ShallowPropsType = Pick<
| 'canDownload' | 'canDownload'
| 'canReply' | 'canReply'
| 'contact' | 'contact'
| 'contactNameColor'
| 'conversationColor' | 'conversationColor'
| 'conversationId' | 'conversationId'
| 'conversationType' | 'conversationType'
@ -497,12 +504,15 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
accountSelector, accountSelector,
conversationSelector, conversationSelector,
ourConversationId, ourConversationId,
ourNumber,
ourUuid,
regionCode, regionCode,
selectedMessageId, selectedMessageId,
selectedMessageCounter, selectedMessageCounter,
contactNameColorSelector,
}: GetPropsForMessageOptions }: GetPropsForMessageOptions
): ShallowPropsType => { ): ShallowPropsType => {
const { expireTimer, expirationStartTimestamp } = message; const { expireTimer, expirationStartTimestamp, conversationId } = message;
const expirationLength = expireTimer ? expireTimer * 1000 : undefined; const expirationLength = expireTimer ? expireTimer * 1000 : undefined;
const expirationTimestamp = const expirationTimestamp =
expirationStartTimestamp && expirationLength expirationStartTimestamp && expirationLength
@ -522,14 +532,26 @@ const getShallowPropsForMessage = createSelectorCreator(memoizeByRoot, isEqual)(
{} {}
).emoji; ).emoji;
const author = getContact(message, {
conversationSelector,
ourConversationId,
ourNumber,
ourUuid,
});
const contactNameColor = contactNameColorSelector(
conversationId,
author.id
);
return { return {
canDeleteForEveryone: canDeleteForEveryone(message), canDeleteForEveryone: canDeleteForEveryone(message),
canDownload: canDownload(message, conversationSelector), canDownload: canDownload(message, conversationSelector),
canReply: canReply(message, ourConversationId, conversationSelector), canReply: canReply(message, ourConversationId, conversationSelector),
contact: getPropsForEmbeddedContact(message, regionCode, accountSelector), contact: getPropsForEmbeddedContact(message, regionCode, accountSelector),
contactNameColor,
conversationColor: conversationColor:
conversation?.conversationColor ?? ConversationColors[0], conversation?.conversationColor ?? ConversationColors[0],
conversationId: message.conversationId, conversationId,
conversationType: isGroup ? 'group' : 'direct', conversationType: isGroup ? 'group' : 'direct',
customColor: conversation?.customColor, customColor: conversation?.customColor,
deletedForEveryone: message.deletedForEveryone || false, deletedForEveryone: message.deletedForEveryone || false,

View file

@ -10,7 +10,6 @@ import { StateType } from '../reducer';
import { TimelineItem } from '../../components/conversation/TimelineItem'; import { TimelineItem } from '../../components/conversation/TimelineItem';
import { getIntl, getInteractionMode, getTheme } from '../selectors/user'; import { getIntl, getInteractionMode, getTheme } from '../selectors/user';
import { import {
getContactNameColorSelector,
getConversationSelector, getConversationSelector,
getMessageSelector, getMessageSelector,
getSelectedMessage, getSelectedMessage,
@ -44,14 +43,6 @@ const mapStateToProps = (state: StateType, props: ExternalProps) => {
const messageSelector = getMessageSelector(state); const messageSelector = getMessageSelector(state);
const item = messageSelector(id); const item = messageSelector(id);
if (item?.type === 'message' && item.data.conversationType === 'group') {
const { author } = item.data;
item.data.contactNameColor = getContactNameColorSelector(state)(
conversationId,
author.id
);
}
const selectedMessage = getSelectedMessage(state); const selectedMessage = getSelectedMessage(state);
const isSelected = Boolean(selectedMessage && id === selectedMessage.id); const isSelected = Boolean(selectedMessage && id === selectedMessage.id);

View file

@ -1737,6 +1737,7 @@ describe('both/state/selectors/conversations', () => {
it('returns the right color order sorted by UUID ASC', () => { it('returns the right color order sorted by UUID ASC', () => {
const group = makeConversation('group'); const group = makeConversation('group');
group.type = 'group';
group.sortedGroupMembers = [ group.sortedGroupMembers = [
makeConversationWithUuid('zyx'), makeConversationWithUuid('zyx'),
makeConversationWithUuid('vut'), makeConversationWithUuid('vut'),
@ -1766,5 +1767,28 @@ describe('both/state/selectors/conversations', () => {
assert.equal(contactNameColorSelector('group', 'vut'), '330'); assert.equal(contactNameColorSelector('group', 'vut'), '330');
assert.equal(contactNameColorSelector('group', 'zyx'), '230'); assert.equal(contactNameColorSelector('group', 'zyx'), '230');
}); });
it('returns the right colors for direct conversation', () => {
const direct = makeConversation('theirId');
const emptyState = getEmptyRootState();
const state = {
...emptyState,
user: {
...emptyState.user,
ourConversationId: 'us',
},
conversations: {
...getEmptyState(),
conversationLookup: {
direct,
},
},
};
const contactNameColorSelector = getContactNameColorSelector(state);
assert.equal(contactNameColorSelector('direct', 'theirId'), '200');
assert.equal(contactNameColorSelector('direct', 'us'), '200');
});
}); });
}); });