mainview: make all context menus generated at run time with the same style

By giving a base context menu, all context menus are generated at run time
and kept the same style. Some issues are fixed along with the patch.

Gitlab: #8
Gitlab: #35
Change-Id: Ieb812420fcb44c33d161a62c8574f6705dc5e1a9
diff --git a/src/commoncomponents/BaseContextMenu.qml b/src/commoncomponents/BaseContextMenu.qml
new file mode 100644
index 0000000..05227a5
--- /dev/null
+++ b/src/commoncomponents/BaseContextMenu.qml
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.14
+import QtQuick.Controls 2.14
+import QtGraphicalEffects 1.12
+import net.jami.Models 1.0
+
+Menu {
+    id: root
+
+    property int menuItemsPreferredWidth: 220
+    property int menuItemsPreferredHeight: 48
+    property int generalMenuSeparatorCount: 0
+    property int commonBorderWidth: 1
+    font.pointSize: JamiTheme.menuFontSize
+
+    function openMenu(){
+        visible = true
+        visible = false
+        visible = true
+    }
+
+    background: Rectangle {
+        implicitWidth: menuItemsPreferredWidth
+        implicitHeight: menuItemsPreferredHeight
+                        * (root.count - generalMenuSeparatorCount)
+
+        border.width: commonBorderWidth
+        border.color: JamiTheme.tabbarBorderColor
+    }
+}
diff --git a/src/commoncomponents/GeneralMenuItem.qml b/src/commoncomponents/GeneralMenuItem.qml
index a457f8d..199f59d 100644
--- a/src/commoncomponents/GeneralMenuItem.qml
+++ b/src/commoncomponents/GeneralMenuItem.qml
@@ -35,8 +35,7 @@
     property string iconSource: ""
     property int preferredWidth: 220
     property int preferredHeight: 48
-    property int topBorderWidth: 0
-    property int bottomBorderWidth: 0
+
     property int leftBorderWidth: 0
     property int rightBorderWidth: 0
 
@@ -99,8 +98,6 @@
         id: contextMenuBackgroundRect
 
         anchors.fill: parent
-        anchors.topMargin: topBorderWidth
-        anchors.bottomMargin: bottomBorderWidth
         anchors.leftMargin: leftBorderWidth
         anchors.rightMargin: rightBorderWidth
 
@@ -132,8 +129,8 @@
             commonBorder: false
             lBorderwidth: leftBorderWidth
             rBorderwidth: rightBorderWidth
-            tBorderwidth: topBorderWidth
-            bBorderwidth: bottomBorderWidth
+            tBorderwidth: 0
+            bBorderwidth: 0
             borderColor: JamiTheme.tabbarBorderColor
         }
     }
diff --git a/src/commoncomponents/js/contextmenugenerator.js b/src/commoncomponents/js/contextmenugenerator.js
new file mode 100644
index 0000000..7c55365
--- /dev/null
+++ b/src/commoncomponents/js/contextmenugenerator.js
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2020 by Savoir-faire Linux
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+// Global base context menu, object variable for creation.
+var baseContextMenuComponent
+var baseContextMenuObject
+var menuItemList = []
+
+function createBaseContextMenuObjects(parent) {
+    // If already created, return, since object cannot be destroyed.
+    if (baseContextMenuObject)
+        return
+
+    baseContextMenuComponent = Qt.createComponent(
+                "../BaseContextMenu.qml")
+    if (baseContextMenuComponent.status === Component.Ready)
+        finishCreation(parent)
+    else if (baseContextMenuComponent.status === Component.Error)
+        console.log("Error loading component:",
+                    baseContextMenuComponent.errorString())
+}
+
+function finishCreation(parent) {
+    baseContextMenuObject = baseContextMenuComponent.createObject(parent)
+    if (baseContextMenuObject === null) {
+        // Error Handling.
+        console.log("Error creating object for base context menu")
+    }
+
+    baseContextMenuObject.closed.connect(function (){
+        // Remove the menu items when hidden.
+        for (var i = 0; i < menuItemList.length; i++) {
+            baseContextMenuObject.removeItem(menuItemList[i])
+        }
+    })
+
+    baseContextMenuObject.aboutToShow.connect(function (){
+        // Add default separator at the bottom.
+        addMenuSeparator(8, "transparent")
+    })
+}
+
+function addMenuSeparator(separatorHeight, separatorColor) {
+    var menuSeparatorObject
+    var menuSeparatorComponent = Qt.createComponent("../GeneralMenuSeparator.qml");
+    if (menuSeparatorComponent.status === Component.Ready) {
+        baseContextMenuObject.generalMenuSeparatorCount ++
+        menuSeparatorObject = menuSeparatorComponent.createObject(null,
+                              {preferredWidth: baseContextMenuObject.menuItemsPreferredWidth,
+                               preferredHeight: separatorHeight ? separatorHeight : 1})
+        if (separatorColor)
+            menuSeparatorObject.separatorColor = separatorColor
+    } else if (menuSeparatorComponent.status === Component.Error)
+        console.log("Error loading component:",
+                    menuSeparatorComponent.errorString())
+    if (menuSeparatorObject !== null) {
+        baseContextMenuObject.addItem(menuSeparatorObject)
+
+        menuItemList.push(menuSeparatorObject)
+    } else {
+        // Error handling.
+        console.log("Error creating object")
+    }
+}
+
+function addMenuItem(itemName,
+                     iconSource,
+                     onClickedCallback) {
+    if (!baseContextMenuObject.count){
+        // Add default separator at the top.
+        addMenuSeparator(8, "transparent")
+    }
+
+    var menuItemObject
+    var menuItemComponent = Qt.createComponent("../GeneralMenuItem.qml");
+    if (menuItemComponent.status === Component.Ready) {
+        menuItemObject = menuItemComponent.createObject(null,
+                         {itemName: itemName,
+                          iconSource: iconSource,
+                          leftBorderWidth: baseContextMenuObject.commonBorderWidth,
+                          rightBorderWidth: baseContextMenuObject.commonBorderWidth})
+    } else if (menuItemComponent.status === Component.Error)
+        console.log("Error loading component:",
+                    menuItemComponent.errorString())
+    if (menuItemObject !== null) {
+        menuItemObject.clicked.connect(function () {baseContextMenuObject.close()})
+        menuItemObject.clicked.connect(onClickedCallback)
+        menuItemObject.icon.color = "black"
+
+        baseContextMenuObject.addItem(menuItemObject)
+
+        menuItemList.push(menuItemObject)
+    } else {
+        // Error handling.
+        console.log("Error creating object")
+    }
+}
+
+function getMenu() {
+    return baseContextMenuObject
+}
diff --git a/src/mainview/components/CallOverlayButtonGroup.qml b/src/mainview/components/CallOverlayButtonGroup.qml
index 107ab9e..88a097f 100644
--- a/src/mainview/components/CallOverlayButtonGroup.qml
+++ b/src/mainview/components/CallOverlayButtonGroup.qml
@@ -230,7 +230,7 @@
 
             onClicked: {
                 var rectPos = mapToItem(callStackViewWindow, optionsButton.x, optionsButton.y)
-                callViewContextMenu.activate()
+                callViewContextMenu.openMenu()
                 callViewContextMenu.x = rectPos.x + optionsButton.width/2 - callViewContextMenu.width/2
                 callViewContextMenu.y = rectPos.y - 12 - callViewContextMenu.height
             }
diff --git a/src/mainview/components/CallViewContextMenu.qml b/src/mainview/components/CallViewContextMenu.qml
index 2deeeaa..5b2272a 100644
--- a/src/mainview/components/CallViewContextMenu.qml
+++ b/src/mainview/components/CallViewContextMenu.qml
@@ -17,6 +17,7 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
 import QtQuick 2.14
 import QtQuick.Controls 2.14
 import QtGraphicalEffects 1.12
@@ -24,61 +25,125 @@
 
 import "../../commoncomponents"
 
+import "../../commoncomponents/js/contextmenugenerator.js" as ContextMenuGenerator
 import "../js/videodevicecontextmenuitemcreation.js" as VideoDeviceContextMenuItemCreation
 import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation
+import "../js/screenrubberbandcreation.js" as ScreenRubberBandCreation
 
-Menu {
+Item {
     id: root
 
-    property int generalMenuSeparatorCount: 0
-    property int commonBorderWidth: 1
-
-    signal pluginItemClicked
-
-    font.pointSize: JamiTheme.textFontSize+3
-
     property bool isSIP: false
     property bool isPaused: false
     property bool isAudioOnly: false
     property bool isRecording: false
 
+    signal pluginItemClicked
     signal transferCallButtonClicked
 
-    function activate() {
+    function openMenu(){
+        if (isSIP){
+            ContextMenuGenerator.addMenuItem(isPaused ? qsTr("Resume call") : qsTr("Hold call"),
+                                             isPaused ?
+                                                 "qrc:/images/icons/play_circle_outline-24px.svg" :
+                                                 "qrc:/images/icons/pause_circle_outline-24px.svg",
+                                             function (){
+                                                 CallAdapter.holdThisCallToggle()
+                                             })
+            ContextMenuGenerator.addMenuItem(qsTr("Sip Input Panel"),
+                                             "qrc:/images/icons/ic_keypad.svg",
+                                             function (){
+                                                 sipInputPanel.open()
+                                             })
+            ContextMenuGenerator.addMenuItem(qsTr("Transfer call"),
+                                             "qrc:/images/icons/phone_forwarded-24px.svg",
+                                             function (){
+                                                 root.transferCallButtonClicked()
+                                             })
+
+            ContextMenuGenerator.addMenuSeparator()
+        }
+
+        if (!isAudioOnly) {
+            ContextMenuGenerator.addMenuItem(isRecording ? qsTr("Stop recording") :
+                                                           qsTr("Start recording"),
+                                             "qrc:/images/icons/ic_video_call_24px.svg",
+                                             function (){
+                                                  CallAdapter.recordThisCallToggle()
+                                             })
+            ContextMenuGenerator.addMenuItem(videoCallPage.isFullscreen ? qsTr("Exit full screen") :
+                                                                          qsTr("Full screen mode"),
+                                             videoCallPage.isFullscreen ?
+                                                 "qrc:/images/icons/close_fullscreen-24px.svg" :
+                                                 "qrc:/images/icons/open_in_full-24px.svg",
+                                             function (){
+                                                  videoCallPageRect.needToShowInFullScreen()
+                                             })
+
+            ContextMenuGenerator.addMenuSeparator()
+
+            generateDeviceMenuItem()
+
+            ContextMenuGenerator.addMenuSeparator()
+
+            ContextMenuGenerator.addMenuItem(qsTr("Share entire screen"),
+                                             "qrc:/images/icons/screen_share-24px.svg",
+                                             function (){
+                                                 if (Qt.application.screens.length === 1) {
+                                                     AvAdapter.shareEntireScreen(0)
+                                                 } else {
+                                                     SelectScreenWindowCreation.createSelectScreenWindowObject()
+                                                     SelectScreenWindowCreation.showSelectScreenWindow()
+                                                 }
+                                             })
+            ContextMenuGenerator.addMenuItem(qsTr("Share screen area"),
+                                             "qrc:/images/icons/screen_share-24px.svg",
+                                             function (){
+                                                 if (Qt.application.screens.length === 1) {
+                                                     ScreenRubberBandCreation.createScreenRubberBandWindowObject(
+                                                                 null, 0)
+                                                     ScreenRubberBandCreation.showScreenRubberBandWindow()
+                                                 } else {
+                                                     SelectScreenWindowCreation.createSelectScreenWindowObject(true)
+                                                     SelectScreenWindowCreation.showSelectScreenWindow()
+                                                 }
+                                             })
+            ContextMenuGenerator.addMenuItem(qsTr("Share file"),
+                                             "qrc:/images/icons/insert_photo-24px.svg",
+                                             function (){
+                                                  jamiFileDialog.open()
+                                             })
+        }
+
+        ContextMenuGenerator.addMenuItem(qsTr("Toggle plugin"),
+                                         "qrc:/images/icons/extension_24dp.svg",
+                                         function (){
+                                              root.pluginItemClicked()
+                                         })
+
+        root.height = ContextMenuGenerator.getMenu().height
+        root.width = ContextMenuGenerator.getMenu().width
+        ContextMenuGenerator.getMenu().open()
+    }
+
+    function generateDeviceMenuItem() {
         var deviceContextMenuInfoMap = AvAdapter.populateVideoDeviceContextMenuItem()
+
         /*
          * Somehow, the map size is undefined, so use this instead.
          */
         var mapSize = deviceContextMenuInfoMap["size"]
 
-        var count = 2
+        if (mapSize === 0)
+            VideoDeviceContextMenuItemCreation.createVideoDeviceContextMenuItemObjects(
+                        qsTr("No video device"), false)
+
         for (var deviceName in deviceContextMenuInfoMap) {
-            if (deviceName === "size" || root.isAudioOnly)
+            if (deviceName === "size")
                 continue
-            if (videoDeviceItem.itemName === "No video device") {
-                videoDeviceItem.checkable = true
-                videoDeviceItem.itemName = deviceName
-                videoDeviceItem.checked = deviceContextMenuInfoMap[deviceName]
-                if (count === mapSize)
-                    root.open()
-            } else {
-                VideoDeviceContextMenuItemCreation.createVideoDeviceContextMenuItemObjects(
-                            deviceName, deviceContextMenuInfoMap[deviceName],
-                            count === mapSize)
-            }
-            count++
+            VideoDeviceContextMenuItemCreation.createVideoDeviceContextMenuItemObjects(
+                        deviceName, deviceContextMenuInfoMap[deviceName])
         }
-        root.open()
-    }
-
-    Component.onCompleted: {
-        VideoDeviceContextMenuItemCreation.setVideoContextMenuObject(root)
-    }
-
-
-    onClosed: {
-        videoDeviceItem.itemName = "No video device"
-        VideoDeviceContextMenuItemCreation.removeCreatedItems()
     }
 
     JamiFileDialog {
@@ -92,187 +157,13 @@
         }
     }
 
-    /*
-     * All GeneralMenuItems should remain the same width / height.
-     */
-    GeneralMenuItem {
-        id: holdCallButton
+    Component.onCompleted: {
+        ContextMenuGenerator.createBaseContextMenuObjects(root)
+        VideoDeviceContextMenuItemCreation.setVideoContextMenuObject(ContextMenuGenerator.getMenu())
 
-        visible: isSIP
-        height: isSIP? undefined : 0
-
-        itemName: isPaused? qsTr("Resume call") : qsTr("Hold call")
-        iconSource: isPaused? "qrc:/images/icons/play_circle_outline-24px.svg" : "qrc:/images/icons/pause_circle_outline-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            CallAdapter.holdThisCallToggle()
-            root.close()
-        }
-    }
-
-    GeneralMenuItem {
-        id: showSipInputPanelButton
-
-        visible: isSIP
-        height: isSIP? undefined : 0
-
-        itemName: qsTr("Sip Input Panel")
-        iconSource: "qrc:/images/icons/ic_keypad.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            root.close()
-            sipInputPanel.open()
-        }
-    }
-
-    GeneralMenuItem {
-        id: transferCallButton
-
-        visible: isSIP
-        height: isSIP? undefined : 0
-
-        itemName: qsTr("Transfer call")
-        iconSource: "qrc:/images/icons/phone_forwarded-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            root.transferCallButtonClicked()
-            root.close()
-        }
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startRecordingItem.preferredWidth
-        preferredHeight: commonBorderWidth
-
-        visible: isSIP
-        height: isSIP? undefined : 0
-
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    GeneralMenuItem {
-        id: startRecordingItem
-
-        itemName: isRecording? qsTr("Stop recording") : qsTr("Start recording")
-        iconSource: "qrc:/images/icons/ic_video_call_24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            root.close()
-            CallAdapter.recordThisCallToggle()
-        }
-    }
-
-    GeneralMenuItem {
-        id: fullScreenItem
-
-        itemName: videoCallPage.isFullscreen ? qsTr("Exit full screen") : qsTr(
-                                     "Full screen mode")
-        iconSource: videoCallPage.isFullscreen ? "qrc:/images/icons/close_fullscreen-24px.svg" : "qrc:/images/icons/open_in_full-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            root.close()
-            videoCallPageRect.needToShowInFullScreen()
-        }
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startRecordingItem.preferredWidth
-        preferredHeight: commonBorderWidth
-
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    VideoCallPageContextMenuDeviceItem {
-        id: videoDeviceItem
-        visible: !isAudioOnly
-        height: !isAudioOnly? undefined : 0
-
-        contextMenuPreferredWidth: root.implicitWidth
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startRecordingItem.preferredWidth
-        preferredHeight: commonBorderWidth
-        visible: !isAudioOnly
-        height: !isAudioOnly? undefined : 0
-
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    GeneralMenuItem {
-        id: shareEntireScreenItem
-
-        itemName: qsTr("Share entire screen")
-        iconSource: "qrc:/images/icons/screen_share-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-        visible: !isAudioOnly
-        height: !isAudioOnly? undefined : 0
-
-        onClicked: {
-            root.close()
-            if (Qt.application.screens.length === 1) {
-                AvAdapter.shareEntireScreen(0)
-            } else {
-                SelectScreenWindowCreation.createSelectScreenWindowObject()
-                SelectScreenWindowCreation.showSelectScreenWindow()
-            }
-        }
-    }
-
-    GeneralMenuItem {
-        id: shareScreenAreaItem
-
-        itemName: qsTr("Share screen area")
-        iconSource: "qrc:/images/icons/screen_share-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-        visible: !isAudioOnly
-        height: !isAudioOnly? undefined : 0
-
-        onClicked: {
-            root.close()
-            if (Qt.application.screens.length === 1) {
-                ScreenRubberBandCreation.createScreenRubberBandWindowObject(
-                            null, 0)
-                ScreenRubberBandCreation.showScreenRubberBandWindow()
-            } else {
-                SelectScreenWindowCreation.createSelectScreenWindowObject(true)
-                SelectScreenWindowCreation.showSelectScreenWindow()
-            }
-        }
-    }
-
-    GeneralMenuItem {
-        id: shareFileItem
-
-        itemName: qsTr("Share file")
-        iconSource: "qrc:/images/icons/insert_photo-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-        visible: !isAudioOnly
-        height: !isAudioOnly? undefined : 0
-
-        onClicked: {
-            root.close()
-            jamiFileDialog.open()
-        }
+        ContextMenuGenerator.getMenu().closed.connect(function (){
+            VideoDeviceContextMenuItemCreation.removeCreatedItems()
+        })
     }
 
     /* TODO: In the future we want to implement this
@@ -289,31 +180,5 @@
             root.close()
         }
     }*/
-
-    GeneralMenuItem {
-        id: pluginItem
-
-        itemName: qsTr("Toggle plugin")
-        iconSource: "qrc:/images/icons/extension_24dp.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            root.pluginItemClicked()
-            root.close()
-        }
-    }
-
-    background: Rectangle {
-        implicitWidth: startRecordingItem.preferredWidth
-        implicitHeight: startRecordingItem.preferredHeight
-                        * (root.count
-                          - (isSIP? 0 : 2)
-                          - (isAudioOnly? 6 : 0)
-                          - generalMenuSeparatorCount)
-
-        border.width: commonBorderWidth
-        border.color: JamiTheme.tabbarBorderColor
-    }
 }
 
diff --git a/src/mainview/components/ConversationSmartListContextMenu.qml b/src/mainview/components/ConversationSmartListContextMenu.qml
index 26c5406..c5aa471 100644
--- a/src/mainview/components/ConversationSmartListContextMenu.qml
+++ b/src/mainview/components/ConversationSmartListContextMenu.qml
@@ -1,4 +1,3 @@
-
 /*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
@@ -16,6 +15,7 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
 import QtQuick 2.14
 import QtQuick.Controls 2.14
 import QtGraphicalEffects 1.12
@@ -23,157 +23,88 @@
 
 import "../../commoncomponents"
 
-Menu {
-    id: contextMenu
+import "../../commoncomponents/js/contextmenugenerator.js" as ContextMenuGenerator
+
+Item {
+    id: root
+
     property string responsibleAccountId: ""
     property string responsibleConvUid: ""
-
-    property int generalMenuSeparatorCount: 0
-    property int commonBorderWidth: 1
-    font.pointSize: JamiTheme.menuFontSize
+    property int contactType: Profile.Type.INVALID
 
     function openMenu(){
-        visible = true
-        visible = false
-        visible = true
-    }
+        ContextMenuGenerator.addMenuItem(qsTr("Start video call"),
+                                         "qrc:/images/icons/ic_video_call_24px.svg",
+                                         function (){
+                                             ConversationsAdapter.selectConversation(
+                                                         responsibleAccountId,
+                                                         responsibleConvUid, false)
+                                             CallAdapter.placeCall()
+                                         })
+        ContextMenuGenerator.addMenuItem(qsTr("Start audio call"),
+                                         "qrc:/images/icons/ic_phone_24px.svg",
+                                         function (){
+                                             ConversationsAdapter.selectConversation(
+                                                         responsibleAccountId,
+                                                         responsibleConvUid, false)
+                                             CallAdapter.placeAudioOnlyCall()
+                                         })
+        ContextMenuGenerator.addMenuItem(qsTr("Clear conversation"),
+                                         "qrc:/images/icons/ic_clear_24px.svg",
+                                         function (){
+                                             ClientWrapper.utilsAdaptor.clearConversationHistory(
+                                                         responsibleAccountId,
+                                                         responsibleConvUid)
+                                         })
 
-    GeneralMenuSeparator {
-        preferredWidth: startVideoCallItem.preferredWidth
-        preferredHeight: 8
-        separatorColor: "transparent"
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
+        if (contactType === Profile.Type.RING || contactType === Profile.Type.SIP) {
+            ContextMenuGenerator.addMenuItem(qsTr("Remove contact"),
+                                             "qrc:/images/icons/round-remove_circle-24px.svg",
+                                             function (){
+                                                 ClientWrapper.utilsAdaptor.removeConversation(
+                                                             responsibleAccountId,
+                                                             responsibleConvUid)
+                                             })
         }
-    }
 
-    /*
-     * All GeneralMenuItems should remain the same width / height.
-     */
-    GeneralMenuItem {
-        id: startVideoCallItem
+        if (contactType === Profile.Type.RING || contactType === Profile.Type.PENDING) {
+            ContextMenuGenerator.addMenuSeparator()
 
-        itemName: qsTr("Start video call")
-        iconSource: "qrc:/images/icons/ic_video_call_24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
+            if (contactType === Profile.Type.PENDING) {
+                ContextMenuGenerator.addMenuItem(qsTr("Accept request"),
+                                                 "qrc:/images/icons/person_add-24px.svg",
+                                                 function (){
+                                                     MessagesAdapter.acceptInvitation(
+                                                                 responsibleConvUid)
+                                                 })
+                ContextMenuGenerator.addMenuItem(qsTr("Decline request"),
+                                                 "qrc:/images/icons/round-close-24px.svg",
+                                                 function (){
+                                                     MessagesAdapter.refuseInvitation(
+                                                                 responsibleConvUid)
+                                                 })
+            }
+            ContextMenuGenerator.addMenuItem(qsTr("Block contact"),
+                                             "qrc:/images/icons/ic_block_24px.svg",
+                                             function (){
+                                                 MessagesAdapter.blockConversation(
+                                                             responsibleConvUid)
+                                             })
 
-        onClicked: {
-            contextMenu.close()
-            ConversationsAdapter.selectConversation(responsibleAccountId,
-                                                    responsibleConvUid, false)
-            CallAdapter.placeCall()
+            ContextMenuGenerator.addMenuSeparator()
+            ContextMenuGenerator.addMenuItem(qsTr("Profile"),
+                                             "qrc:/images/icons/person-24px.svg",
+                                             function (){
+                                                 userProfile.open()
+                                             })
         }
+
+        root.height = ContextMenuGenerator.getMenu().height
+        root.width = ContextMenuGenerator.getMenu().width
+        ContextMenuGenerator.getMenu().open()
     }
 
-    GeneralMenuItem {
-        id: startAudioCallItem
-
-        itemName: qsTr("Start audio call")
-        iconSource: "qrc:/images/icons/ic_phone_24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            contextMenu.close()
-            ConversationsAdapter.selectConversation(responsibleAccountId,
-                                                    responsibleConvUid, false)
-            CallAdapter.placeAudioOnlyCall()
-        }
-    }
-
-    GeneralMenuItem {
-        id: clearConversationItem
-
-        itemName: qsTr("Clear conversation")
-        iconSource: "qrc:/images/icons/ic_clear_24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            contextMenu.close()
-            ClientWrapper.utilsAdaptor.clearConversationHistory(responsibleAccountId,
-                                                  responsibleConvUid)
-        }
-    }
-
-    GeneralMenuItem {
-        id: removeContactItem
-
-        itemName: qsTr("Remove contact")
-        iconSource: "qrc:/images/icons/round-remove_circle-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            contextMenu.close()
-            ClientWrapper.utilsAdaptor.removeConversation(responsibleAccountId,
-                                            responsibleConvUid)
-        }
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startVideoCallItem.preferredWidth
-        preferredHeight: commonBorderWidth
-
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    GeneralMenuItem {
-        id: blockContactItem
-
-        itemName: qsTr("Block contact")
-        iconSource: "qrc:/images/icons/ic_block_24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            contextMenu.close()
-            ClientWrapper.utilsAdaptor.removeConversation(responsibleAccountId,
-                                            responsibleConvUid, true)
-        }
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startVideoCallItem.preferredWidth
-        preferredHeight: commonBorderWidth
-
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    GeneralMenuItem {
-        id: profileItem
-
-        itemName: qsTr("Profile")
-        iconSource: "qrc:/images/icons/person-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            contextMenu.close()
-            userProfile.open()
-        }
-    }
-
-    GeneralMenuSeparator {
-        preferredWidth: startVideoCallItem.preferredWidth
-        preferredHeight: 8
-        separatorColor: "transparent"
-        Component.onCompleted: {
-            generalMenuSeparatorCount++
-        }
-    }
-
-    background: Rectangle {
-        implicitWidth: startVideoCallItem.preferredWidth
-        implicitHeight: startVideoCallItem.preferredHeight
-                        * (contextMenu.count - generalMenuSeparatorCount)
-
-        border.width: commonBorderWidth
-        border.color: JamiTheme.tabbarBorderColor
+    Component.onCompleted: {
+        ContextMenuGenerator.createBaseContextMenuObjects(root)
     }
 }
diff --git a/src/mainview/components/ConversationSmartListView.qml b/src/mainview/components/ConversationSmartListView.qml
index ee787f7..d16b96d 100644
--- a/src/mainview/components/ConversationSmartListView.qml
+++ b/src/mainview/components/ConversationSmartListView.qml
@@ -82,6 +82,10 @@
         conversationSmartListView.model.setAccount(accountId)
     }
 
+    ConversationSmartListContextMenu {
+        id: smartListContextMenu
+    }
+
     Connections {
         target: CallAdapter
 
diff --git a/src/mainview/components/ConversationSmartListViewItemDelegate.qml b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
index 7b564a8..74d07db 100644
--- a/src/mainview/components/ConversationSmartListViewItemDelegate.qml
+++ b/src/mainview/components/ConversationSmartListViewItemDelegate.qml
@@ -180,7 +180,7 @@
                 itemSmartListBackground.color = JamiTheme.releaseColor
             }
             if (mouse.button === Qt.RightButton) {
-
+                smartListContextMenu.parent = mouseAreaSmartListItemDelegate
 
                 /*
                  * Make menu pos at mouse.
@@ -191,6 +191,7 @@
                 smartListContextMenu.y = relativeMousePos.y
                 smartListContextMenu.responsibleAccountId = ClientWrapper.utilsAdaptor.getCurrAccId()
                 smartListContextMenu.responsibleConvUid = UID
+                smartListContextMenu.contactType = ContactType
                 userProfile.responsibleConvUid = UID
                 userProfile.aliasText = DisplayName
                 userProfile.registeredNameText = DisplayID
@@ -225,8 +226,4 @@
             }
         }
     }
-
-    ConversationSmartListContextMenu {
-        id: smartListContextMenu
-    }
 }
diff --git a/src/mainview/components/ParticipantContextMenu.qml b/src/mainview/components/ParticipantContextMenu.qml
index 9397701..87d883a 100644
--- a/src/mainview/components/ParticipantContextMenu.qml
+++ b/src/mainview/components/ParticipantContextMenu.qml
@@ -1,7 +1,7 @@
-
 /*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+ * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
 import QtQuick 2.14
 import QtQuick.Controls 2.14
 import QtGraphicalEffects 1.12
@@ -23,105 +24,46 @@
 
 import "../../commoncomponents"
 
-import "../js/videodevicecontextmenuitemcreation.js" as VideoDeviceContextMenuItemCreation
-import "../js/selectscreenwindowcreation.js" as SelectScreenWindowCreation
+import "../../commoncomponents/js/contextmenugenerator.js" as ContextMenuGenerator
 
-Menu {
+Item {
     id: root
 
-    property int generalMenuSeparatorCount: 0
-    property int commonBorderWidth: 1
-    font.pointSize: JamiTheme.textFontSize + 3
     property var uri: ""
     property var maximized: true
     property var active: true
+    property var showHangup: false
+    property var showMaximize: false
+    property var showMinimize: false
 
-    function showHangup(show) {
-        if (show) {
-            hangupItem.visible = true
-            hangupItem.height = hangupItem.preferredHeight
-        } else {
-            hangupItem.visible = false
-            hangupItem.height = 0
-        }
+    function openMenu(){
+        if (showHangup)
+            ContextMenuGenerator.addMenuItem(qsTr("Hang up"),
+                                             "qrc:/images/icons/ic_call_end_white_24px.svg",
+                                             function (){
+                                                 CallAdapter.hangupCall(uri)
+                                             })
+
+        if (showMaximize)
+            ContextMenuGenerator.addMenuItem(qsTr("Maximize participant"),
+                                             "qrc:/images/icons/open_in_full-24px.svg",
+                                             function (){
+                                                  CallAdapter.maximizeParticipant(uri, active)
+                                             })
+        if (showMinimize)
+            ContextMenuGenerator.addMenuItem(qsTr("Minimize participant"),
+                                             "qrc:/images/icons/close_fullscreen-24px.svg",
+                                             function (){
+                                                  CallAdapter.minimizeParticipant()
+                                             })
+
+        root.height = ContextMenuGenerator.getMenu().height
+        root.width = ContextMenuGenerator.getMenu().width
+        ContextMenuGenerator.getMenu().open()
     }
 
-    function showMaximize(show) {
-        if (show) {
-            maximizeItem.visible = true
-            maximizeItem.height = hangupItem.preferredHeight
-        } else {
-            maximizeItem.visible = false
-            maximizeItem.height = 0
-        }
-    }
-
-    function showMinimize(show) {
-        if (show) {
-            minimizeItem.visible = true
-            minimizeItem.height = hangupItem.preferredHeight
-        } else {
-            minimizeItem.visible = false
-            minimizeItem.height = 0
-        }
-    }
-
-    function setHeight(visibleItems) {
-        root.height = hangupItem.preferredHeight * visibleItems;
-    }
-
-    /*
-     * All GeneralMenuItems should remain the same width / height.
-     */
-    GeneralMenuItem {
-        id: hangupItem
-
-        itemName: qsTr("Hangup")
-        iconSource: "qrc:/images/icons/ic_call_end_white_24px.svg"
-        icon.color: "black"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-
-        onClicked: {
-            CallAdapter.hangupCall(uri)
-            root.close()
-        }
-    }
-    GeneralMenuItem {
-        id: maximizeItem
-
-        itemName: qsTr("Maximize participant")
-        iconSource: "qrc:/images/icons/open_in_full-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-        visible: !maximized
-
-        onClicked: {
-            CallAdapter.maximizeParticipant(uri, active)
-            root.close()
-        }
-    }
-    GeneralMenuItem {
-        id: minimizeItem
-
-        itemName: qsTr("Minimize participant")
-        iconSource: "qrc:/images/icons/close_fullscreen-24px.svg"
-        leftBorderWidth: commonBorderWidth
-        rightBorderWidth: commonBorderWidth
-        visible: maximized
-
-        onClicked: {
-            CallAdapter.minimizeParticipant()
-            root.close()
-        }
-    }
-
-    background: Rectangle {
-        implicitWidth: hangupItem.preferredWidth
-        implicitHeight: hangupItem.preferredHeight * 3
-
-        border.width: commonBorderWidth
-        border.color: JamiTheme.tabbarBorderColor
+    Component.onCompleted: {
+        ContextMenuGenerator.createBaseContextMenuObjects(root)
     }
 }
 
diff --git a/src/mainview/components/ParticipantOverlay.qml b/src/mainview/components/ParticipantOverlay.qml
index fb49a46..2f17b08 100644
--- a/src/mainview/components/ParticipantOverlay.qml
+++ b/src/mainview/components/ParticipantOverlay.qml
@@ -112,18 +112,14 @@
                         var layout = CallAdapter.getCurrentLayoutType()
                         var showMaximized = layout !== 2
                         var showMinimized = !(layout === 0 || (layout === 1 && !active))
-                        injectedContextMenu.showHangup(!root.isLocal)
-                        injectedContextMenu.showMaximize(showMaximized)
-                        injectedContextMenu.showMinimize(showMinimized)
-                        injectedContextMenu.setHeight(
-                            (root.isLocal ? 0 : 1)
-                            + (showMaximized ? 1 : 0)
-                            + (showMinimized ? 1 : 0))
+                        injectedContextMenu.showHangup = !root.isLocal
+                        injectedContextMenu.showMaximize = showMaximized
+                        injectedContextMenu.showMinimize = showMinimized
                         injectedContextMenu.uri = uri
                         injectedContextMenu.active = active
                         injectedContextMenu.x = mousePos.x
                         injectedContextMenu.y = mousePos.y - injectedContextMenu.height
-                        injectedContextMenu.open()
+                        injectedContextMenu.openMenu()
                     }
                 }
             }
@@ -166,4 +162,4 @@
             duration: 500
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/mainview/components/VideoCallPageContextMenuDeviceItem.qml b/src/mainview/components/VideoCallPageContextMenuDeviceItem.qml
index 73e692b..a6a4f80 100644
--- a/src/mainview/components/VideoCallPageContextMenuDeviceItem.qml
+++ b/src/mainview/components/VideoCallPageContextMenuDeviceItem.qml
@@ -1,4 +1,3 @@
-
 /*
  * Copyright (C) 2020 by Savoir-faire Linux
  * Author: Mingrui Zhang <mingrui.zhang@savoirfairelinux.com>
@@ -16,24 +15,21 @@
  * You should have received a copy of the GNU General Public License
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
+
 import QtQuick 2.14
 import QtQuick.Controls 2.14
 import net.jami.Models 1.0
 
 import "../../commoncomponents"
 
-
 /*
- * Take advantage of child can access parent's item (ex: contextMenu, commonBorderWidth).
+ * Menu item wrapper for video device checkable item.
  */
 GeneralMenuItem {
     id: videoCallPageContextMenuDeviceItem
 
     property int contextMenuPreferredWidth: 250
 
-    leftBorderWidth: commonBorderWidth
-    rightBorderWidth: commonBorderWidth
-
     TextMetrics {
         id: textMetrics
         elide: Text.ElideMiddle
@@ -54,7 +50,6 @@
 
     onClicked: {
         var deviceName = videoCallPageContextMenuDeviceItem.itemName
-        contextMenu.close()
         AvAdapter.onVideoContextMenuDeviceItemClicked(deviceName)
     }
 }
diff --git a/src/mainview/js/videodevicecontextmenuitemcreation.js b/src/mainview/js/videodevicecontextmenuitemcreation.js
index 02aa292..ca17fc7 100644
--- a/src/mainview/js/videodevicecontextmenuitemcreation.js
+++ b/src/mainview/js/videodevicecontextmenuitemcreation.js
@@ -40,24 +40,28 @@
     videoContextMenuObject = obj
 }
 
