Don't mutate state in TimelineItem
This commit is contained in:
parent
1cc7c5dc2d
commit
80c1ad6ee3
5 changed files with 127 additions and 61 deletions
|
@ -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,
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue