diff --git a/stylesheets/_session_left_pane.scss b/stylesheets/_session_left_pane.scss index ade13d4d4..982d7a4d7 100644 --- a/stylesheets/_session_left_pane.scss +++ b/stylesheets/_session_left_pane.scss @@ -249,6 +249,14 @@ $session-compose-margin: 20px; margin-bottom: 3rem; flex-shrink: 0; } + + .message-request-list__container { + width: 100%; + + .session-button { + margin: $session-margin-xs 0; + } + } } } .module-search-results { diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index cdb1921f3..99d1a1d07 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -27,6 +27,10 @@ import { useSelector } from 'react-redux'; import { SectionType } from '../state/ducks/section'; import { getFocusedSection } from '../state/selectors/section'; import { ConversationNotificationSettingType } from '../models/conversation'; +import { Flex } from './basic/Flex'; +import { SessionButton } from './session/SessionButton'; +import { getConversationById } from '../data/data'; +import { getConversationController } from '../session/conversations'; // tslint:disable-next-line: no-empty-interface export interface ConversationListItemProps extends ReduxConversationType {} @@ -42,6 +46,7 @@ export const StyledConversationListItemIconWrapper = styled.div` type PropsHousekeeping = { style?: Object; + isMessageRequest?: boolean; }; // tslint:disable: use-simple-attributes @@ -261,6 +266,7 @@ const ConversationListItem = (props: Props) => { avatarPath, isPrivate, currentNotificationSetting, + isMessageRequest, } = props; const triggerId = `conversation-item-${conversationId}-ctxmenu`; const key = `conversation-item-${conversationId}`; @@ -277,15 +283,32 @@ const ConversationListItem = (props: Props) => { [conversationId] ); + /** + * deletes the conversation + */ + const handleConversationDecline = async () => { + await getConversationController().deleteContact(conversationId); + }; + + /** + * marks the conversation as approved. + */ + const handleConversationAccept = async () => { + const convo = await getConversationById(conversationId); + convo?.setIsApproved(true); + console.warn('convo marked as approved'); + console.warn({ convo }); + }; + return (
{ - e.stopPropagation(); - e.preventDefault(); - }} + // onMouseUp={e => { + // e.stopPropagation(); + // e.preventDefault(); + // }} onContextMenu={(e: any) => { contextMenu.show({ id: triggerId, @@ -327,6 +350,16 @@ const ConversationListItem = (props: Props) => { unreadCount={unreadCount || 0} lastMessage={lastMessage} /> + {isMessageRequest ? ( + + Decline + Accept + + ) : null}
diff --git a/ts/components/session/LeftPaneMessageSection.tsx b/ts/components/session/LeftPaneMessageSection.tsx index 23a3112eb..fc6871017 100644 --- a/ts/components/session/LeftPaneMessageSection.tsx +++ b/ts/components/session/LeftPaneMessageSection.tsx @@ -28,6 +28,7 @@ import { onsNameRegex } from '../../session/snode_api/SNodeAPI'; import { SNodeAPI } from '../../session/snode_api'; import { clearSearch, search, updateSearchTerm } from '../../state/ducks/search'; import _ from 'lodash'; +import { MessageRequestsBanner } from './MessageRequestsBanner'; export interface Props { searchTerm: string; @@ -95,6 +96,15 @@ export class LeftPaneMessageSection extends React.Component { throw new Error('render: must provided conversations if no search results are provided'); } + // TODO: make selectors for this instead. + // TODO: only filter conversations if setting for requests is applied + const approvedConversations = conversations.filter(c => c.isApproved === true); + console.warn({ approvedConversations }); + const messageRequestsEnabled = + window.inboxStore?.getState().userConfig.messageRequests === true; + + const conversationsForList = messageRequestsEnabled ? approvedConversations : conversations; + const length = conversations.length; const listKey = 0; // Note: conversations is not a known prop for List, but it is required to ensure that @@ -106,7 +116,7 @@ export class LeftPaneMessageSection extends React.Component { {({ height, width }) => ( { onChange={this.updateSearch} placeholder={window.i18n('searchFor...')} /> -
message requests
+ {this.renderList()} {this.renderBottomButtons()} diff --git a/ts/components/session/MessageRequestsBanner.tsx b/ts/components/session/MessageRequestsBanner.tsx new file mode 100644 index 000000000..8e5fcb2ce --- /dev/null +++ b/ts/components/session/MessageRequestsBanner.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import styled from 'styled-components'; +import { getLeftPaneLists } from '../../state/selectors/conversations'; +import { SessionIcon, SessionIconSize, SessionIconType } from './icon'; + +const StyledMessageRequestBanner = styled.div` + border-left: var(--border-unread); + height: 64px; + width: 100%; + max-width: 300px; + display: flex; + flex-direction: row; + padding: 8px 16px; + align-items: center; + cursor: pointer; + + transition: var(--session-transition-duration); + + &:hover { + background: var(--color-clickable-hovered); + } +`; + +const StyledMessageRequestBannerHeader = styled.span` + font-weight: bold; + font-size: 15px; + color: var(--color-text-subtle); + padding-left: var(--margin-xs); + margin-inline-start: 12px; + margin-top: var(--margin-sm); + line-height: 18px; + overflow-x: hidden; + overflow-y: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const StyledCircleIcon = styled.div` + padding-left: var(--margin-xs); +`; + +const StyledUnreadCounter = styled.div` + font-weight: bold; + border-radius: 50%; + background-color: var(--color-clickable-hovered); + margin-left: 10px; + width: 20px; + height: 20px; + line-height: 25px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; +`; + +const StyledGridContainer = styled.div` + border: solid 1px black; + display: flex; + width: 36px; + height: 36px; + align-items: center; + border-radius: 50%; + justify-content: center; + background-color: var(--color-conversation-item-has-unread); +`; + +export const CirclularIcon = (props: { iconType: SessionIconType; iconSize: SessionIconSize }) => { + const { iconSize, iconType } = props; + + return ( + + + + + + ); +}; + +export const MessageRequestsBanner = (props: { handleOnClick: () => any }) => { + const { handleOnClick } = props; + const convos = useSelector(getLeftPaneLists).conversations; + const pendingRequests = convos.filter(c => c.isApproved !== true) || []; + + return ( + + + Message Requests + +
{pendingRequests.length}
+
+
+ ); +}; diff --git a/ts/components/session/SessionClosableOverlay.tsx b/ts/components/session/SessionClosableOverlay.tsx index c643762be..a396d62ca 100644 --- a/ts/components/session/SessionClosableOverlay.tsx +++ b/ts/components/session/SessionClosableOverlay.tsx @@ -292,79 +292,30 @@ export class SessionClosableOverlay extends React.Component { } } + + const MessageRequestList = () => { // get all conversations with (accepted / known) - // const convos = useSelector(getConversationLookup); - const lists = useSelector(getLeftPaneLists); - const conversationx = lists?.conversations as Array; - console.warn({ conversationx }); - - // console.warn({ convos }); - // const allConversations = getConversationController().getConversations(); - // const messageRequests = allConversations.filter(convo => convo.get('isApproved') !== true); - + const unapprovedConversations = lists?.conversations.filter(c => { + return !c.isApproved; + }) as Array; return ( - <> - {/* {messageRequests.map(convoOfMessage => { */} - {conversationx.map(convoOfMessage => { - return ; +
+ {unapprovedConversations.map(conversation => { + return ; })} - +
); }; // const MessageRequestListItem = (props: { conversation: ConversationModel }) => { const MessageRequestListItem = (props: { conversation: ConversationListItemProps }) => { const { conversation } = props; - // const { id: conversationId } = conversation; - - // TODO: add hovering - // TODO: add styling - - /** - * open the conversation selected - */ - // const openConvo = useCallback( - // async (e: React.MouseEvent) => { - // // mousedown is invoked sooner than onClick, but for both right and left click - // if (e.button === 0) { - // await openConversationWithMessages({ conversationKey: conversationId }); - // } - // }, - // [conversationId] - // ); - - // /** - // * show basic highlight animation - // */ - // const handleMouseOver = () => { - // console.warn('hovered'); - // }; - return ( - //
{ - // e.stopPropagation(); - // e.preventDefault(); - // }} - // // className="message-request__item" - - // // className={classNames( - // // 'module-conversation-list-item', - // // unreadCount && unreadCount > 0 ? 'module-conversation-list-item--has-unread' : null, - // // unreadCount && unreadCount > 0 && mentionedUs - // // ? 'module-conversation-list-item--mentioned-us' - // // : null, - // // isSelected ? 'module-conversation-list-item--is-selected' : null, - // // isBlocked ? 'module-conversation-list-item--is-blocked' : null - // // )} - // > - // {conversation.getName()} - //
- - + ); }; diff --git a/ts/components/session/settings/SessionSettings.tsx b/ts/components/session/settings/SessionSettings.tsx index 05db39c86..5c6228ee0 100644 --- a/ts/components/session/settings/SessionSettings.tsx +++ b/ts/components/session/settings/SessionSettings.tsx @@ -17,7 +17,11 @@ import { import { shell } from 'electron'; import { mapDispatchToProps } from '../../../state/actions'; import { unblockConvoById } from '../../../interactions/conversationInteractions'; -import { toggleAudioAutoplay } from '../../../state/ducks/userConfig'; +import { + disableMessageRequests, + enableMessageRequests, + toggleAudioAutoplay, +} from '../../../state/ducks/userConfig'; import { sessionPassword, updateConfirmModal } from '../../../state/ducks/modalDialog'; import { PasswordAction } from '../../dialog/SessionPasswordDialog'; import { SessionIconButton } from '../icon'; @@ -406,7 +410,28 @@ class SettingsViewInner extends React.Component { comparisonValue: undefined, onClick: undefined, }, + { + id: 'message-request-setting', + title: 'Message Requests', // TODO: translation + description: 'Enable Message Request Inbox', + hidden: false, + type: SessionSettingType.Toggle, + category: SessionSettingCategory.Appearance, + setFn: () => { + window.inboxStore?.dispatch(toggleAudioAutoplay()); + if (window.inboxStore?.getState().userConfig.messageRequests) { + window.inboxStore?.dispatch(disableMessageRequests()); + } else { + window.inboxStore?.dispatch(enableMessageRequests()); + } + }, + content: { + defaultValue: window.inboxStore?.getState().userConfig.audioAutoplay, + }, + comparisonValue: undefined, + onClick: undefined, + }, { id: 'notification-setting', title: window.i18n('notificationSettingsDialog'), diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 3704170a2..8919c9afe 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -714,6 +714,12 @@ export class ConversationModel extends Backbone.Model { const sentAt = message.get('sent_at'); + // TODO: for debuggong + if (message.get('body')?.includes('unapprove')) { + console.warn('setting to unapprove'); + await this.setIsApproved(false); + } + if (!sentAt) { throw new Error('sendMessageJob() sent_at must be set.'); } @@ -771,6 +777,7 @@ export class ConversationModel extends Backbone.Model { const chatMessagePrivate = new VisibleMessage(chatMessageParams); await getMessageQueue().sendToPubKey(destinationPubkey, chatMessagePrivate); + // this.setIsApproved(true); // consider the conversation approved even if the message fails to send return; } @@ -1384,7 +1391,7 @@ export class ConversationModel extends Backbone.Model { public async setIsApproved(value: boolean) { if (value !== this.get('isApproved')) { this.set({ - isApproved: true, + isApproved: value, }); await this.commit(); } diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index 08f3e39a0..3e0988f87 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -99,6 +99,7 @@ export class ConversationController { const create = async () => { try { + debugger; await saveConversation(conversation.attributes); } catch (error) { window?.log?.error( diff --git a/ts/state/ducks/userConfig.tsx b/ts/state/ducks/userConfig.tsx index af64b2cb4..ea34b9cfd 100644 --- a/ts/state/ducks/userConfig.tsx +++ b/ts/state/ducks/userConfig.tsx @@ -7,11 +7,13 @@ import { createSlice } from '@reduxjs/toolkit'; export interface UserConfigState { audioAutoplay: boolean; showRecoveryPhrasePrompt: boolean; + messageRequests: boolean; } export const initialUserConfigState = { audioAutoplay: false, showRecoveryPhrasePrompt: true, + messageRequests: true, }; const userConfigSlice = createSlice({ @@ -24,9 +26,20 @@ const userConfigSlice = createSlice({ disableRecoveryPhrasePrompt: state => { state.showRecoveryPhrasePrompt = false; }, + disableMessageRequests: state => { + state.messageRequests = false; + }, + enableMessageRequests: state => { + state.messageRequests = true; + }, }, }); const { actions, reducer } = userConfigSlice; -export const { toggleAudioAutoplay, disableRecoveryPhrasePrompt } = actions; +export const { + toggleAudioAutoplay, + disableRecoveryPhrasePrompt, + disableMessageRequests, + enableMessageRequests, +} = actions; export const userConfigReducer = reducer;