From f3430bdcb57fffb8c90f7b2de3027025fd8fd021 Mon Sep 17 00:00:00 2001
From: Audric Ackermann <audric@loki.network>
Date: Thu, 12 Nov 2020 14:39:37 +1100
Subject: [PATCH] add toast on isBlocked, isKicked, ... for sending of messages

---
 js/models/conversations.js                    |   1 +
 js/models/messages.js                         |  53 ++++----
 js/modules/debug.js                           |   2 +-
 js/views/conversation_view.js                 |  40 ------
 .../conversation/SessionCompositionBox.tsx    | 121 +++++++++++++-----
 .../conversation/SessionConversation.tsx      |  16 ++-
 .../SessionConversationMessagesList.tsx       |   1 +
 ts/session/utils/Toast.tsx                    |  16 +++
 ts/state/ducks/conversations.ts               |   2 +
 ts/state/ducks/messages.ts                    |  16 ++-
 ts/test/state/selectors/conversations_test.ts |  11 +-
 ts/window.d.ts                                |   2 +
 12 files changed, 171 insertions(+), 110 deletions(-)

diff --git a/js/models/conversations.js b/js/models/conversations.js
index 0c1fcfed2..32ffe48b0 100644
--- a/js/models/conversations.js
+++ b/js/models/conversations.js
@@ -517,6 +517,7 @@
         isOnline: this.isOnline(),
         hasNickname: !!this.getNickname(),
         isKickedFromGroup: !!this.get('isKickedFromGroup'),
+        leftGroup: !!this.get('left'),
 
         selectedMessages: this.selectedMessages,
 
diff --git a/js/models/messages.js b/js/models/messages.js
index 567f55d68..44898a3cf 100644
--- a/js/models/messages.js
+++ b/js/models/messages.js
@@ -660,8 +660,8 @@
         contact.number && contact.number[0] && contact.number[0].value;
       const onSendMessage = firstNumber
         ? () => {
-          this.trigger('open-conversation', firstNumber);
-        }
+            this.trigger('open-conversation', firstNumber);
+          }
         : null;
       const onClick = async () => {
         // First let's be sure that the signal account check is complete.
@@ -692,8 +692,8 @@
         !path && !objectUrl
           ? null
           : Object.assign({}, attachment.thumbnail || {}, {
-            objectUrl: path || objectUrl,
-          });
+              objectUrl: path || objectUrl,
+            });
 
       return Object.assign({}, attachment, {
         isVoiceMessage: Signal.Types.Attachment.isVoiceMessage(attachment),
@@ -746,13 +746,13 @@
       const onClick = noClick
         ? null
         : event => {
-          event.stopPropagation();
-          this.trigger('scroll-to-message', {
-            author,
-            id,
-            referencedMessageNotFound,
-          });
-        };
+            event.stopPropagation();
+            this.trigger('scroll-to-message', {
+              author,
+              id,
+              referencedMessageNotFound,
+            });
+          };
 
       const firstAttachment = quote.attachments && quote.attachments[0];
 
@@ -787,15 +787,15 @@
         url: path ? getAbsoluteAttachmentPath(path) : null,
         screenshot: screenshot
           ? {
-            ...screenshot,
-            url: getAbsoluteAttachmentPath(screenshot.path),
-          }
+              ...screenshot,
+              url: getAbsoluteAttachmentPath(screenshot.path),
+            }
           : null,
         thumbnail: thumbnail
           ? {
-            ...thumbnail,
-            url: getAbsoluteAttachmentPath(thumbnail.path),
-          }
+              ...thumbnail,
+              url: getAbsoluteAttachmentPath(thumbnail.path),
+            }
           : null,
       };
     },
@@ -824,9 +824,9 @@
       const phoneNumbers = this.isIncoming()
         ? [this.get('source')]
         : _.union(
-          this.get('sent_to') || [],
-          this.get('recipients') || this.getConversation().getRecipients()
-        );
+            this.get('sent_to') || [],
+            this.get('recipients') || this.getConversation().getRecipients()
+          );
 
       // This will make the error message for outgoing key errors a bit nicer
       const allErrors = (this.get('errors') || []).map(error => {
@@ -1052,7 +1052,6 @@
           window.log.warn('retrySend: Nobody to send to!');
 
           return this.commit();
-
         }
 
         const { body, attachments, preview, quote } = await this.uploadData();
@@ -1276,8 +1275,8 @@
          */
         const hasBodyOrAttachments = Boolean(
           dataMessage &&
-          (dataMessage.body ||
-            (dataMessage.attachments && dataMessage.attachments.length))
+            (dataMessage.body ||
+              (dataMessage.attachments && dataMessage.attachments.length))
         );
         const shouldNotifyPushServer =
           hasBodyOrAttachments && isSessionOrClosedMessage;
@@ -1448,7 +1447,6 @@
       });
 
       await this.commit();
