diff --git a/js/background.js b/js/background.js
index de74ed7e8..081ce55bc 100644
--- a/js/background.js
+++ b/js/background.js
@@ -601,7 +601,7 @@
       }
     });
 
-    Whisper.events.on('leaveGroup', async groupConvo => {
+    Whisper.events.on('leaveClosedGroup', async groupConvo => {
       if (appView) {
         appView.showLeaveGroupDialog(groupConvo);
       }
diff --git a/js/views/admin_leave_closed_group_dialog_view.js b/js/views/admin_leave_closed_group_dialog_view.js
index 00f9a14e5..b1c0e337a 100644
--- a/js/views/admin_leave_closed_group_dialog_view.js
+++ b/js/views/admin_leave_closed_group_dialog_view.js
@@ -37,7 +37,7 @@
       this.remove();
     },
     submit() {
-      this.convo.leaveGroup();
+      this.convo.leaveClosedGroup();
     },
   });
 })();
diff --git a/js/views/app_view.js b/js/views/app_view.js
index 0464dfe84..2f4934475 100644
--- a/js/views/app_view.js
+++ b/js/views/app_view.js
@@ -178,7 +178,7 @@
         window.confirmationDialog({
           title,
           message,
-          resolve: () => groupConvo.leaveGroup(),
+          resolve: () => groupConvo.leaveClosedGroup(),
           theme: this.getThemeObject(),
         });
       } else {
diff --git a/test/models/conversations_test.js b/test/models/conversations_test.js
index 203c47749..b71c148a7 100644
--- a/test/models/conversations_test.js
+++ b/test/models/conversations_test.js
@@ -61,7 +61,7 @@ describe('ConversationCollection', () => {
   //       type: 'group',
   //       id: '052d11d01e56d9bfc3d74115c33225a632321b509ac17a13fdeac71165d09b94ab',
   //     });
-  //     await convo.leaveGroup();
+  //     await convo.leaveClosedGroup();
   //     assert.notEqual(convo.messageCollection.length, 0);
   //   });
   //   it('has a title', () => {
diff --git a/ts/components/session/conversation/SessionConversation.tsx b/ts/components/session/conversation/SessionConversation.tsx
index 5bc87a1ec..fcd1ba1f6 100644
--- a/ts/components/session/conversation/SessionConversation.tsx
+++ b/ts/components/session/conversation/SessionConversation.tsx
@@ -384,7 +384,7 @@ export class SessionConversation extends React.Component<Props, State> {
         conversation.copyPublicKey();
       },
       onLeaveGroup: () => {
-        window.Whisper.events.trigger('leaveGroup', conversation);
+        window.Whisper.events.trigger('leaveClosedGroup', conversation);
       },
       onInviteContacts: () => {
         window.Whisper.events.trigger('inviteContacts', conversation);
@@ -492,8 +492,9 @@ export class SessionConversation extends React.Component<Props, State> {
       onInviteContacts: () => {
         window.Whisper.events.trigger('inviteContacts', conversation);
       },
+      onDeleteContact: conversation.deleteContact,
       onLeaveGroup: () => {
-        window.Whisper.events.trigger('leaveGroup', conversation);
+        window.Whisper.events.trigger('leaveClosedGroup', conversation);
       },
       onAddModerators: () => {
         window.Whisper.events.trigger('addModerators', conversation);
diff --git a/ts/components/session/conversation/SessionRightPanel.tsx b/ts/components/session/conversation/SessionRightPanel.tsx
index 27a883ef7..90be5504b 100644
--- a/ts/components/session/conversation/SessionRightPanel.tsx
+++ b/ts/components/session/conversation/SessionRightPanel.tsx
@@ -39,6 +39,7 @@ interface Props {
   onGoBack: () => void;
   onInviteContacts: () => void;
   onLeaveGroup: () => void;
+  onDeleteContact: () => void;
   onUpdateGroupName: () => void;
   onAddModerators: () => void;
   onRemoveModerators: () => void;
@@ -218,6 +219,7 @@ class SessionRightPanel extends React.Component<Props, State> {
       name,
       timerOptions,
       onLeaveGroup,
+      onDeleteContact,
       isKickedFromGroup,
       left,
       isPublic,
@@ -310,7 +312,7 @@ class SessionRightPanel extends React.Component<Props, State> {
             buttonColor={SessionButtonColor.Danger}
             disabled={isKickedFromGroup || left}
             buttonType={SessionButtonType.SquareOutline}
-            onClick={onLeaveGroup}
+            onClick={isPublic ? onDeleteContact : onLeaveGroup}
           />
         )}
       </div>
diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts
index 2b78ad2fa..b8c9b5be4 100644
--- a/ts/models/conversation.ts
+++ b/ts/models/conversation.ts
@@ -419,7 +419,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
       onCopyPublicKey: this.copyPublicKey,
       onDeleteContact: this.deleteContact,
       onLeaveGroup: () => {
-        window.Whisper.events.trigger('leaveGroup', this);
+        window.Whisper.events.trigger('leaveClosedGroup', this);
       },
       onDeleteMessages: this.deleteMessages,
       onInviteContacts: () => {
@@ -953,7 +953,7 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
     return model;
   }
 
-  public async leaveGroup() {
+  public async leaveClosedGroup() {
     if (this.isMediumGroup()) {
       await leaveClosedGroup(this.id);
     } else {
diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts
index edcfb7ed1..a5b5db48e 100644
--- a/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts
+++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2.ts
@@ -419,7 +419,9 @@ export const getAllRoomInfos = async (roomInfos: OpenGroupV2Room) => {
   return parseRooms(result);
 };
 
-export const getMemberCount = async (roomInfos: OpenGroupRequestCommonType): Promise<void> => {
+export const getMemberCount = async (
+  roomInfos: OpenGroupRequestCommonType
+): Promise<number | undefined> => {
   const request: OpenGroupV2Request = {
     method: 'GET',
     room: roomInfos.roomId,
@@ -438,18 +440,7 @@ export const getMemberCount = async (roomInfos: OpenGroupRequestCommonType): Pro
     return;
   }
 
-  const conversationId = getOpenGroupV2ConversationId(roomInfos.serverUrl, roomInfos.roomId);
-
-  const convo = ConversationController.getInstance().get(conversationId);
-  if (!convo) {
-    window.log.warn('cannot update conversation memberCount as it does not exist');
-    return;
-  }
-  if (convo.get('subscriberCount') !== count) {
-    convo.set({ subscriberCount: count });
-    // triggers the save to db and the refresh of the UI
-    await convo.commit();
-  }
+  return count;
 };
 
 /**
diff --git a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts
index bb59a51de..8a353a6f0 100644
--- a/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts
+++ b/ts/opengroup/opengroupV2/OpenGroupAPIV2CompactPoll.ts
@@ -8,7 +8,7 @@ import { parseStatusCodeFromOnionRequest } from './OpenGroupAPIV2Parser';
 import _ from 'lodash';
 import { sendViaOnion } from '../../session/onions/onionSend';
 import { OpenGroupMessageV2 } from './OpenGroupMessageV2';
-import { downloadPreviewOpenGroupV2, getAuthToken } from './OpenGroupAPIV2';
+import { downloadPreviewOpenGroupV2, getAuthToken, getMemberCount } from './OpenGroupAPIV2';
 
 const COMPACT_POLL_ENDPOINT = 'compact_poll';
 
@@ -72,6 +72,50 @@ export const getAllBase64AvatarForRooms = async (
   return validPreviewBase64 ? validPreviewBase64 : null;
 };
 
+export const getAllMemberCount = async (
+  serverUrl: string,
+  rooms: Set<string>,
+  abortSignal: AbortSignal
+): Promise<Array<ParsedMemberCount> | null> => {
+  // fetch all we need
+  const allValidRoomInfos = await getAllValidRoomInfos(serverUrl, rooms);
+  if (!allValidRoomInfos?.length) {
+    window.log.info('getAllMemberCount: no valid roominfos got.');
+    return null;
+  }
+  if (abortSignal.aborted) {
+    window.log.info('memberCount aborted, returning null');
+    return null;
+  }
+  // Currently this call will not abort if AbortSignal is aborted,
+  // but the call will return null.
+  const validMemberCount = _.compact(
+    await Promise.all(
+      allValidRoomInfos.map(async room => {
+        try {
+          const memberCount = await getMemberCount(room);
+          if (memberCount !== undefined) {
+            return {
+              roomId: room.roomId,
+              memberCount,
+            };
+          }
+        } catch (e) {
+          window.log.warn('getPreview failed for room', room);
+        }
+        return null;
+      })
+    )
+  );
+
+  if (abortSignal.aborted) {
+    window.log.info('getMemberCount aborted, returning null');
+    return null;
+  }
+
+  return validMemberCount ? validMemberCount : null;
+};
+
 /**
  * This function fetches the valid roomInfos from the database.
  * It also makes sure that the pubkey for all those rooms are the same, or returns null.
@@ -258,6 +302,11 @@ export type ParsedBase64Avatar = {
   base64: string;
 };
 
+export type ParsedMemberCount = {
+  roomId: string;
+  memberCount: number;
+};
+
 const parseCompactPollResult = async (
   singleRoomResult: any,
   serverUrl: string
diff --git a/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts b/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts
index a063e7f15..3b5153212 100644
--- a/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts
+++ b/ts/opengroup/opengroupV2/OpenGroupServerPoller.ts
@@ -5,8 +5,10 @@ import { OpenGroupRequestCommonType } from './ApiUtil';
 import {
   compactFetchEverything,
   getAllBase64AvatarForRooms,
+  getAllMemberCount,
   ParsedBase64Avatar,
   ParsedDeletions,
+  ParsedMemberCount,
   ParsedRoomCompactPollResults,
 } from './OpenGroupAPIV2CompactPoll';
 import _ from 'lodash';
@@ -15,13 +17,14 @@ import { getMessageIdsFromServerIds, removeMessage } from '../../data/data';
 import { getV2OpenGroupRoom, saveV2OpenGroupRoom } from '../../data/opengroups';
 import { OpenGroupMessageV2 } from './OpenGroupMessageV2';
 import { handleOpenGroupV2Message } from '../../receiver/receiver';
-import { DAYS, SECONDS } from '../../session/utils/Number';
+import { DAYS, MINUTES, SECONDS } from '../../session/utils/Number';
 import autoBind from 'auto-bind';
 import { sha256 } from '../../session/crypto';
 import { fromBase64ToArrayBuffer } from '../../session/utils/String';
 
 const pollForEverythingInterval = SECONDS * 6;
 const pollForRoomAvatarInterval = DAYS * 1;
+const pollForMemberCountInterval = MINUTES * 10;
 
 /**
  * An OpenGroupServerPollerV2 polls for everything for a particular server. We should
@@ -31,10 +34,26 @@ const pollForRoomAvatarInterval = DAYS * 1;
  * for this server.
  */
 export class OpenGroupServerPoller {
+  /**
+   * The server url to poll for this opengroup poller.
+   * Remember, we have one poller per opengroup poller, no matter how many rooms we have joined on this same server
+   */
   private readonly serverUrl: string;
+
+  /**
+   * The set of rooms to poll from.
+   *
+   */
   private readonly roomIdsToPoll: Set<string> = new Set();
+
+  /**
+   * This timer is used to tick for compact Polling for this opengroup server
+   * It ticks every `pollForEverythingInterval` except.
+   * If the last run is still in progress, the new one won't start and just return.
+   */
   private pollForEverythingTimer?: NodeJS.Timeout;
   private pollForRoomAvatarTimer?: NodeJS.Timeout;
+  private pollForMemberCountTimer?: NodeJS.Timeout;
   private readonly abortController: AbortController;
 
   /**
@@ -45,6 +64,7 @@ export class OpenGroupServerPoller {
    */
   private isPolling = false;
   private isPreviewPolling = false;
+  private isMemberCountPolling = false;
   private wasStopped = false;
 
   constructor(roomInfos: Array<OpenGroupRequestCommonType>) {
@@ -71,6 +91,10 @@ export class OpenGroupServerPoller {
       this.previewPerRoomPoll,
       pollForRoomAvatarInterval
     );
+    this.pollForMemberCountTimer = global.setInterval(
+      this.pollForAllMemberCount,
+      pollForMemberCountInterval
+    );
   }
 
   /**
@@ -90,6 +114,7 @@ export class OpenGroupServerPoller {
     // if we are not already polling right now, trigger a polling
     void this.compactPoll();
     void this.previewPerRoomPoll();
+    void this.pollForAllMemberCount();
   }
 
   public removeRoomFromPoll(room: OpenGroupRequestCommonType) {
@@ -120,6 +145,10 @@ export class OpenGroupServerPoller {
     if (this.pollForRoomAvatarTimer) {
       global.clearInterval(this.pollForRoomAvatarTimer);
     }
+
+    if (this.pollForMemberCountTimer) {
+      global.clearInterval(this.pollForMemberCountTimer);
+    }
     if (this.pollForEverythingTimer) {
       // cancel next ticks for each timer
       global.clearInterval(this.pollForEverythingTimer);
@@ -128,6 +157,7 @@ export class OpenGroupServerPoller {
       this.abortController?.abort();
       this.pollForEverythingTimer = undefined;
       this.pollForRoomAvatarTimer = undefined;
+      this.pollForMemberCountTimer = undefined;
       this.wasStopped = true;
     }
   }
@@ -162,6 +192,21 @@ export class OpenGroupServerPoller {
     return true;
   }
 
+  private shouldPollForMemberCount() {
+    if (this.wasStopped) {
+      window.log.error('Serverpoller was stopped. PolLForMemberCount should not happen');
+      return false;
+    }
+    if (!this.roomIdsToPoll.size) {
+      return false;
+    }
+    // return early if a poll is already in progress
+    if (this.isMemberCountPolling) {
+      return false;
+    }
+    return true;
+  }
+
   private async previewPerRoomPoll() {
     if (!this.shouldPollPreview()) {
       return;
@@ -201,6 +246,46 @@ export class OpenGroupServerPoller {
     }
   }
 
+  private async pollForAllMemberCount() {
+    if (!this.shouldPollForMemberCount()) {
+      return;
+    }
+    // do everything with throwing so we can check only at one place
+    // what we have to clean
+    try {
+      this.isMemberCountPolling = true;
+      // don't try to make the request if we are aborted
+      if (this.abortController.signal.aborted) {
+        throw new Error('Poller aborted');
+      }
+
+      let memberCountGotResults = await getAllMemberCount(
+        this.serverUrl,
+        this.roomIdsToPoll,
+        this.abortController.signal
+      );
+
+      // check that we are still not aborted
+      if (this.abortController.signal.aborted) {
+        throw new Error('Abort controller was canceled. Dropping memberCount request');
+      }
+      if (!memberCountGotResults) {
+        throw new Error('MemberCount: no results');
+      }
+      // we were not aborted, make sure to filter out roomIds we are not polling for anymore
+      memberCountGotResults = memberCountGotResults.filter(result =>
+        this.roomIdsToPoll.has(result.roomId)
+      );
+
+      // ==> At this point all those results need to trigger conversation updates, so update what we have to update
+      await handleAllMemberCount(this.serverUrl, memberCountGotResults);
+    } catch (e) {
+      window.log.warn('Got error while memberCount fetch:', e);
+    } finally {
+      this.isMemberCountPolling = false;
+    }
+  }
+
   private async compactPoll() {
     if (!this.shouldPoll()) {
       return;
@@ -396,3 +481,29 @@ const handleBase64AvatarUpdate = async (
     })
   );
 };
+
+async function handleAllMemberCount(
+  serverUrl: string,
+  memberCountGotResults: Array<ParsedMemberCount>
+) {
+  if (!memberCountGotResults.length) {
+    return;
+  }
+
+  await Promise.all(
+    memberCountGotResults.map(async roomCount => {
+      const conversationId = getOpenGroupV2ConversationId(serverUrl, roomCount.roomId);
+
+      const convo = ConversationController.getInstance().get(conversationId);
+      if (!convo) {
+        window.log.warn('cannot update conversation memberCount as it does not exist');
+        return;
+      }
+      if (convo.get('subscriberCount') !== roomCount.memberCount) {
+        convo.set({ subscriberCount: roomCount.memberCount });
+        // triggers the save to db and the refresh of the UI
+        await convo.commit();
+      }
+    })
+  );
+}
diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts
index 8cfe27909..654b2568e 100644
--- a/ts/session/conversations/ConversationController.ts
+++ b/ts/session/conversations/ConversationController.ts
@@ -189,7 +189,7 @@ export class ConversationController {
 
     // Close group leaving
     if (conversation.isClosedGroup()) {
-      await conversation.leaveGroup();
+      await conversation.leaveClosedGroup();
     } else if (conversation.isPublic() && !conversation.isOpenGroupV2()) {
       const channelAPI = await conversation.getPublicSendData();
       if (channelAPI === null) {
diff --git a/ts/session/snode_api/onions.ts b/ts/session/snode_api/onions.ts
index f35667b51..7b1abeba2 100644
--- a/ts/session/snode_api/onions.ts
+++ b/ts/session/snode_api/onions.ts
@@ -439,6 +439,7 @@ const sendOnionRequest = async (
   const guardUrl = `https://${nodePath[0].ip}:${nodePath[0].port}${target}`;
   // no logs for that one as we do need to call insecureNodeFetch to our guardNode
   // window.log.info('insecureNodeFetch => plaintext for sendOnionRequest');
+  console.warn('sendViaOnion payload: ', payload.length);
 
   const response = await insecureNodeFetch(guardUrl, guardFetchOptions);
   return processOnionResponse(reqIdx, response, destCtx.symmetricKey, false, abortSignal);