-function createVideoDeviceContextMenuItemObjects(deviceName, setChecked, last) {
+function createVideoDeviceContextMenuItemObjects(deviceName, setChecked) {
 
     videoDeviceContextMenuItemComponent = Qt.createComponent(
                 "../components/VideoCallPageContextMenuDeviceItem.qml")
     if (videoDeviceContextMenuItemComponent.status === Component.Ready)
-        finishCreation(deviceName, setChecked, last)
+        finishCreation(deviceName, setChecked)
     else if (videoDeviceContextMenuItemComponent.status === Component.Error)
         console.log("Error loading component:",
                     videoDeviceContextMenuItemComponent.errorString())
 }
 
-function finishCreation(deviceName, setChecked, last) {
+function finishCreation(deviceName, setChecked) {
     videoDeviceContextMenuItemObject = videoDeviceContextMenuItemComponent.createObject()
     if (videoDeviceContextMenuItemObject === null) {
         // Error Handling.
         console.log("Error creating video context menu object")
     }
 
+    videoDeviceContextMenuItemObject.leftBorderWidth =
+            videoContextMenuObject.commonBorderWidth
+    videoDeviceContextMenuItemObject.rightBorderWidth =
+            videoContextMenuObject.commonBorderWidth
     videoDeviceContextMenuItemObject.itemName = deviceName
     videoDeviceContextMenuItemObject.checkable = true
     videoDeviceContextMenuItemObject.checked = setChecked
@@ -68,14 +72,11 @@
      * Push into the storage array, and insert it into context menu.
      */
     itemArray.push(videoDeviceContextMenuItemObject)
-    videoContextMenuObject.insertItem(3 /* The button is at pos 3 in the menu */, videoDeviceContextMenuItemObject)
+    videoContextMenuObject.addItem(videoDeviceContextMenuItemObject)
 
-
-    /*
-     * If it is the last device context menu item, open the context menu.
-     */
-    if (last)
-        videoContextMenuObject.open()
+    videoDeviceContextMenuItemObject.clicked.connect(function () {
+        videoContextMenuObject.close()
+    })
 }
 
 function removeCreatedItems() {
diff --git a/src/messagesadapter.cpp b/src/messagesadapter.cpp
index 5214d0b..37b477f 100644
--- a/src/messagesadapter.cpp
+++ b/src/messagesadapter.cpp
@@ -633,24 +633,24 @@
 }
 
 void
-MessagesAdapter::acceptInvitation()
+MessagesAdapter::acceptInvitation(const QString &convUid)
 {
-    const auto convUid = LRCInstance::getCurrentConvUid();
-    LRCInstance::getCurrentConversationModel()->makePermanent(convUid);
+    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
+    LRCInstance::getCurrentConversationModel()->makePermanent(currentConvUid);
 }
 
 void
-MessagesAdapter::refuseInvitation()
+MessagesAdapter::refuseInvitation(const QString &convUid)
 {
-    auto convUid = LRCInstance::getCurrentConvUid();
-    LRCInstance::getCurrentConversationModel()->removeConversation(convUid, false);
+    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
+    LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, false);
     setInvitation(false);
 }
 
 void
-MessagesAdapter::blockConversation()
+MessagesAdapter::blockConversation(const QString &convUid)
 {
-    auto convUid = LRCInstance::getCurrentConvUid();
-    LRCInstance::getCurrentConversationModel()->removeConversation(convUid, true);
+    const auto currentConvUid = convUid.isEmpty() ? LRCInstance::getCurrentConvUid() : convUid;
+    LRCInstance::getCurrentConversationModel()->removeConversation(currentConvUid, true);
     setInvitation(false);
 }
diff --git a/src/messagesadapter.h b/src/messagesadapter.h
index 6b520d2..aaf11da 100644
--- a/src/messagesadapter.h
+++ b/src/messagesadapter.h
@@ -41,9 +41,9 @@
     /*
      * JS Q_INVOKABLE.
      */
-    Q_INVOKABLE void acceptInvitation();
-    Q_INVOKABLE void refuseInvitation();
-    Q_INVOKABLE void blockConversation();
+    Q_INVOKABLE void acceptInvitation(const QString &convUid = "");
+    Q_INVOKABLE void refuseInvitation(const QString &convUid = "");
+    Q_INVOKABLE void blockConversation(const QString &convUid = "");
     Q_INVOKABLE void setNewMessagesContent(const QString &path);
     Q_INVOKABLE void sendMessage(const QString &message);
     Q_INVOKABLE void sendImage(const QString &message);
diff --git a/src/settingsview/components/BannedItemDelegate.qml b/src/settingsview/components/BannedItemDelegate.qml
index 0322bb0..999b494 100644
--- a/src/settingsview/components/BannedItemDelegate.qml
+++ b/src/settingsview/components/BannedItemDelegate.qml
@@ -173,7 +173,7 @@
             buttonImageHeight: height - 8
             buttonImageWidth: width - 8
 
-            source: "qrc:/images/icons/ic_person_add_black_24dp_2x.png"
+            source: "qrc:/images/icons/person_add-24px.svg"
 
             radius: height / 2
             width: 25