-
     },
     getServerId() {
       return this.get('serverId');
@@ -1463,7 +1461,6 @@
       });
 
       await this.commit();
-
     },
     async setServerTimestamp(serverTimestamp) {
       if (_.isEqual(this.get('serverTimestamp'), serverTimestamp)) {
@@ -1475,7 +1472,6 @@
       });
 
       await this.commit();
-
     },
     async setIsPublic(isPublic) {
       if (_.isEqual(this.get('isPublic'), isPublic)) {
@@ -1487,7 +1483,6 @@
       });
 
       await this.commit();
-
     },
 
     async sendSyncMessageOnly(dataMessage) {
@@ -1499,7 +1494,6 @@
 
       await this.commit();
 
-
       const data =
         dataMessage instanceof libsession.Messages.Outgoing.DataMessage
           ? dataMessage.dataProto()
@@ -1531,7 +1525,6 @@
 
       this.set({ sentSync: true });
       await this.commit();
-
     },
 
     someRecipientsFailed() {
@@ -1613,8 +1606,6 @@
         forceSave,
         Message: Whisper.Message,
       });
-      console.warn('case commit')
-
       this.trigger('change');
       return id;
     },
diff --git a/js/modules/debug.js b/js/modules/debug.js
index 7d6bee2c7..f91a5dd47 100644
--- a/js/modules/debug.js
+++ b/js/modules/debug.js
@@ -1,5 +1,5 @@
 /* eslint-env node */
-/* global log, Signal, Whisper */
+/* global log */
 
 const fs = require('fs-extra');
 const path = require('path');
diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js
index eac946c70..e11a073f8 100644
--- a/js/views/conversation_view.js
+++ b/js/views/conversation_view.js
@@ -8,7 +8,6 @@
   textsecure,
   Whisper,
   ConversationController,
-  BlockedNumberController,
 */
 
 // eslint-disable-next-line func-names
@@ -1051,45 +1050,6 @@
         toastOptions.title = i18n('expiredWarning');
         toastOptions.id = 'expiredWarning';
       }
