diff --git a/js/models/conversations.d.ts b/js/models/conversations.d.ts index 082bfc856..c94f64668 100644 --- a/js/models/conversations.d.ts +++ b/js/models/conversations.d.ts @@ -65,7 +65,7 @@ export interface ConversationModel isRss: () => boolean; isBlocked: () => boolean; isClosable: () => boolean; - isModerator: (id: string) => boolean; + isAdmin: (id: string) => boolean; throttledBumpTyping: () => void; messageCollection: Backbone.Collection; diff --git a/js/models/conversations.js b/js/models/conversations.js index c1d171cea..981117a0c 100644 --- a/js/models/conversations.js +++ b/js/models/conversations.js @@ -457,14 +457,18 @@ format() { return this.cachedProps; }, + getGroupAdmins() { + return this.get('groupAdmins') || this.get('moderators'); + }, getProps() { const { format } = PhoneNumber; const regionCode = storage.get('regionCode'); const typingKeys = Object.keys(this.contactTypingTimers || {}); - const groupAdmins = this.isPublic() - ? this.get('moderators') - : this.get('groupAdmins'); + const groupAdmins = this.getGroupAdmins(); + + const members = + this.isGroup() && !this.isPublic() ? this.get('members') : undefined; const result = { id: this.id, @@ -499,7 +503,7 @@ isKickedFromGroup: !!this.get('isKickedFromGroup'), left: !!this.get('left'), groupAdmins, - + members, onClick: () => this.trigger('select', this), onBlockContact: () => this.block(), onUnblockContact: () => this.unblock(), @@ -676,6 +680,15 @@ } }, async updateGroupAdmins(groupAdmins) { + const existingAdmins = _.sortBy(this.getGroupAdmins()); + const newAdmins = _.sortBy(groupAdmins); + + if (_.isEqual(existingAdmins, newAdmins)) { + window.log.info( + 'Skipping updates of groupAdmins/moderators. No change detected.' + ); + return; + } this.set({ groupAdmins }); await this.commit(); }, @@ -1828,27 +1841,16 @@ await this.commit(); } }, - isModerator(pubKey) { + isAdmin(pubKey) { if (!this.isPublic()) { return false; } if (!pubKey) { - throw new Error('isModerator() pubKey is falsy'); + throw new Error('isAdmin() pubKey is falsy'); } - const moderators = this.get('moderators'); - return Array.isArray(moderators) && moderators.includes(pubKey); + const groupAdmins = this.getGroupAdmins(); + return Array.isArray(groupAdmins) && groupAdmins.includes(pubKey); }, - async setModerators(moderators) { - if (!this.isPublic()) { - return; - } - // TODO: compare array properly - if (!_.isEqual(this.get('moderators'), moderators)) { - this.set({ moderators }); - await this.commit(); - } - }, - // SIGNAL PROFILES onChangeProfileKey() { diff --git a/js/models/messages.js b/js/models/messages.js index 0f0c8cc26..32e9b77fe 100644 --- a/js/models/messages.js +++ b/js/models/messages.js @@ -568,8 +568,7 @@ // for the public group chat const conversation = this.getConversation(); - const isModerator = - conversation && !!conversation.isModerator(phoneNumber); + const isAdmin = conversation && !!conversation.isAdmin(phoneNumber); const convoId = conversation ? conversation.id : undefined; const isGroup = !!conversation && !conversation.isPrivate(); @@ -606,9 +605,9 @@ conversation && conversation.get('isKickedFromGroup'), isDeletable: !this.get('isPublic') || - isModerator || + isAdmin || phoneNumber === textsecure.storage.user.getNumber(), - isModerator, + isAdmin, onCopyText: () => this.copyText(), onCopyPubKey: () => this.copyPubKey(), diff --git a/js/modules/loki_app_dot_net_api.js b/js/modules/loki_app_dot_net_api.js index 48c1d301b..379eae63b 100644 --- a/js/modules/loki_app_dot_net_api.js +++ b/js/modules/loki_app_dot_net_api.js @@ -1204,7 +1204,7 @@ class LokiPublicChannelAPI { } if (this.running) { - await this.conversation.setModerators(moderators || []); + await this.conversation.updateGroupAdmins(moderators || []); } } diff --git a/js/views/create_group_dialog_view.js b/js/views/create_group_dialog_view.js index 802576813..c09ed9955 100644 --- a/js/views/create_group_dialog_view.js +++ b/js/views/create_group_dialog_view.js @@ -31,7 +31,7 @@ this.titleText = i18n('updateGroupDialogTitle', this.groupName); // I'd much prefer to integrate mods with groupAdmins // but lets discuss first... - this.isAdmin = groupConvo.isModerator( + this.isAdmin = groupConvo.isAdmin( window.storage.get('primaryDevicePubKey') ); } @@ -92,7 +92,7 @@ this.titleText = i18n('updateGroupDialogTitle', this.groupName); // I'd much prefer to integrate mods with groupAdmins // but lets discuss first... - this.isAdmin = groupConvo.isModerator( + this.isAdmin = groupConvo.isAdmin( window.storage.get('primaryDevicePubKey') ); // zero out contactList for now diff --git a/ts/components/LeftPane.tsx b/ts/components/LeftPane.tsx index 640bf0d7e..5e01cfb05 100644 --- a/ts/components/LeftPane.tsx +++ b/ts/components/LeftPane.tsx @@ -69,7 +69,6 @@ export class LeftPane extends React.Component { } public render(): JSX.Element { - const ourPrimaryConversation = this.props.ourPrimaryConversation; return (
@@ -77,8 +76,6 @@ export class LeftPane extends React.Component { {...this.props} selectedSection={this.props.focusedSection} onSectionSelected={this.handleSectionSelected} - unreadMessageCount={this.props.unreadMessageCount} - ourPrimaryConversation={ourPrimaryConversation} />
{this.renderSection()}
@@ -162,22 +159,13 @@ export class LeftPane extends React.Component { } private renderSettingSection() { - const { - isSecondaryDevice, - showSessionSettingsCategory, - settingsCategory, - } = this.props; + const { settingsCategory } = this.props; const category = settingsCategory || SessionSettingCategory.Appearance; return ( <> - + ); } diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index 3e9f72df4..e4f3d1720 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -52,8 +52,8 @@ interface LinkPreviewType { export interface Props { disableMenu?: boolean; isDeletable: boolean; - isModerator?: boolean; - weAreModerator?: boolean; + isAdmin?: boolean; + weAreAdmin?: boolean; text?: string; bodyPending?: boolean; id: string; @@ -574,7 +574,7 @@ class MessageInner extends React.PureComponent { authorPhoneNumber, authorProfileName, collapseMetadata, - isModerator, + isAdmin, conversationType, direction, onShowUserDetails, @@ -605,7 +605,7 @@ class MessageInner extends React.PureComponent { }} pubkey={authorPhoneNumber} /> - {isModerator && ( + {isAdmin && (
@@ -692,7 +692,7 @@ class MessageInner extends React.PureComponent { onRetrySend, onShowDetail, isPublic, - weAreModerator, + weAreAdmin, onBanUser, } = this.props; @@ -760,7 +760,7 @@ class MessageInner extends React.PureComponent { ) : null} - {weAreModerator && isPublic ? ( + {weAreAdmin && isPublic ? ( {window.i18n('banUser')} ) : null} diff --git a/ts/components/conversation/message/MessageMetadata.tsx b/ts/components/conversation/message/MessageMetadata.tsx index 26bbf19e8..dec4ce8fb 100644 --- a/ts/components/conversation/message/MessageMetadata.tsx +++ b/ts/components/conversation/message/MessageMetadata.tsx @@ -14,7 +14,7 @@ import styled, { DefaultTheme } from 'styled-components'; type Props = { disableMenu?: boolean; - isModerator?: boolean; + isAdmin?: boolean; isDeletable: boolean; text?: string; bodyPending?: boolean; @@ -74,7 +74,7 @@ export const MessageMetadata = (props: Props) => { serverTimestamp, isShowingImage, isPublic, - isModerator, + isAdmin, theme, } = props; @@ -109,7 +109,7 @@ export const MessageMetadata = (props: Props) => { diff --git a/ts/components/conversation/message/MetadataBadge.tsx b/ts/components/conversation/message/MetadataBadge.tsx index 584feeccf..11d3d45cd 100644 --- a/ts/components/conversation/message/MetadataBadge.tsx +++ b/ts/components/conversation/message/MetadataBadge.tsx @@ -45,15 +45,15 @@ type BadgesProps = { id: string; direction: string; isPublic?: boolean; - isModerator?: boolean; + isAdmin?: boolean; withImageNoCaption: boolean; }; export const MetadataBadges = (props: BadgesProps): JSX.Element => { - const { id, direction, isPublic, isModerator, withImageNoCaption } = props; + const { id, direction, isPublic, isAdmin, withImageNoCaption } = props; const badges = [ (isPublic && 'Public') || null, - (isModerator && 'Mod') || null, + (isAdmin && 'Mod') || null, ].filter(nonNullish); if (!badges || badges.length === 0) { diff --git a/ts/components/session/ActionsPanel.tsx b/ts/components/session/ActionsPanel.tsx index 4bb1f87cc..eace9d52b 100644 --- a/ts/components/session/ActionsPanel.tsx +++ b/ts/components/session/ActionsPanel.tsx @@ -10,10 +10,11 @@ import { ConversationType } from '../../state/ducks/conversations'; import { noop } from 'lodash'; import { DefaultTheme } from 'styled-components'; import { StateType } from '../../state/reducer'; -import { MessageEncrypter } from '../../session/crypto'; -import { PubKey } from '../../session/types'; import { UserUtil } from '../../util'; import { ConversationController } from '../../session/conversations'; +import { getFocusedSection } from '../../state/selectors/section'; +import { getTheme } from '../../state/selectors/theme'; +import { getPrimaryPubkey } from '../../state/selectors/user'; // tslint:disable-next-line: no-import-side-effect no-submodule-imports export enum SectionType { @@ -30,6 +31,7 @@ interface Props { selectedSection: SectionType; unreadMessageCount: number; ourPrimaryConversation: ConversationType; + ourPrimary: string; applyTheme?: any; theme: DefaultTheme; } @@ -68,6 +70,7 @@ class ActionsPanelPrivate extends React.Component { avatarPath?: string; notificationCount?: number; }) => { + const { ourPrimary } = this.props; const handleClick = onSelect ? () => { /* tslint:disable:no-void-expression */ @@ -89,7 +92,6 @@ class ActionsPanelPrivate extends React.Component { : undefined; if (type === SectionType.Profile) { - const ourPrimary = window.storage.get('primaryDevicePubKey'); const conversation = ConversationController.getInstance().get(ourPrimary); const profile = conversation?.getLokiProfile(); @@ -202,11 +204,10 @@ class ActionsPanelPrivate extends React.Component { } const mapStateToProps = (state: StateType) => { - const { section, theme } = state; - return { - section: section.focusedSection, - theme, + section: getFocusedSection(state), + theme: getTheme(state), + ourPrimary: getPrimaryPubkey(state), }; }; diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx index 6fb9a096c..5c4c5d9c7 100644 --- a/ts/components/session/conversation/SessionCompositionBox.tsx +++ b/ts/components/session/conversation/SessionCompositionBox.tsx @@ -29,6 +29,7 @@ import { MemberItem } from '../../conversation/MemberList'; import { CaptionEditor } from '../../CaptionEditor'; import { DefaultTheme } from 'styled-components'; import { ConversationController } from '../../../session/conversations/ConversationController'; +import { ConversationType } from '../../../state/ducks/conversations'; export interface ReplyingToMessageProps { convoId: string; @@ -64,7 +65,8 @@ interface Props { isPrivate: boolean; isKickedFromGroup: boolean; left: boolean; - conversationKey: string; + selectedConversationKey: string; + selectedConversation: ConversationType | undefined; isPublic: boolean; quotedMessageProps?: ReplyingToMessageProps; @@ -186,7 +188,9 @@ export class SessionCompositionBox extends React.Component { } public componentDidUpdate(prevProps: Props, _prevState: State) { // reset the state on new conversation key - if (prevProps.conversationKey !== this.props.conversationKey) { + if ( + prevProps.selectedConversationKey !== this.props.selectedConversationKey + ) { this.setState(getDefaultState(), this.focusCompositionBox); this.lastBumpTypingMessageLength = 0; } else if ( @@ -452,13 +456,14 @@ export class SessionCompositionBox extends React.Component { } private fetchUsersForClosedGroup(query: any, callback: any) { - const conversationModel = ConversationController.getInstance().get( - this.props.conversationKey - ); - if (!conversationModel) { + const { selectedConversation } = this.props; + if (!selectedConversation) { + return; + } + const allPubKeys = selectedConversation.members; + if (!allPubKeys || allPubKeys.length === 0) { return; } - const allPubKeys = conversationModel.get('members'); const allMembers = allPubKeys.map(pubKey => { const conv = ConversationController.getInstance().get(pubKey); @@ -724,7 +729,7 @@ export class SessionCompositionBox extends React.Component { // catching ESC, tab, or whatever which is not typing if (message.length && message.length !== this.lastBumpTypingMessageLength) { const conversationModel = ConversationController.getInstance().get( - this.props.conversationKey + this.props.selectedConversationKey ); if (!conversationModel) { return; diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx index 85ba665ab..c274406e2 100644 --- a/ts/components/session/conversation/SessionConversation.tsx +++ b/ts/components/session/conversation/SessionConversation.tsx @@ -72,8 +72,8 @@ interface State { interface Props { ourPrimary: string; - conversationKey: string; - conversation: ConversationType; + selectedConversationKey: string; + selectedConversation?: ConversationType; theme: DefaultTheme; messages: Array; actions: any; @@ -88,13 +88,7 @@ export class SessionConversation extends React.Component { constructor(props: any) { super(props); - const { conversationKey } = this.props; - - const conversationModel = ConversationController.getInstance().get( - conversationKey - ); - - const unreadCount = conversationModel?.get('unreadCount') || 0; + const unreadCount = this.props.selectedConversation?.unreadCount || 0; this.state = { messageProgressVisible: false, sendingProgress: 0, @@ -163,10 +157,10 @@ export class SessionConversation extends React.Component { public componentDidUpdate(prevProps: Props, prevState: State) { const { - conversationKey: newConversationKey, - conversation: newConversation, + selectedConversationKey: newConversationKey, + selectedConversation: newConversation, } = this.props; - const { conversationKey: oldConversationKey } = prevProps; + const { selectedConversationKey: oldConversationKey } = prevProps; // if the convo is valid, and it changed, register for drag events if ( @@ -255,18 +249,19 @@ export class SessionConversation extends React.Component { } = this.state; const selectionMode = !!selectedMessages.length; - const { conversation, conversationKey, messages } = this.props; - const conversationModel = ConversationController.getInstance().get( - conversationKey - ); + const { + selectedConversation, + selectedConversationKey, + messages, + } = this.props; - if (!conversationModel || !messages) { + if (!selectedConversation || !messages) { // return an empty message view return ; } - - const { isRss } = conversation; - + const conversationModel = ConversationController.getInstance().get( + selectedConversationKey + ); // TODO VINCE: OPTIMISE FOR NEW SENDING??? const sendMessageFn = ( body: any, @@ -276,6 +271,9 @@ export class SessionConversation extends React.Component { groupInvitation: any, otherOptions: any ) => { + if (!conversationModel) { + return; + } void conversationModel.sendMessage( body, attachments, @@ -292,11 +290,12 @@ export class SessionConversation extends React.Component { } }; - const shouldRenderRightPanel = !conversationModel.isRss(); - const showSafetyNumber = infoViewState === 'safetyNumber'; const showMessageDetails = !!messageDetailShowProps; + const isPublic = selectedConversation.isPublic || false; + const isPrivate = selectedConversation.type === 'direct'; + return (
{this.renderHeader()}
@@ -344,45 +343,41 @@ export class SessionConversation extends React.Component { {isDraggingFile && }
- {!isRss && ( - // tslint:disable-next-line: use-simple-attributes - { - void this.replyToMessage(undefined); - }} - textarea={this.compositionBoxRef} - clearAttachments={this.clearAttachments} - removeAttachment={this.removeAttachment} - onChoseAttachments={this.onChoseAttachments} - theme={this.props.theme} - /> - )} + { + void this.replyToMessage(undefined); + }} + textarea={this.compositionBoxRef} + clearAttachments={this.clearAttachments} + removeAttachment={this.removeAttachment} + onChoseAttachments={this.onChoseAttachments} + theme={this.props.theme} + /> - {shouldRenderRightPanel && ( -
- -
- )} +
+ +
); } @@ -397,33 +392,34 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public async loadInitialMessages() { - const { conversationKey } = this.props; - const conversationModel = ConversationController.getInstance().get( - conversationKey - ); - if (!conversationModel) { + const { selectedConversation, selectedConversationKey } = this.props; + + if (!selectedConversation) { return; } + const conversationModel = ConversationController.getInstance().get( + selectedConversationKey + ); const unreadCount = await conversationModel.getUnreadCount(); const messagesToFetch = Math.max( Constants.CONVERSATION.DEFAULT_MESSAGE_FETCH_COUNT, unreadCount ); this.props.actions.fetchMessagesForConversation({ - conversationKey, + conversationKey: selectedConversationKey, count: messagesToFetch, }); } public getHeaderProps() { - const { conversationKey } = this.props; + const { selectedConversationKey, ourPrimary } = this.props; const { selectedMessages, infoViewState, messageDetailShowProps, } = this.state; const conversation = ConversationController.getInstance().getOrThrow( - conversationKey + selectedConversationKey ); const expireTimer = conversation.get('expireTimer'); const expirationSettingName = expireTimer @@ -446,9 +442,7 @@ export class SessionConversation extends React.Component { isPrivate: conversation.isPrivate(), isPublic: conversation.isPublic(), isRss: conversation.isRss(), - isAdmin: conversation.isModerator( - window.storage.get('primaryDevicePubKey') - ), + isAdmin: conversation.isAdmin(ourPrimary), members, subscriberCount: conversation.get('subscriberCount'), isKickedFromGroup: conversation.get('isKickedFromGroup'), @@ -524,17 +518,23 @@ export class SessionConversation extends React.Component { } public getMessagesListProps() { - const { conversation, ourPrimary, messages, actions } = this.props; + const { + selectedConversation, + selectedConversationKey, + ourPrimary, + messages, + actions, + } = this.props; const { quotedMessageTimestamp, selectedMessages } = this.state; return { selectedMessages, ourPrimary, - conversationKey: conversation.id, + conversationKey: selectedConversationKey, messages, resetSelection: this.resetSelection, quotedMessageTimestamp, - conversation, + conversation: selectedConversation as ConversationType, selectMessage: this.selectMessage, deleteMessage: this.deleteMessage, fetchMessagesForConversation: actions.fetchMessagesForConversation, @@ -548,9 +548,9 @@ export class SessionConversation extends React.Component { } public getRightPanelProps() { - const { conversationKey } = this.props; + const { selectedConversationKey } = this.props; const conversation = ConversationController.getInstance().getOrThrow( - conversationKey + selectedConversationKey ); const ourPrimary = window.storage.get('primaryDevicePubKey'); @@ -558,7 +558,7 @@ export class SessionConversation extends React.Component { const isAdmin = conversation.isMediumGroup() ? true : conversation.isPublic() - ? conversation.isModerator(ourPrimary) + ? conversation.isAdmin(ourPrimary) : false; return { @@ -670,26 +670,32 @@ export class SessionConversation extends React.Component { askUserForConfirmation: boolean ) { // Get message objects - const { conversationKey, messages } = this.props; + const { + selectedConversationKey, + selectedConversation, + messages, + } = this.props; const conversationModel = ConversationController.getInstance().getOrThrow( - conversationKey + selectedConversationKey ); + if (!selectedConversation) { + window.log.info('No valid selected conversation.'); + return; + } const selectedMessages = messages.filter(message => messageIds.find(selectedMessage => selectedMessage === message.id) ); const multiple = selectedMessages.length > 1; - const isPublic = conversationModel.isPublic(); - // In future, we may be able to unsend private messages also // isServerDeletable also defined in ConversationHeader.tsx for // future reference - const isServerDeletable = isPublic; + const isServerDeletable = selectedConversation.isPublic; const warningMessage = (() => { - if (isPublic) { + if (selectedConversation.isPublic) { return multiple ? window.i18n('deleteMultiplePublicWarning') : window.i18n('deletePublicWarning'); @@ -704,7 +710,7 @@ export class SessionConversation extends React.Component { // VINCE TODO: MARK TO-DELETE MESSAGES AS READ - if (isPublic) { + if (selectedConversation.isPublic) { // Get our Moderator status const ourDevicePubkey = await UserUtil.getCurrentDevicePubKey(); if (!ourDevicePubkey) { @@ -713,7 +719,7 @@ export class SessionConversation extends React.Component { const ourPrimaryPubkey = ( await MultiDeviceProtocol.getPrimaryDevice(ourDevicePubkey) ).key; - const isModerator = conversationModel.isModerator(ourPrimaryPubkey); + const isAdmin = conversationModel.isAdmin(ourPrimaryPubkey); const ourNumbers = (await MultiDeviceProtocol.getOurDevices()).map( m => m.key ); @@ -721,7 +727,7 @@ export class SessionConversation extends React.Component { ourNumbers.includes(message.attributes.source) ); - if (!isAllOurs && !isModerator) { + if (!isAllOurs && !isAdmin) { ToastUtils.pushMessageDeleteForbidden(); this.setState({ selectedMessages: [] }); @@ -820,14 +826,14 @@ export class SessionConversation extends React.Component { // ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private async replyToMessage(quotedMessageTimestamp?: number) { - if (this.props.conversation.isBlocked) { + if (this.props.selectedConversation?.isBlocked) { pushUnblockToSend(); return; } if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) { - const { messages, conversationKey } = this.props; + const { messages, selectedConversationKey } = this.props; const conversationModel = ConversationController.getInstance().getOrThrow( - conversationKey + selectedConversationKey ); let quotedMessageProps = null; @@ -1229,7 +1235,7 @@ export class SessionConversation extends React.Component { private async updateMemberList() { const allPubKeys = await window.Signal.Data.getPubkeysInPublicConversation( - this.props.conversationKey + this.props.selectedConversationKey ); const allMembers = allPubKeys.map((pubKey: string) => { diff --git a/ts/components/session/conversation/SessionMessagesList.tsx b/ts/components/session/conversation/SessionMessagesList.tsx index 6f7435bab..c096dc7b9 100644 --- a/ts/components/session/conversation/SessionMessagesList.tsx +++ b/ts/components/session/conversation/SessionMessagesList.tsx @@ -297,9 +297,8 @@ export class SessionMessagesList extends React.Component { ); } - // allow moderators feature on messages (like banning a user) - if (messageProps.isPublic) { - messageProps.weAreModerator = conversation.groupAdmins?.includes( + if (messageProps.conversationType === 'group') { + messageProps.weAreAdmin = conversation.groupAdmins?.includes( ourPrimary ); } diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index b49284ee0..8dec10474 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -19,6 +19,10 @@ import { mapDispatchToProps } from '../../../state/actions'; import { connect } from 'react-redux'; import { StateType } from '../../../state/reducer'; import { ConversationController } from '../../../session/conversations'; +import { + getConversationLookup, + getConversations, +} from '../../../state/selectors/conversations'; export enum SessionSettingCategory { Appearance = 'appearance', @@ -746,10 +750,8 @@ class SettingsViewInner extends React.Component { } const mapStateToProps = (state: StateType) => { - const { conversations } = state; - return { - conversations: conversations.conversationLookup, + conversations: getConversationLookup(state), }; }; diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts index b70893c31..2992b54bd 100644 --- a/ts/state/ducks/conversations.ts +++ b/ts/state/ducks/conversations.ts @@ -35,7 +35,7 @@ export type MessageType = { isSelected?: boolean; }; -type MessageTypeInConvo = { +export type MessageTypeInConvo = { id: string; conversationId: string; attributes: any; @@ -80,6 +80,7 @@ export type ConversationType = { left: boolean; avatarPath?: string; // absolute filepath to the avatar groupAdmins?: Array; // admins for closed groups and moderators for open groups + members?: Array; // members for closed groups only }; export type ConversationLookupType = { [key: string]: ConversationType; diff --git a/ts/state/reducer.ts b/ts/state/reducer.ts index aa7429963..f6b07979b 100644 --- a/ts/state/reducer.ts +++ b/ts/state/reducer.ts @@ -8,7 +8,6 @@ import { import { reducer as user, UserStateType } from './ducks/user'; import { reducer as theme, ThemeStateType } from './ducks/theme'; import { reducer as section, SectionStateType } from './ducks/section'; -import { PubKey } from '../session/types'; export type StateType = { search: SearchStateType; diff --git a/ts/state/selectors/conversations.ts b/ts/state/selectors/conversations.ts index f7ae74be6..bd9e3f63b 100644 --- a/ts/state/selectors/conversations.ts +++ b/ts/state/selectors/conversations.ts @@ -7,6 +7,7 @@ import { ConversationLookupType, ConversationsStateType, ConversationType, + MessageTypeInConvo, } from '../ducks/conversations'; import { getIntl, getRegionCode, getUserNumber } from './user'; @@ -23,19 +24,33 @@ export const getConversationLookup = createSelector( } ); -export const getSelectedConversation = createSelector( +export const getSelectedConversationKey = createSelector( getConversations, (state: ConversationsStateType): string | undefined => { return state.selectedConversation; } ); +export const getSelectedConversation = createSelector( + getConversations, + (state: ConversationsStateType): ConversationType | undefined => { + return state.selectedConversation + ? state.conversationLookup[state.selectedConversation] + : undefined; + } +); + export const getOurPrimaryConversation = createSelector( getConversations, (state: ConversationsStateType): ConversationType => state.conversationLookup[window.storage.get('primaryDevicePubKey')] ); +export const getMessagesOfSelectedConversation = createSelector( + getConversations, + (state: ConversationsStateType): Array => state.messages +); + function getConversationTitle( conversation: ConversationType, options: { i18n: LocalizerType; ourRegionCode: string } @@ -229,14 +244,14 @@ export const _getSessionConversationInfo = ( export const getLeftPaneLists = createSelector( getConversationLookup, getConversationComparator, - getSelectedConversation, + getSelectedConversationKey, _getLeftPaneLists ); export const getSessionConversationInfo = createSelector( getConversationLookup, getConversationComparator, - getSelectedConversation, + getSelectedConversationKey, _getSessionConversationInfo ); diff --git a/ts/state/selectors/search.ts b/ts/state/selectors/search.ts index 17a997bc3..6465e6114 100644 --- a/ts/state/selectors/search.ts +++ b/ts/state/selectors/search.ts @@ -7,6 +7,7 @@ import { SearchStateType } from '../ducks/search'; import { getConversationLookup, getSelectedConversation, + getSelectedConversationKey, } from './conversations'; import { ConversationLookupType } from '../ducks/conversations'; @@ -38,7 +39,7 @@ export const getSearchResults = createSelector( getSearch, getRegionCode, getConversationLookup, - getSelectedConversation, + getSelectedConversationKey, getSelectedMessage, ], ( diff --git a/ts/state/selectors/section.ts b/ts/state/selectors/section.ts new file mode 100644 index 000000000..33a9bc66e --- /dev/null +++ b/ts/state/selectors/section.ts @@ -0,0 +1,12 @@ +import { createSelector } from 'reselect'; + +import { StateType } from '../reducer'; +import { SectionStateType } from '../ducks/section'; +import { SectionType } from '../../components/session/ActionsPanel'; + +export const getSection = (state: StateType): SectionStateType => state.section; + +export const getFocusedSection = createSelector( + getSection, + (state: SectionStateType): SectionType => state.focusedSection +); diff --git a/ts/state/selectors/theme.ts b/ts/state/selectors/theme.ts new file mode 100644 index 000000000..947da7e0b --- /dev/null +++ b/ts/state/selectors/theme.ts @@ -0,0 +1,4 @@ +import { StateType } from '../reducer'; +import { ThemeStateType } from '../ducks/theme'; + +export const getTheme = (state: StateType): ThemeStateType => state.theme; diff --git a/ts/state/smart/LeftPane.tsx b/ts/state/smart/LeftPane.tsx index 3b00d4634..648f4b82d 100644 --- a/ts/state/smart/LeftPane.tsx +++ b/ts/state/smart/LeftPane.tsx @@ -14,6 +14,8 @@ import { getOurPrimaryConversation, } from '../selectors/conversations'; import { mapDispatchToProps } from '../actions'; +import { getFocusedSection } from '../selectors/section'; +import { getTheme } from '../selectors/theme'; // Workaround: A react component's required properties are filtering up through connect() // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363 @@ -34,8 +36,8 @@ const mapStateToProps = (state: StateType) => { searchResults, i18n: getIntl(state), unreadMessageCount: leftPaneList.unreadCount, - theme: state.theme, - focusedSection: state.section.focusedSection, + theme: getTheme(state), + focusedSection: getFocusedSection(state), }; }; diff --git a/ts/state/smart/SessionConversation.tsx b/ts/state/smart/SessionConversation.tsx index 4753306a2..2b50bbd31 100644 --- a/ts/state/smart/SessionConversation.tsx +++ b/ts/state/smart/SessionConversation.tsx @@ -3,20 +3,20 @@ import { mapDispatchToProps } from '../actions'; import { SessionConversation } from '../../components/session/conversation/SessionConversation'; import { StateType } from '../reducer'; import { getPrimaryPubkey } from '../selectors/user'; +import { getTheme } from '../selectors/theme'; +import { + getMessagesOfSelectedConversation, + getSelectedConversation, + getSelectedConversationKey, +} from '../selectors/conversations'; const mapStateToProps = (state: StateType) => { - const conversationKey = state.conversations.selectedConversation; - const ourPrimary = getPrimaryPubkey(state); - const conversation = - (conversationKey && - state.conversations.conversationLookup[conversationKey]) || - null; return { - conversation, - conversationKey, - theme: state.theme, - messages: state.conversations.messages, - ourPrimary, + selectedConversation: getSelectedConversation(state), + selectedConversationKey: getSelectedConversationKey(state), + theme: getTheme(state), + messages: getMessagesOfSelectedConversation(state), + ourPrimary: getPrimaryPubkey(state), }; };