-      if (!window.clientClockSynced) {
-        let clockSynced = false;
-        if (window.setClockParams) {
-          // Check to see if user has updated their clock to current time
-          clockSynced = await window.setClockParams();
-        } else {
-          window.log.info('setClockParams not loaded yet');
-        }
-        if (clockSynced) {
-          toastOptions.title = i18n('clockOutOfSync');
-          toastOptions.id = 'clockOutOfSync';
-        }
-      }
-      if (
-        this.model.isPrivate() &&
-        BlockedNumberController.isBlocked(this.model.id)
-      ) {
-        toastOptions.title = i18n('unblockToSend');
-        toastOptions.id = 'unblockToSend';
-      }
-      if (
-        !this.model.isPrivate() &&
-        BlockedNumberController.isGroupBlocked(this.model.id)
-      ) {
-        toastOptions.title = i18n('unblockGroupToSend');
-        toastOptions.id = 'unblockGroupToSend';
-      }
-      if (!this.model.isPrivate() && this.model.get('left')) {
-        toastOptions.title = i18n('youLeftTheGroup');
-        toastOptions.id = 'youLeftTheGroup';
-      }
-      if (
-        message.length >
-        window.libsession.Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH
-      ) {
-        toastOptions.title = i18n('messageBodyTooLong');
-        toastOptions.id = 'messageBodyTooLong';
-      }
-
       if (toastOptions.title) {
         window.pushToast(toastOptions);
         this.focusMessageFieldAndClearDisabled();
diff --git a/ts/components/session/conversation/SessionCompositionBox.tsx b/ts/components/session/conversation/SessionCompositionBox.tsx
index db3fd2d5c..ec33d27bc 100644
--- a/ts/components/session/conversation/SessionCompositionBox.tsx
+++ b/ts/components/session/conversation/SessionCompositionBox.tsx
@@ -59,6 +59,10 @@ interface Props {
 
   onLoadVoiceNoteView: any;
   onExitVoiceNoteView: any;
+  isBlocked: boolean;
+  isPrivate: boolean;
+  isKickedFromGroup: boolean;
+  leftGroup: boolean;
 
   quotedMessageProps?: ReplyingToMessageProps;
   removeQuotedMessage: () => void;
@@ -206,16 +210,35 @@ export class SessionCompositionBox extends React.Component<Props, State> {
   }
 
   private renderCompositionView() {
-    const { placeholder } = this.props;
+    const {
+      placeholder,
+      isBlocked,
+      isKickedFromGroup,
+      leftGroup,
+      isPrivate,
+    } = this.props;
     const { showEmojiPanel, message } = this.state;
+    const typingEnabled = !(isBlocked || isKickedFromGroup || leftGroup);
+    const { i18n } = window;
+    const messageWithWarning = isKickedFromGroup
+      ? i18n('youGotKickedFromGroup')
+      : leftGroup
+      ? i18n('youLeftTheGroup')
+      : isBlocked && isPrivate
+      ? i18n('unblockToSend')
+      : isBlocked && !isPrivate
+      ? i18n('unblockGroupToSend')
+      : undefined;
 
     return (
       <>
-        <SessionIconButton
-          iconType={SessionIconType.CirclePlus}
-          iconSize={SessionIconSize.Large}
-          onClick={this.onChooseAttachment}
-        />
+        {typingEnabled && (
+          <SessionIconButton
+            iconType={SessionIconType.CirclePlus}
+            iconSize={SessionIconSize.Large}
+            onClick={this.onChooseAttachment}
+          />
+        )}
 
         <input
           className="hidden"
@@ -226,11 +249,13 @@ export class SessionCompositionBox extends React.Component<Props, State> {
           onChange={this.onChoseAttachment}
         />
 
-        <SessionIconButton
-          iconType={SessionIconType.Microphone}
-          iconSize={SessionIconSize.Huge}
-          onClick={this.onLoadVoiceNoteView}
-        />
+        {typingEnabled && (
+          <SessionIconButton
+            iconType={SessionIconType.Microphone}
+            iconSize={SessionIconSize.Huge}
+            onClick={this.onLoadVoiceNoteView}
+          />
+        )}
 
         <div
           className="send-message-input"
@@ -245,16 +270,19 @@ export class SessionCompositionBox extends React.Component<Props, State> {
             placeholder={placeholder}
             maxLength={Constants.CONVERSATION.MAX_MESSAGE_BODY_LENGTH}
             onKeyDown={this.onKeyDown}
-            value={message}
+            value={messageWithWarning || message}
             onChange={this.onChange}
+            disabled={!typingEnabled}
           />
         </div>
 
-        <SessionIconButton
-          iconType={SessionIconType.Emoji}
-          iconSize={SessionIconSize.Large}
-          onClick={this.toggleEmojiPanel}
-        />
+        {typingEnabled && (
+          <SessionIconButton
+            iconType={SessionIconType.Emoji}
+            iconSize={SessionIconSize.Large}
+            onClick={this.toggleEmojiPanel}
+          />
+        )}
         <div className="send-message-button">
           <SessionIconButton
             iconType={SessionIconType.Send}
@@ -264,18 +292,20 @@ export class SessionCompositionBox extends React.Component<Props, State> {
           />
         </div>
 
-        <div
-          ref={ref => (this.emojiPanel = ref)}
-          onKeyDown={this.onKeyDown}
-          role="button"
-        >
-          {showEmojiPanel && (
-            <SessionEmojiPanel
-              onEmojiClicked={this.onEmojiClick}
-              show={showEmojiPanel}
-            />
-          )}
-        </div>
+        {typingEnabled && (
+          <div
+            ref={ref => (this.emojiPanel = ref)}
+            onKeyDown={this.onKeyDown}
+            role="button"
+          >
+            {showEmojiPanel && (
+              <SessionEmojiPanel
+                onEmojiClicked={this.onEmojiClick}
+                show={showEmojiPanel}
+              />
+            )}
+          </div>
+        )}
       </>
     );
   }
@@ -471,9 +501,19 @@ export class SessionCompositionBox extends React.Component<Props, State> {
     }, '');
   }
 
+  // tslint:disable-next-line: cyclomatic-complexity
   private async onSendMessage() {
     const messagePlaintext = this.parseEmojis(this.state.message);
 
+    const { isBlocked, isPrivate, leftGroup, isKickedFromGroup } = this.props;
+    if (isBlocked && isPrivate) {
+      ToastUtils.pushUnblockToSend();
+      return;
+    }
+    if (isBlocked && !isPrivate) {
+      ToastUtils.pushUnblockToSendGroup();
+      return;
+    }
     // Verify message length
     const msgLen = messagePlaintext?.length || 0;
     if (msgLen > window.CONSTANTS.MAX_MESSAGE_BODY_LENGTH) {
@@ -484,6 +524,29 @@ export class SessionCompositionBox extends React.Component<Props, State> {
       ToastUtils.pushMessageBodyMissing();
       return;
     }
+    if (!window.clientClockSynced) {
+      let clockSynced = false;
+      if (window.setClockParams) {
+        // Check to see if user has updated their clock to current time
+        clockSynced = await window.setClockParams();
+      } else {
+        window.log.info('setClockParams not loaded yet');
+      }
+      if (clockSynced) {
+        ToastUtils.pushClockOutOfSync();
+        return;
+      }
+    }
+
+    if (!isPrivate && leftGroup) {
+      ToastUtils.pushYouLeftTheGroup();
+      return;
+    }
+    if (!isPrivate && isKickedFromGroup) {
+      ToastUtils.pushYouLeftTheGroup();
+      return;
+    }
+
     const { quotedMessageProps } = this.props;
     const { stagedLinkPreview } = this.state;
 
diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx
index 5a82d61fe..545f43b22 100644
--- a/ts/components/session/conversation/SessionConversation.tsx
+++ b/ts/components/session/conversation/SessionConversation.tsx
@@ -28,6 +28,8 @@ import * as MIME from '../../../types/MIME';
 import { SessionFileDropzone } from './SessionFileDropzone';
 import { ConversationType } from '../../../state/ducks/conversations';
 import { MessageView } from '../../MainViewController';
+import { getMessageById } from '../../../../js/modules/data';
+import { pushUnblockToSend } from '../../../session/utils/Toast';
 
 interface State {
   // Message sending progress
@@ -161,7 +163,6 @@ export class SessionConversation extends React.Component<Props, State> {
     div?.removeEventListener('drop', this.handleDrop);
   }
 
-
   public componentDidMount() {
     // Pause thread to wait for rendering to complete
     setTimeout(() => {
@@ -253,7 +254,12 @@ export class SessionConversation extends React.Component<Props, State> {
           </div>
 
           {!isRss && (
+            // tslint:disable-next-line: use-simple-attributes
             <SessionCompositionBox
+              isBlocked={conversation.isBlocked}
+              leftGroup={conversation.leftGroup}
+              isKickedFromGroup={conversation.isKickedFromGroup}
+              isPrivate={conversation.type === 'direct'}
               sendMessage={sendMessageFn}
               stagedAttachments={stagedAttachments}
               onMessageSending={this.onMessageSending}
@@ -689,6 +695,10 @@ export class SessionConversation extends React.Component<Props, State> {
   // ~~~~~~~~~~~~~~ MESSAGE QUOTE ~~~~~~~~~~~~~~~
   // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   private async replyToMessage(quotedMessageTimestamp?: number) {
+    if (this.props.conversation.isBlocked) {
+      pushUnblockToSend();
+      return;
+    }
     if (!_.isEqual(this.state.quotedMessageTimestamp, quotedMessageTimestamp)) {
       const { messages, conversationKey } = this.props;
       const conversationModel = window.ConversationController.getOrThrow(
@@ -697,7 +707,9 @@ export class SessionConversation extends React.Component<Props, State> {
 
       let quotedMessageProps = null;
       if (quotedMessageTimestamp) {
-        const quotedMessage = messages.find(m => m.attributes.sent_at === quotedMessageTimestamp);
+        const quotedMessage = messages.find(
+          m => m.attributes.sent_at === quotedMessageTimestamp
+        );
 
         if (quotedMessage) {
           const quotedMessageModel = await getMessageById(quotedMessage.id, {
diff --git a/ts/components/session/conversation/SessionConversationMessagesList.tsx b/ts/components/session/conversation/SessionConversationMessagesList.tsx
index 359523be4..7ca18480a 100644
--- a/ts/components/session/conversation/SessionConversationMessagesList.tsx
+++ b/ts/components/session/conversation/SessionConversationMessagesList.tsx
@@ -144,6 +144,7 @@ export class SessionConversationMessagesList extends React.Component<
     if (conversation.unreadCount === 0) {
       findFirstUnreadIndex = -1;
     }
+    const isConvoBlocked = conversation.isBlocked;
 
     return (
       <>
diff --git a/ts/session/utils/Toast.tsx b/ts/session/utils/Toast.tsx
index aaac6bc72..e7d58810b 100644
--- a/ts/session/utils/Toast.tsx
+++ b/ts/session/utils/Toast.tsx
@@ -211,3 +211,19 @@ export function pushPairingRequestReceived(alreadyLinked: boolean) {
     );
   }
 }
+
+export function pushUnblockToSend() {
+  pushToastInfo('unblockToSend', window.i18n('unblockToSend'));
+}
+
+export function pushUnblockToSendGroup() {
+  pushToastInfo('unblockGroupToSend', window.i18n('unblockGroupToSend'));
+}
+
+export function pushClockOutOfSync() {
+  pushToastError('clockOutOfSync', window.i18n('clockOutOfSync'));
+}
+
+export function pushYouLeftTheGroup() {
+  pushToastError('youLeftTheGroup', window.i18n('youLeftTheGroup'));
+}
diff --git a/ts/state/ducks/conversations.ts b/ts/state/ducks/conversations.ts
index 7a503f1ac..d2670b4d3 100644
--- a/ts/state/ducks/conversations.ts
+++ b/ts/state/ducks/conversations.ts
@@ -56,6 +56,8 @@ export type ConversationType = {
   isSecondary?: boolean;
   primaryDevice: string;
   isBlocked: boolean;
+  isKickedFromGroup: boolean;
+  leftGroup: boolean;
 };
 export type ConversationLookupType = {
   [key: string]: ConversationType;
diff --git a/ts/state/ducks/messages.ts b/ts/state/ducks/messages.ts
index dbc7afc35..126678279 100644
--- a/ts/state/ducks/messages.ts
+++ b/ts/state/ducks/messages.ts
@@ -8,7 +8,7 @@ export type MessagesStateType = Array<MessageType>;
 export async function getMessages(
   conversationKey: string,
   numMessages: number
-) : Promise<MessagesStateType> {
+): Promise<MessagesStateType> {
   const conversation = window.ConversationController.get(conversationKey);
   if (!conversation) {
     // no valid conversation, early return
@@ -88,12 +88,16 @@ const messageSlice = createSlice({
   name: 'messages',
   initialState: [] as MessagesStateType,
   reducers: {
-    messageChanged(state, action){
-      console.log('message changed ', state, action)
-      const messageInStoreIndex = state.findIndex(m => m.id === action.payload.id);
+    messageChanged(state, action) {
+      // console.log('message changed ', action);
+      const messageInStoreIndex = state.findIndex(
+        m => m.id === action.payload.id
+      );
       if (messageInStoreIndex >= 0) {
-        state[messageInStoreIndex] = _.omit(action.payload, toOmitFromMessageModel)
-        ;
+        state[messageInStoreIndex] = _.omit(
+          action.payload,
+          toOmitFromMessageModel
+        );
       }
       return state;
     },
diff --git a/ts/test/state/selectors/conversations_test.ts b/ts/test/state/selectors/conversations_test.ts
index d29c91ae0..1c35296eb 100644
--- a/ts/test/state/selectors/conversations_test.ts
+++ b/ts/test/state/selectors/conversations_test.ts
@@ -30,6 +30,8 @@ describe('state/selectors/conversations', () => {
           isSelected: false,
           isTyping: false,
           isBlocked: false,
+          isKickedFromGroup: false,
+          leftGroup: false,
         },
         id2: {
           id: 'id2',
@@ -49,6 +51,8 @@ describe('state/selectors/conversations', () => {
           isSelected: false,
           isTyping: false,
           isBlocked: false,
+          isKickedFromGroup: false,
+          leftGroup: false,
         },
         id3: {
           id: 'id3',
@@ -68,6 +72,8 @@ describe('state/selectors/conversations', () => {
           isSelected: false,
           isTyping: false,
           isBlocked: false,
+          isKickedFromGroup: false,
+          leftGroup: false,
         },
         id4: {
           id: 'id4',
@@ -78,7 +84,6 @@ describe('state/selectors/conversations', () => {
           isArchived: false,
           isSecondary: false,
           primaryDevice: 'id4',
-
           type: 'direct',
           isMe: false,
           lastUpdated: Date.now(),
@@ -87,6 +92,8 @@ describe('state/selectors/conversations', () => {
           isSelected: false,
           isTyping: false,
           isBlocked: false,
+          isKickedFromGroup: false,
+          leftGroup: false,
         },
         id5: {
           id: 'id5',
@@ -106,6 +113,8 @@ describe('state/selectors/conversations', () => {
           isSelected: false,
           isTyping: false,
           isBlocked: false,
+          isKickedFromGroup: false,
+          leftGroup: false,
         },
       };
       const comparator = _getConversationComparator(i18n, regionCode);
diff --git a/ts/window.d.ts b/ts/window.d.ts
index a5ee275ec..97b78d4c0 100644
--- a/ts/window.d.ts
+++ b/ts/window.d.ts
@@ -109,5 +109,7 @@ declare global {
     sessionGenerateKeyPair: (
       seed: ArrayBuffer
     ) => Promise<{ pubKey: ArrayBufferLike; privKey: ArrayBufferLike }>;
+    setClockParams: any;
+    clientClockSynced: number | undefined;
   }
 }