alqindiirsyam 3 éve
szülő
commit
66fcd02def
97 módosított fájl, 8493 hozzáadás és 1621 törlés
  1. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/Attachment.imageset/Attachment.png
  2. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/bni_background.imageset/bni_background.png
  3. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/button_broadcast.imageset/Contents.json
  4. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/gaspol_icon.imageset/nexilis_icon.png
  5. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_internal.imageset/Contents.json
  6. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_internal_cc.imageset/ic_internal_cc.png
  7. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_launcher_pb.imageset/Contents.json
  8. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_launcher_pb.imageset/ic_launcher_pb.png
  9. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_official_flag.imageset/Contents.json
  10. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ball_b.imageset/Contents.json
  11. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ball_b.imageset/ic_launcher_fit.png
  12. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button.imageset/Contents.json
  13. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button.imageset/pb_button.png
  14. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_call.imageset/Contents.json
  15. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_cc-1.imageset/pb_button_cc.png
  16. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_cc.imageset/Contents.json
  17. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_chat-1.imageset/pb_button_chat.png
  18. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_chat.imageset/Contents.json
  19. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_wb.imageset/pb_button_wb.png
  20. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_call_center.imageset/Contents.json
  21. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_bni2.imageset/pb_fb_bni2.png
  22. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_expora2.imageset/Contents.json
  23. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_expora2.imageset/pb_fb_expora2.png
  24. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_live_tv.imageset/Contents.json
  25. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered.imageset/pb_ball.png
  26. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered_button.imageset/Contents.json
  27. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered_button.imageset/pb_powered_button.png
  28. 21 0
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_seminar_wpr.imageset/Contents.json
  29. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pdf-icon.imageset/pdf-icon.png
  30. 89 45
      appbuilder-ios/NexilisLite/NexilisLite/Resource/Palio.storyboard
  31. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/PreviewAttachmentImageVideo.xib
  32. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-LightItalic.ttf
  33. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-Medium.ttf
  34. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-MediumItalic.ttf
  35. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-Regular.ttf
  36. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-SemiBold.ttf
  37. BIN
      appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-SemiBoldItalic.ttf
  38. 17 57
      appbuilder-ios/NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings
  39. 211 3
      appbuilder-ios/NexilisLite/NexilisLite/Source/Archive.swift
  40. 25 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift
  41. 8 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift
  42. 19 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageUtil.swift
  43. 359 47
      appbuilder-ios/NexilisLite/NexilisLite/Source/Download.swift
  44. 416 199
      appbuilder-ios/NexilisLite/NexilisLite/Source/FloatingButton/FloatingButton.swift
  45. 158 42
      appbuilder-ios/NexilisLite/NexilisLite/Source/IncomingThread.swift
  46. 68 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/Model/CategoryCC.swift
  47. 9 4
      appbuilder-ios/NexilisLite/NexilisLite/Source/Model/Chat.swift
  48. 27 11
      appbuilder-ios/NexilisLite/NexilisLite/Source/Model/Model.swift
  49. 47 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/Model/WorkingArea.swift
  50. 0 79
      appbuilder-ios/NexilisLite/NexilisLite/Source/MultipartFormDataRequest.swift
  51. 636 319
      appbuilder-ios/NexilisLite/NexilisLite/Source/Network.swift
  52. 1 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/OutgoingThread.swift
  53. 1 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/TMessage.swift
  54. 4 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/Utils.swift
  55. 116 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift
  56. 106 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/BNIView/WorkingAreaPicker.swift
  57. 22 7
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/AudioViewController.swift
  58. 641 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/GroupView.swift
  59. 305 13
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift
  60. 106 12
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraCallContactViewController.swift
  61. 437 93
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift
  62. 21 21
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/VideoViewController.swift
  63. 143 41
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/WhiteboardViewController.swift
  64. 786 123
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift
  65. 24 6
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift
  66. 338 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/FormEditor.swift
  67. 1 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.swift
  68. 1 1
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.xib
  69. 48 7
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Contact/ContactCallViewController.swift
  70. 11 14
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/AddFriendTableViewController.swift
  71. 4 3
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/AudienceViewController.swift
  72. 25 7
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/BroadcastMembersTableViewCell.swift
  73. 26 17
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/BroadcastModeViewController.swift
  74. 17 10
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift
  75. 28 15
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeNamePassswordViewController.swift
  76. 8 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeNameTableViewController.swift
  77. 7 7
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangePasswordViewController.swift
  78. 870 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/CheckConnection.swift
  79. 149 48
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift
  80. 3 3
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/DocumentPicker.swift
  81. 139 26
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupDescViewController.swift
  82. 1 4
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupMemberViewController.swift
  83. 31 7
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupNameViewController.swift
  84. 170 16
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/HistoryCCViewController.swift
  85. 4 4
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ImageVideoPicker.swift
  86. 210 28
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ProfileViewController.swift
  87. 122 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/QRScannerView.swift
  88. 107 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ScannerViewController.swift
  89. 408 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/SetOfficerBNI.swift
  90. 533 223
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift
  91. 6 2
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/TypeViewController.swift
  92. 67 17
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraCreateStreamingViewController.swift
  93. 1 4
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraGroupChooserViewController.swift
  94. 11 11
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraStreamingViewController.swift
  95. 20 9
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraUserChooserViewController.swift
  96. 31 12
      appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/StreamingViewController.swift
  97. 0 0
      appbuilder-ios/NexilisLite/NexilisLite/Source/WhiteboardDelegate.swift

+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/Attachment.imageset/Attachment.png


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/bni_background.imageset/bni_background.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/button_broadcast.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "nexilis_icon.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/gaspol_icon.imageset/nexilis_icon.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_internal.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "ic_internal_cc.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_internal_cc.imageset/ic_internal_cc.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_launcher_pb.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "ic_launcher_pb.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_launcher_pb.imageset/ic_launcher_pb.png


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/ic_official_flag.imageset/Contents.json


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ball_b.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "ic_launcher_fit.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_ball_b.imageset/ic_launcher_fit.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_button.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button.imageset/pb_button.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_call.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_button_cc.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_cc-1.imageset/pb_button_cc.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_cc.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_button_chat.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_chat-1.imageset/pb_button_chat.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_chat.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_button_wb.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_button_wb.imageset/pb_button_wb.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_call_center.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_fb_bni2.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_bni2.imageset/pb_fb_bni2.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_expora2.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_fb_expora2.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_fb_expora2.imageset/pb_fb_expora2.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_live_tv.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_ball.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered.imageset/pb_ball.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered_button.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pb_powered_button.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_powered_button.imageset/pb_powered_button.png


+ 21 - 0
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pb_seminar_wpr.imageset/Contents.json

@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "pdf-icon.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/Assets.xcassets/pdf-icon.imageset/pdf-icon.png


+ 89 - 45
appbuilder-ios/NexilisLite/NexilisLite/Resource/Palio.storyboard

@@ -412,7 +412,7 @@
         <!--Setting Table View Controller-->
         <scene sceneID="olm-Kj-Eed">
             <objects>
-                <tableViewController storyboardIdentifier="settingTable" useStoryboardIdentifierAsRestorationIdentifier="YES" id="Qua-Qd-ZFt" customClass="SettingTableViewController" customModule="NexilisLite" customModuleProvider="target" sceneMemberID="viewController">
+                <tableViewController id="Qua-Qd-ZFt" customClass="SettingTableViewController" customModule="NexilisLite" customModuleProvider="target" sceneMemberID="viewController">
                     <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="F0a-VD-4oM">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@@ -534,31 +534,43 @@
                                                     <nil key="highlightedColor"/>
                                                 </label>
                                                 <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="4UK-RB-Iio">
-                                                    <rect key="frame" x="344" y="57.5" width="40" height="39"/>
+                                                    <rect key="frame" x="309" y="57.5" width="40" height="39"/>
                                                     <constraints>
                                                         <constraint firstAttribute="width" constant="40" id="O3j-ai-d65"/>
                                                         <constraint firstAttribute="height" constant="40" id="ZKu-YQ-3Dg"/>
                                                     </constraints>
                                                     <fontDescription key="fontDescription" type="system" pointSize="20"/>
-                                                    <color key="tintColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                                    <color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                                     <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
                                                     <state key="normal" backgroundImage="message.circle.fill" catalog="system"/>
                                                 </button>
+                                                <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sec-Fk-lXl">
+                                                    <rect key="frame" x="354" y="57" width="40" height="40"/>
+                                                    <constraints>
+                                                        <constraint firstAttribute="width" constant="40" id="GsX-JC-bwQ"/>
+                                                        <constraint firstAttribute="height" constant="40" id="Vq2-v4-epE"/>
+                                                    </constraints>
+                                                    <color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                                                    <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
+                                                    <state key="normal" backgroundImage="pdf-icon"/>
+                                                </button>
                                             </subviews>
                                             <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                             <constraints>
                                                 <constraint firstAttribute="bottom" secondItem="f9L-Jl-Y5h" secondAttribute="bottom" constant="10" id="0GG-fu-rrU"/>
                                                 <constraint firstItem="f9L-Jl-Y5h" firstAttribute="leading" secondItem="ZkE-zW-tDU" secondAttribute="leading" constant="58" id="2Yn-1X-ozM"/>
+                                                <constraint firstItem="sec-Fk-lXl" firstAttribute="top" secondItem="0ZX-Vv-pzx" secondAttribute="bottom" id="38B-Hu-gW6"/>
                                                 <constraint firstItem="hkO-r2-Lhb" firstAttribute="top" secondItem="ZkE-zW-tDU" secondAttribute="top" id="3yY-Be-FB2"/>
                                                 <constraint firstItem="MY5-fu-8Ij" firstAttribute="leading" secondItem="9dg-SC-bM5" secondAttribute="trailing" constant="8" symbolic="YES" id="9Ww-9u-hQS"/>
                                                 <constraint firstItem="0ZX-Vv-pzx" firstAttribute="top" secondItem="hkO-r2-Lhb" secondAttribute="bottom" constant="10" id="AaB-tu-efQ"/>
+                                                <constraint firstItem="sec-Fk-lXl" firstAttribute="leading" secondItem="4UK-RB-Iio" secondAttribute="trailing" constant="5" id="IDR-jD-b5b"/>
                                                 <constraint firstItem="MY5-fu-8Ij" firstAttribute="top" secondItem="M0u-0O-BEp" secondAttribute="bottom" constant="1" id="SLa-0r-OER"/>
                                                 <constraint firstAttribute="trailing" secondItem="hkO-r2-Lhb" secondAttribute="trailing" id="UPV-FZ-5yU"/>
                                                 <constraint firstItem="4UK-RB-Iio" firstAttribute="top" secondItem="0ZX-Vv-pzx" secondAttribute="bottom" id="VtO-23-gcd"/>
                                                 <constraint firstItem="9dg-SC-bM5" firstAttribute="top" secondItem="hkO-r2-Lhb" secondAttribute="bottom" constant="10" id="Y4L-aM-N4L"/>
+                                                <constraint firstAttribute="trailing" secondItem="sec-Fk-lXl" secondAttribute="trailing" constant="10" id="fvi-ek-rWS"/>
                                                 <constraint firstItem="M0u-0O-BEp" firstAttribute="leading" secondItem="9dg-SC-bM5" secondAttribute="trailing" constant="8" symbolic="YES" id="hDa-Ha-J8D"/>
                                                 <constraint firstItem="9dg-SC-bM5" firstAttribute="leading" secondItem="ZkE-zW-tDU" secondAttribute="leading" constant="10" id="hGw-hX-E5p"/>
-                                                <constraint firstAttribute="trailing" secondItem="4UK-RB-Iio" secondAttribute="trailing" constant="20" id="mPd-a1-gb0"/>
                                                 <constraint firstItem="M0u-0O-BEp" firstAttribute="top" secondItem="hkO-r2-Lhb" secondAttribute="bottom" constant="10" id="qE0-cC-X3J"/>
                                                 <constraint firstItem="hkO-r2-Lhb" firstAttribute="leading" secondItem="ZkE-zW-tDU" secondAttribute="leading" id="sWb-HV-IYG"/>
                                                 <constraint firstAttribute="trailing" secondItem="0ZX-Vv-pzx" secondAttribute="trailing" constant="10" id="xdl-9I-5gw"/>
@@ -574,6 +586,7 @@
                                 </tableViewCellContentView>
                                 <connections>
                                     <outlet property="buttonEditor" destination="4UK-RB-Iio" id="M3N-a4-dcg"/>
+                                    <outlet property="buttonPDF" destination="sec-Fk-lXl" id="yce-HC-Omk"/>
                                     <outlet property="imageOfficer" destination="9dg-SC-bM5" id="uJS-5A-NSa"/>
                                     <outlet property="labelComplaintId" destination="f9L-Jl-Y5h" id="gZ2-1b-lOP"/>
                                     <outlet property="labelDate" destination="0ZX-Vv-pzx" id="rxs-qD-i9Y"/>
@@ -651,7 +664,7 @@
                                             <rect key="frame" x="0.0" y="0.0" width="374" height="200"/>
                                             <autoresizingMask key="autoresizingMask"/>
                                             <subviews>
-                                                <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Sofa" translatesAutoresizingMaskIntoConstraints="NO" id="0K5-VL-Mqq">
+                                                <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bni_background" translatesAutoresizingMaskIntoConstraints="NO" id="0K5-VL-Mqq">
                                                     <rect key="frame" x="0.0" y="0.0" width="374" height="200"/>
                                                 </imageView>
                                                 <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="person.circle.fill" catalog="system" translatesAutoresizingMaskIntoConstraints="NO" id="Al4-Bd-c6i">
@@ -664,16 +677,16 @@
                                                     </constraints>
                                                 </imageView>
                                                 <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="40M-iH-oyA">
-                                                    <rect key="frame" x="153" y="158" width="68" height="20"/>
+                                                    <rect key="frame" x="286" y="154.5" width="68" height="24.5"/>
                                                     <subviews>
                                                         <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="ic_internal" translatesAutoresizingMaskIntoConstraints="NO" id="yKV-8p-D06">
-                                                            <rect key="frame" x="5" y="5" width="10" height="10"/>
+                                                            <rect key="frame" x="5" y="-51.5" width="10" height="128"/>
                                                             <constraints>
                                                                 <constraint firstAttribute="width" constant="10" id="Vh5-7K-sQv"/>
                                                             </constraints>
                                                         </imageView>
-                                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Internal" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="evL-sf-Uad">
-                                                            <rect key="frame" x="20" y="5" width="43" height="10"/>
+                                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Internal" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="evL-sf-Uad">
+                                                            <rect key="frame" x="20" y="5" width="43" height="14.5"/>
                                                             <fontDescription key="fontDescription" type="system" pointSize="12"/>
                                                             <nil key="textColor"/>
                                                             <nil key="highlightedColor"/>
@@ -681,26 +694,49 @@
                                                     </subviews>
                                                     <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                                                     <constraints>
-                                                        <constraint firstAttribute="bottom" secondItem="yKV-8p-D06" secondAttribute="bottom" constant="5" id="89b-IH-aAQ"/>
                                                         <constraint firstAttribute="trailing" secondItem="evL-sf-Uad" secondAttribute="trailing" constant="5" id="APn-5r-5jX"/>
+                                                        <constraint firstItem="evL-sf-Uad" firstAttribute="centerY" secondItem="40M-iH-oyA" secondAttribute="centerY" id="Dha-zc-uGG"/>
                                                         <constraint firstItem="evL-sf-Uad" firstAttribute="leading" secondItem="yKV-8p-D06" secondAttribute="trailing" constant="5" id="Lyl-gk-lIi"/>
                                                         <constraint firstItem="evL-sf-Uad" firstAttribute="top" secondItem="40M-iH-oyA" secondAttribute="top" constant="5" id="e6Y-0s-mBS"/>
-                                                        <constraint firstAttribute="height" constant="20" id="i0Q-fH-ts0"/>
+                                                        <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="20" id="i0Q-fH-ts0"/>
                                                         <constraint firstAttribute="bottom" secondItem="evL-sf-Uad" secondAttribute="bottom" constant="5" id="lgM-7n-GLu"/>
-                                                        <constraint firstItem="yKV-8p-D06" firstAttribute="top" secondItem="40M-iH-oyA" secondAttribute="top" constant="5" id="ptd-4f-K3D"/>
                                                         <constraint firstItem="yKV-8p-D06" firstAttribute="leading" secondItem="40M-iH-oyA" secondAttribute="leading" constant="5" id="rfS-v4-lis"/>
+                                                        <constraint firstItem="yKV-8p-D06" firstAttribute="centerY" secondItem="40M-iH-oyA" secondAttribute="centerY" id="siP-9D-Nbu"/>
+                                                        <constraint firstAttribute="width" relation="lessThanOrEqual" constant="100" id="xNl-lY-hhD"/>
+                                                    </constraints>
+                                                </view>
+                                                <view hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="4Od-Bu-62G">
+                                                    <rect key="frame" x="155.5" y="158" width="63" height="20"/>
+                                                    <subviews>
+                                                        <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="0 Friends" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yNx-gJ-gXq">
+                                                            <rect key="frame" x="5" y="0.0" width="53" height="20"/>
+                                                            <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                                                            <nil key="textColor"/>
+                                                            <nil key="highlightedColor"/>
+                                                        </label>
+                                                    </subviews>
+                                                    <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                                                    <constraints>
+                                                        <constraint firstAttribute="bottom" secondItem="yNx-gJ-gXq" secondAttribute="bottom" id="6Ku-Qa-otC"/>
+                                                        <constraint firstItem="yNx-gJ-gXq" firstAttribute="leading" secondItem="4Od-Bu-62G" secondAttribute="leading" constant="5" id="anl-DA-QKr"/>
+                                                        <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="40" id="fcY-Ya-vjd"/>
+                                                        <constraint firstItem="yNx-gJ-gXq" firstAttribute="top" secondItem="4Od-Bu-62G" secondAttribute="top" id="jeg-1b-IcN"/>
+                                                        <constraint firstAttribute="height" constant="20" id="l4y-uB-0Vs"/>
+                                                        <constraint firstAttribute="trailing" secondItem="yNx-gJ-gXq" secondAttribute="trailing" constant="5" id="lhT-7H-PB4"/>
                                                     </constraints>
                                                 </view>
                                             </subviews>
                                             <constraints>
+                                                <constraint firstAttribute="trailingMargin" secondItem="40M-iH-oyA" secondAttribute="trailing" id="2Dq-Mc-H4d"/>
                                                 <constraint firstAttribute="bottom" secondItem="0K5-VL-Mqq" secondAttribute="bottom" id="7fF-SI-jIM"/>
-                                                <constraint firstItem="40M-iH-oyA" firstAttribute="top" secondItem="Al4-Bd-c6i" secondAttribute="bottom" constant="8" symbolic="YES" id="Gbb-LD-q7F"/>
+                                                <constraint firstItem="4Od-Bu-62G" firstAttribute="top" secondItem="Al4-Bd-c6i" secondAttribute="bottom" constant="8" symbolic="YES" id="A2c-If-gTE"/>
                                                 <constraint firstItem="0K5-VL-Mqq" firstAttribute="top" secondItem="lop-Ow-tJu" secondAttribute="top" id="KSw-ew-NOs"/>
-                                                <constraint firstItem="40M-iH-oyA" firstAttribute="centerX" secondItem="lop-Ow-tJu" secondAttribute="centerX" id="R9s-kV-9kW"/>
                                                 <constraint firstItem="Al4-Bd-c6i" firstAttribute="centerX" secondItem="lop-Ow-tJu" secondAttribute="centerX" id="RCV-tS-V7i"/>
+                                                <constraint firstItem="4Od-Bu-62G" firstAttribute="centerX" secondItem="lop-Ow-tJu" secondAttribute="centerX" id="WaS-Yt-ldB"/>
                                                 <constraint firstItem="0K5-VL-Mqq" firstAttribute="leading" secondItem="lop-Ow-tJu" secondAttribute="leading" id="dG1-3T-Dc8"/>
                                                 <constraint firstAttribute="trailing" secondItem="0K5-VL-Mqq" secondAttribute="trailing" id="dvH-ey-GNH"/>
                                                 <constraint firstItem="Al4-Bd-c6i" firstAttribute="centerY" secondItem="lop-Ow-tJu" secondAttribute="centerY" id="meT-ah-cdH"/>
+                                                <constraint firstAttribute="bottomMargin" secondItem="40M-iH-oyA" secondAttribute="bottom" constant="10" id="r1W-TW-sUP"/>
                                             </constraints>
                                         </tableViewCellContentView>
                                     </tableViewCell>
@@ -761,7 +797,7 @@
                                                             <color key="onTintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                                         </switch>
                                                     </subviews>
-                                                    <color key="backgroundColor" systemColor="systemBackgroundColor"/>
+                                                    <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                                     <constraints>
                                                         <constraint firstItem="UZm-64-k1t" firstAttribute="leading" secondItem="VAe-OS-2Uz" secondAttribute="leading" constant="8" id="4n3-85-Ctg"/>
                                                         <constraint firstAttribute="trailing" secondItem="NYg-hJ-SNM" secondAttribute="trailing" constant="8" id="LnI-x9-osh"/>
@@ -799,8 +835,10 @@
                                                             <state key="normal" image="message.fill" catalog="system"/>
                                                         </button>
                                                     </subviews>
+                                                    <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                                 </stackView>
                                             </subviews>
+                                            <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                             <constraints>
                                                 <constraint firstAttribute="bottom" secondItem="iZk-GF-RJR" secondAttribute="bottom" id="An6-Mp-rx8"/>
                                                 <constraint firstItem="VAe-OS-2Uz" firstAttribute="leading" secondItem="61A-ex-UYs" secondAttribute="leading" id="E4o-Wh-JSD"/>
@@ -812,6 +850,7 @@
                                                 <constraint firstAttribute="trailing" secondItem="iZk-GF-RJR" secondAttribute="trailing" id="rvp-48-tFT"/>
                                             </constraints>
                                         </tableViewCellContentView>
+                                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
                                     </tableViewCell>
                                 </cells>
                             </tableViewSection>
@@ -826,6 +865,7 @@
                         <outlet property="buttonGroup" destination="iZk-GF-RJR" id="mQl-wh-jCZ"/>
                         <outlet property="buttonHistoryCC" destination="NYg-hJ-SNM" id="1ah-Tv-T3l"/>
                         <outlet property="call" destination="8T1-2m-3h5" id="i1Z-n0-cmD"/>
+                        <outlet property="countFriend" destination="yNx-gJ-gXq" id="d2Q-CD-5xn"/>
                         <outlet property="imageUserType" destination="yKV-8p-D06" id="TfH-As-6uv"/>
                         <outlet property="labelUserType" destination="evL-sf-Uad" id="RCj-po-eey"/>
                         <outlet property="message" destination="Sti-Yk-M8o" id="y6C-te-gGV"/>
@@ -834,6 +874,7 @@
                         <outlet property="switchAcceptCall" destination="imY-6r-cWB" id="Klz-Le-70y"/>
                         <outlet property="switchPrivateAccount" destination="oaQ-vC-3bV" id="bTI-fM-xc6"/>
                         <outlet property="video" destination="Opk-qK-mSQ" id="DkR-nl-nae"/>
+                        <outlet property="viewFriend" destination="4Od-Bu-62G" id="2R5-Lg-lU3"/>
                         <outlet property="viewUserType" destination="40M-iH-oyA" id="iS2-TK-MD9"/>
                     </connections>
                 </tableViewController>
@@ -856,8 +897,8 @@
                                     <constraint firstAttribute="height" constant="2" id="lQU-R4-eCU"/>
                                 </constraints>
                             </view>
-                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Please enter your desired Username and Password" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nim-58-zjc">
-                                <rect key="frame" x="43" y="76" width="328" height="17"/>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Please enter your desired Username and Password" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nim-58-zjc">
+                                <rect key="frame" x="20" y="76" width="374" height="17"/>
                                 <color key="tintColor" systemColor="tintColor"/>
                                 <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                 <color key="textColor" systemColor="systemGrayColor"/>
@@ -890,6 +931,7 @@
                             <constraint firstItem="XNI-ec-RQS" firstAttribute="leading" secondItem="EW3-tZ-XKp" secondAttribute="leading" constant="20" id="F8y-TU-1IG"/>
                             <constraint firstItem="EW3-tZ-XKp" firstAttribute="trailing" secondItem="ytt-6A-LrS" secondAttribute="trailing" constant="30" id="NoJ-Wa-EW3"/>
                             <constraint firstItem="Hor-5y-mXt" firstAttribute="top" secondItem="XNI-ec-RQS" secondAttribute="bottom" constant="10" id="QC1-Jp-kSJ"/>
+                            <constraint firstItem="EW3-tZ-XKp" firstAttribute="trailing" secondItem="nim-58-zjc" secondAttribute="trailing" constant="20" id="Rdb-lb-gOO"/>
                             <constraint firstItem="EW3-tZ-XKp" firstAttribute="trailing" secondItem="Hor-5y-mXt" secondAttribute="trailing" constant="20" id="Uti-pJ-9cB"/>
                             <constraint firstItem="XNI-ec-RQS" firstAttribute="top" secondItem="nim-58-zjc" secondAttribute="bottom" constant="20" id="XJb-ck-66S"/>
                             <constraint firstItem="amR-gf-O8n" firstAttribute="top" secondItem="EW3-tZ-XKp" secondAttribute="top" constant="10" id="cHv-VJ-65A"/>
@@ -898,6 +940,7 @@
                             <constraint firstItem="EW3-tZ-XKp" firstAttribute="trailing" secondItem="XNI-ec-RQS" secondAttribute="trailing" constant="20" id="mK7-0L-TMj"/>
                             <constraint firstItem="ytt-6A-LrS" firstAttribute="top" secondItem="XNI-ec-RQS" secondAttribute="bottom" constant="17" id="nYy-do-ekB"/>
                             <constraint firstItem="amR-gf-O8n" firstAttribute="leading" secondItem="EW3-tZ-XKp" secondAttribute="leading" constant="40" id="qFO-qL-lQ1"/>
+                            <constraint firstItem="nim-58-zjc" firstAttribute="leading" secondItem="EW3-tZ-XKp" secondAttribute="leading" constant="20" id="tZ9-b7-MkZ"/>
                         </constraints>
                     </view>
                     <connections>
@@ -2755,79 +2798,79 @@
             <point key="canvasLocation" x="912" y="751"/>
         </scene>
         <!--Whiteboard View Controller-->
-        <scene sceneID="sO7-Zm-zME">
+        <scene sceneID="ycM-nG-ZKx">
             <objects>
-                <viewController storyboardIdentifier="wbVC" useStoryboardIdentifierAsRestorationIdentifier="YES" id="JN2-2s-jgo" customClass="WhiteboardViewController" customModule="NexilisLite" customModuleProvider="target" sceneMemberID="viewController">
-                    <view key="view" contentMode="scaleToFill" id="ars-iL-8jO">
+                <viewController storyboardIdentifier="wbVC" useStoryboardIdentifierAsRestorationIdentifier="YES" id="m7A-yE-OFx" customClass="WhiteboardViewController" customModule="NexilisLite" customModuleProvider="target" sceneMemberID="viewController">
+                    <view key="view" contentMode="scaleToFill" id="eTm-yY-22u">
                         <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                         <subviews>
-                            <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" alignment="center" spacing="30" translatesAutoresizingMaskIntoConstraints="NO" id="uLP-44-OhX">
+                            <stackView opaque="NO" contentMode="scaleToFill" distribution="equalSpacing" alignment="center" spacing="30" translatesAutoresizingMaskIntoConstraints="NO" id="SYd-6G-9fb">
                                 <rect key="frame" x="107" y="746" width="200" height="80"/>
                                 <subviews>
-                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="fill" contentVerticalAlignment="fill" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="HHF-J4-EPF">
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="fill" contentVerticalAlignment="fill" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5dm-bZ-lpu">
                                         <rect key="frame" x="0.0" y="5" width="70" height="70"/>
                                         <constraints>
-                                            <constraint firstAttribute="height" constant="70" id="RXb-Bz-8Xs"/>
-                                            <constraint firstAttribute="width" constant="70" id="rU1-i4-NwN"/>
+                                            <constraint firstAttribute="height" constant="70" id="MWU-Fy-aD5"/>
+                                            <constraint firstAttribute="width" constant="70" id="OGW-Mx-TCp"/>
                                         </constraints>
                                         <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
                                         <state key="normal" image="xmark.circle.fill" catalog="system"/>
                                         <connections>
-                                            <action selector="didTapClose:" destination="JN2-2s-jgo" eventType="touchUpInside" id="bHz-Tw-qef"/>
+                                            <action selector="didTapClose:" destination="m7A-yE-OFx" eventType="touchUpInside" id="xiJ-oM-wD7"/>
                                         </connections>
                                     </button>
-                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="fill" contentVerticalAlignment="fill" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="p90-fB-ATZ">
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="fill" contentVerticalAlignment="fill" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="C7l-xE-Skp">
                                         <rect key="frame" x="130" y="5" width="70" height="70"/>
                                         <constraints>
-                                            <constraint firstAttribute="width" constant="70" id="Anf-Jb-9Pp"/>
-                                            <constraint firstAttribute="height" constant="70" id="WcM-mV-qUY"/>
+                                            <constraint firstAttribute="height" constant="70" id="1rH-H5-Rfr"/>
+                                            <constraint firstAttribute="width" constant="70" id="FAx-9o-k40"/>
                                         </constraints>
                                         <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
                                         <state key="normal" image="trash.circle.fill" catalog="system"/>
                                         <connections>
-                                            <action selector="didTapClear:" destination="JN2-2s-jgo" eventType="touchUpInside" id="YGl-wx-Txa"/>
+                                            <action selector="didTapClear:" destination="m7A-yE-OFx" eventType="touchUpInside" id="gXq-MR-wEw"/>
                                         </connections>
                                     </button>
                                 </subviews>
                                 <constraints>
-                                    <constraint firstAttribute="height" constant="80" id="it8-Ps-aO4"/>
-                                    <constraint firstAttribute="width" constant="200" id="u2c-1f-HNy"/>
+                                    <constraint firstAttribute="height" constant="80" id="Acy-cm-P16"/>
+                                    <constraint firstAttribute="width" constant="200" id="wcx-DA-9kw"/>
                                 </constraints>
                             </stackView>
-                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="zGZ-Dr-2AD">
+                            <stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yia-0J-7LV">
                                 <rect key="frame" x="107" y="836" width="200" height="30"/>
                                 <subviews>
-                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xvh-jk-sKx">
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ldh-a7-G8u">
                                         <rect key="frame" x="0.0" y="0.0" width="61" height="30"/>
                                         <state key="normal" title="Button"/>
                                         <buttonConfiguration key="configuration" style="plain" title="Alpha"/>
                                     </button>
-                                    <slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="100" minValue="0.0" maxValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="snh-Gc-qha">
+                                    <slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="100" minValue="0.0" maxValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="BPs-KH-mSQ">
                                         <rect key="frame" x="59" y="0.0" width="143" height="31"/>
                                         <connections>
-                                            <action selector="onAlphaChanged:" destination="JN2-2s-jgo" eventType="valueChanged" id="luh-p9-9wa"/>
+                                            <action selector="onAlphaChanged:" destination="m7A-yE-OFx" eventType="valueChanged" id="X0j-xb-PBe"/>
                                         </connections>
                                     </slider>
                                 </subviews>
                                 <constraints>
-                                    <constraint firstAttribute="height" constant="30" id="fTn-E8-5Rc"/>
-                                    <constraint firstAttribute="width" constant="200" id="nCP-47-mi5"/>
+                                    <constraint firstAttribute="width" constant="200" id="5mf-VH-TX5"/>
+                                    <constraint firstAttribute="height" constant="30" id="LHI-bZ-QM4"/>
                                 </constraints>
                             </stackView>
                         </subviews>
-                        <viewLayoutGuide key="safeArea" id="RKb-Q5-FV6"/>
+                        <viewLayoutGuide key="safeArea" id="ZMh-Pr-f5M"/>
                         <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/>
                         <constraints>
-                            <constraint firstItem="uLP-44-OhX" firstAttribute="centerX" secondItem="RKb-Q5-FV6" secondAttribute="centerX" id="czt-x0-AFF"/>
-                            <constraint firstAttribute="bottom" secondItem="zGZ-Dr-2AD" secondAttribute="bottom" constant="30" id="gVL-0s-Mbr"/>
-                            <constraint firstItem="zGZ-Dr-2AD" firstAttribute="top" secondItem="uLP-44-OhX" secondAttribute="bottom" constant="10" id="gnA-ms-ho3"/>
-                            <constraint firstItem="zGZ-Dr-2AD" firstAttribute="centerX" secondItem="RKb-Q5-FV6" secondAttribute="centerX" id="khk-3j-FqI"/>
+                            <constraint firstItem="yia-0J-7LV" firstAttribute="centerX" secondItem="ZMh-Pr-f5M" secondAttribute="centerX" id="U5R-Qy-9a8"/>
+                            <constraint firstItem="yia-0J-7LV" firstAttribute="top" secondItem="SYd-6G-9fb" secondAttribute="bottom" constant="10" id="U6o-0J-bS2"/>
+                            <constraint firstAttribute="bottom" secondItem="yia-0J-7LV" secondAttribute="bottom" constant="30" id="mRC-Od-cmr"/>
+                            <constraint firstItem="SYd-6G-9fb" firstAttribute="centerX" secondItem="ZMh-Pr-f5M" secondAttribute="centerX" id="x8M-1a-saN"/>
                         </constraints>
                     </view>
-                    <navigationItem key="navigationItem" id="td1-fK-fw8"/>
+                    <navigationItem key="navigationItem" id="Csc-Cq-CRL"/>
                 </viewController>
-                <placeholder placeholderIdentifier="IBFirstResponder" id="Vf9-Bi-Ins" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="JAJ-Do-iCj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
             </objects>
             <point key="canvasLocation" x="4366.666666666667" y="10.714285714285714"/>
         </scene>
@@ -2841,16 +2884,17 @@
         <image name="File---Documents" width="500" height="500"/>
         <image name="Send-(White)" width="500" height="500"/>
         <image name="Send-Image" width="500" height="500"/>
-        <image name="Sofa" width="2048" height="1677"/>
         <image name="Sticker---Emoji" width="500" height="500"/>
         <image name="Voice-Record" width="500" height="500"/>
         <image name="arrow.triangle.2.circlepath.camera.fill" catalog="system" width="128" height="94"/>
+        <image name="bni_background" width="300" height="300"/>
         <image name="eye.fill" catalog="system" width="128" height="78"/>
         <image name="ic_internal" width="128" height="128"/>
         <image name="message.circle.fill" catalog="system" width="128" height="121"/>
         <image name="message.fill" catalog="system" width="128" height="113"/>
         <image name="mic.slash.fill" catalog="system" width="108" height="128"/>
         <image name="pb_cd_person" width="512" height="512"/>
+        <image name="pdf-icon" width="860" height="901"/>
         <image name="pencil" catalog="system" width="128" height="113"/>
         <image name="person.badge.plus.fill" catalog="system" width="128" height="124"/>
         <image name="person.circle.fill" catalog="system" width="128" height="121"/>

BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/PreviewAttachmentImageVideo.xib


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-LightItalic.ttf


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-Medium.ttf


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-MediumItalic.ttf


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-Regular.ttf


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-SemiBold.ttf


BIN
appbuilder-ios/NexilisLite/NexilisLite/Resource/fonts/Poppins-SemiBoldItalic.ttf


+ 17 - 57
appbuilder-ios/NexilisLite/NexilisLite/Resource/id.lproj/Localizable.strings

@@ -5,12 +5,8 @@
   Created by Yayan Dwi on 11/11/21.
   
 */
-"Settings" = "Pengaturan";
 "Chats" = "Obrolan";
 "Groups" = "Grup";
-"Chat" = "Obrolan";
-"Group" = "Grup";
-"Search" = "Cari";
 "Contacts" = "Kontak";
 "Discussion" = "Diskusi";
 "Search chats & messages" = "Cari obrolan & pesan";
@@ -59,25 +55,36 @@
 "Image coppied to clipboard" = "Gambar disalin ke papan klip";
 "Unread Messages" = "Pesan yang belum dibaca";
 "Interactive chatbot. Coming soon" = "Chatbot interaktif. Segera akan datang";
-"Start Conversation" = "Mulai Obrolan";
+"Start Conversation" = "Mulai Percakapan";
+
+"Settings" = "Pengaturan";
 "User Profile Management" = "Manajemen Profil Pengguna";
 "Email" = "Email";
+"Logout" = "Keluar Akun";
+"IMI Membership Cards" = "Kartu Keanggotaan IMI";
+"IMI License & Association Cards" = "Kartu Izin & Asosiasi IMI";
+"IMI Membership Registration" = "Daftar Menjadi Anggota IMI";
+"IMI Account Recovery" = "Pemulihan Akun IMI";
 "Blocked User List" = "Daftar Blokir";
 "Content Category" = "Kategori Konten";
-"Language" = "Bahasa";
+"Car" = "Mobil";
+"Motorbike" = "Motor";
+"eBike" = "Sepeda";
+"Change Language" = "Ubah Bahasa";
 "Incoming Message(s)" = "Pesan Masuk";
 "Incoming Call(s)" = "Panggilan Masuk";
 "Vibrate Mode" = "Mode Getar";
 "Save to Gallery" = "Simpan ke Galeri";
 "Auto Download" = "Unduh Otomatis";
 "Version" = "Versi";
-"Powered by Nexilis" = "Didukung oleh Nexilis";
+"Powered by Qmera" = "Dipersembahkan oleh Qmera";
+"Powered by Nexilis" = "Dipersembahkan oleh Nexilis";
 "Sign-Up (Change profile)" = "Daftar (Ganti Profil)";
 "Sign-In (Change Device)" = "Masuk (Ganti Perangkat)";
 "Successfully Logout" = "Berhasil Keluar";
 "Successfully changed device" = "Berhasil masuk/ganti perangkat";
 "Successfully downloaded" = "Berhasil diunduh ke galeri";
-"Log Out" = "Keluar";
+"Logout" = "Keluar";
 "Are you sure want to logout?" = "Apakah anda ingin keluar?";
 "Successfully login Admin" = "Berhasil masuk sebagai Admin";
 "Successfully login Internal Team" = "Berhasil masuk sebagai Tim Internal";
@@ -86,53 +93,6 @@
 "Coming soon feature" = "Fitur akan segera hadir";
 "No call center history" = "Tidak ada riwayat pusat panggilan";
 "Username has been registered, please use another name" = "Nama yang anda masukkan sudah terdaftar, silahkan gunakan nama lain";
-"Scan GasPol QR" = "Pindai QR GasPol";
+"Scan Gaspol QR" = "Memindai Gaspol QR";
+"Scan QR Code" = "Memindai kode QR";
 "To use Gaspol Web, go to gaspol.co.id on your computer." = "Untuk dapat menggunakan Gaspol Web, pergi ke halaman gaspol.co.id pada komputer anda.";
-"Private Account Mode" = "Mode Akun Privat";
-"Change Password" = "Ubah Kata Sandi";
-"Accept Call" = "Terima Panggilan";
-"Contact Center History" = "Riwayat Contact Center";
-"View Profile" = "Lihat Profil";
-"Profile" = "Profil";
-"Data and Privacy Policy" = "Kebijakan Privasi dan Data";
-"Terms & Conditions" = "Syarat & Ketentuan";
-"Help Center" = "Pusat Bantuan";
-"Guest" = "Tamu";
-"Basic Account" = "Akun Dasar";
-"Follow" = "Ikuti";
-"Unfollow" = "Berhenti Ikuti";
-"Bookmark" = "Simpan Penanda";
-"Remove Bookmark" = "Hapus Penanda";
-"Report Post" = "Laporkan Postingan";
-"You must change your Username and Password to use this feature" = "Kamu harus mengubah Nama Pengguna dan Kata Sandi untuk menggunakan fitur ini";
-"You must change your name to use this feature" = "Kamu harus mengubah Nama Pengguna menggunakan fitur ini";
-"Change Profile" = "Ubah Profil";
-"Please enter your desired Username and Password" = "Masukkan Nama Pengguna dan Kata Sandi yang kamu inginkan";
-"Username" = "Nama Pengguna";
-"Password" = "Kata Sandi";
-"Register" = "Daftar";
-"Continue" = "Berikutnya";
-"Don't show this again next time" = "Jangan tampilkan ini lagi lain kali";
-"Edit" = "Ubah";
-"Explore Now" = "Jelajahi Sekarang";
-"Tap Here" = "Tap di Sini";
-"Member Of" = "Anggota Dari";
-"Sign-In" = "Masuk";
-"Sign-Up" = "Daftar";
-"Your Username" = "Tulis Nama Pengguna";
-"Your Password" = "Tulis Kata Sandi";
-"Please enter your name and password to login" = "Harap masukkan nama dan kata sandi untuk masuk";
-"Forgot password?" = "Lupa kata sandi?";
-"Haven't got an account? Tap to sign up" = "Belum menjadi anggota? Ketuk untuk mendaftar";
-"Save" = "Simpan";
-"Change Language" = "Ubah Bahasa";
-"Sign In Admin / Internal" = "Masuk sebagai Admin / Internal";
-"Change Device" = "Ganti Perangkat";
-"Incoming Message(s)" = "Pesan Masuk";
-"Vibrate" = "Mode Getar";
-"Create Group" = "Buat Grup";
-"Add Friend" = "Tambah Teman";
-"Favorite Messages" = "Pesan Favorit";
-"Contact" = "Kontak";
-"Personal Information" = "Profil";
-"Change Floating Button Image Become Photo Profile" = "Ubah Gambar Floating Button Menjadi Foto Profil";

+ 211 - 3
appbuilder-ios/NexilisLite/NexilisLite/Source/Archive.swift

@@ -142,7 +142,7 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
-    public static func sendMessage(l_pin: String, message_scope_id: String, status: String, message_text: String, credential: String, attachment_flag: String, ex_blog_id: String, message_large_text: String, ex_format: String, image_id: String, audio_id: String, video_id: String, file_id: String, thumb_id: String, reff_id: String, read_receipts: String, chat_id: String, is_call_center: String, call_center_id: String) -> TMessage {
+    public static func sendMessage(l_pin: String, message_scope_id: String, status: String, message_text: String, credential: String, attachment_flag: String, ex_blog_id: String, message_large_text: String, ex_format: String, image_id: String, audio_id: String, video_id: String, file_id: String, thumb_id: String, reff_id: String, read_receipts: String, chat_id: String, is_call_center: String, call_center_id: String, opposite_pin: String) -> TMessage {
         let me = UserDefaults.standard.string(forKey: "me")!
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.SEND_CHAT
@@ -165,6 +165,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.FORMAT] = ex_format
         tmessage.mBodies[CoreMessage_TMessageKey.IS_CALL_CENTER] = is_call_center
         tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = call_center_id
+        tmessage.mBodies[CoreMessage_TMessageKey.OPPOSITE_PIN] = opposite_pin
         
         if !image_id.isEmpty {
             tmessage.mBodies[CoreMessage_TMessageKey.IMAGE_ID] = image_id
@@ -1555,6 +1556,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
         tmessage.mPIN = me
         tmessage.mBodies[CoreMessage_TMessageKey.CHANNEL] = "\(p_channel)"
+        tmessage.mBodies[CoreMessage_TMessageKey.BUSINESS_ENTITY] = ""
         return tmessage
     }
     
@@ -1568,7 +1570,7 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
-    public static func acceptRequestCallCenter(channel:String, l_pin: String) -> TMessage {
+    public static func acceptRequestCallCenter(channel:String, l_pin: String, complaint_id: String) -> TMessage {
         let me = UserDefaults.standard.string(forKey: "me")!
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.ACCEPT_CALL_CENTER
@@ -1577,6 +1579,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = me
         tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
         tmessage.mBodies[CoreMessage_TMessageKey.CHANNEL] = channel
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = complaint_id
         return tmessage
     }
     
@@ -1619,6 +1622,7 @@ public class CoreMessage_TMessageBank {
         tmessage.mPIN = me
         tmessage.mBodies[CoreMessage_TMessageKey.TITLE] = title
         tmessage.mBodies[CoreMessage_TMessageKey.BROADCAST_FLAG] = broadcast_flag
+        tmessage.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT_ENG] = message
         tmessage.mBodies[CoreMessage_TMessageKey.MESSAGE_TEXT] = message
         tmessage.mBodies[CoreMessage_TMessageKey.START_DATE] = "\(starting_date)"
         tmessage.mBodies[CoreMessage_TMessageKey.END_DATE] = "\(ending_date)"
@@ -1656,7 +1660,22 @@ public class CoreMessage_TMessageBank {
         tmessage.mPIN = me
         tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = me
         tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
-        tmessage.mBodies[CoreMessage_TMessageKey.USER_TYPE] = user_type
+        tmessage.mBodies[CoreMessage_TMessageKey.TYPE] = user_type
+        return tmessage
+    }
+    
+    public static func getManagementContactCenterBNI(l_pin: String, type: String, category_id: String, area_id: String, is_second_layer: String) -> TMessage {
+        let me = UserDefaults.standard.string(forKey: "me")!
+        let tmessage = TMessage()
+        tmessage.mCode = CoreMessage_TMessageCode.MANAGEMENT_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = tmessage.mPIN
+        tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
+        tmessage.mBodies[CoreMessage_TMessageKey.TYPE] = type
+        tmessage.mBodies[CoreMessage_TMessageKey.CATEGORY_ID] = category_id
+        tmessage.mBodies[CoreMessage_TMessageKey.WORKING_AREA] = area_id
+        tmessage.mBodies[CoreMessage_TMessageKey.IS_SECOND_LAYER] = is_second_layer
         return tmessage
     }
     
@@ -1816,6 +1835,28 @@ public class CoreMessage_TMessageBank {
         return tmessage
     }
     
+    public static func getSendOTPChangeDeviceGaspol(p_email: String, p_idnumber: String, p_vercode: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.SEND_OTP_CHANGE_DEVICE_GASPOL
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.OTP] = p_vercode
+        tmessage.mBodies[CoreMessage_TMessageKey.EMAIL] = p_email
+        tmessage.mBodies[CoreMessage_TMessageKey.USER_ID] = p_idnumber
+        return tmessage
+    }
+    
+    public static func getSendVerifyChangeDevice(p_pin: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.SEND_VERIFY_CHANGE_DEVICE
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = p_pin
+        return tmessage
+    }
+    
     public static func getChangePersonInfo_New(p_f_pin: String) -> TMessage {
         let tmessage = TMessage()
         tmessage.mCode = CoreMessage_TMessageCode.CHANGE_PERSON_INFO
@@ -1834,4 +1875,171 @@ public class CoreMessage_TMessageBank {
         tmessage.mBodies[CoreMessage_TMessageKey.TYPE] = emotion_type
         return tmessage;
     }
+    
+    public static func getCCRoomIsActive(ticket_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.IS_ACTIVE_CALL_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        return tmessage
+    }
+
+    public static func getCCRoomInvite(l_pin: String, ticket_id: String, channel: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.INVITE_TO_ROOM_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        tmessage.mBodies[CoreMessage_TMessageKey.CHANNEL] = channel
+        return tmessage
+    }
+
+    public static func acceptCCRoomInvite(l_pin: String, type: Int, ticket_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.ACCEPT_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
+        tmessage.mBodies[CoreMessage_TMessageKey.TYPE] = "\(type)"
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        return tmessage
+    }
+
+    public static func leaveCCRoomInvite(ticket_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.INVITE_EXIT_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        return tmessage
+    }
+
+    public static func getCallCenterDraw(ticket_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.DRAW_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = tmessage.mPIN
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        return tmessage
+    }
+    
+    public static func getWebLoginQRCode(f_qrcode: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.WEB_LOGIN_QR
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mBodies[CoreMessage_TMessageKey.F_PIN] = me
+        tmessage.mBodies[CoreMessage_TMessageKey.KEY] = f_qrcode
+        return tmessage
+    }
+    
+    public static func getFormApproval(p_f_pin: String, p_ref_id: String, p_approve: String, p_note: String, p_sign: String) -> TMessage {
+        let tmessage = TMessage()
+        tmessage.mCode = CoreMessage_TMessageCode.APPROVE_FORM
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID();
+        tmessage.mPIN = p_f_pin;
+        tmessage.mBodies[CoreMessage_TMessageKey.REF_ID] = p_ref_id
+        tmessage.mBodies[CoreMessage_TMessageKey.STATUS] = p_approve
+        tmessage.mBodies[CoreMessage_TMessageKey.NOTE] = p_note
+        tmessage.mBodies[CoreMessage_TMessageKey.SIGN] = p_sign
+        return tmessage
+    }
+    
+    public static func pullGroupCategory() -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.PULL_GROUP_CATEGORY
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        return tmessage
+    }
+    
+    public static func pullFloatingButton() -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.PULL_FLOATING_BUTTON
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        return tmessage
+    }
+    
+    public static func getServiceBNI(p_pin: String) -> TMessage {
+        let tmessage = TMessage()
+        tmessage.mCode = CoreMessage_TMessageCode.GET_SERVICE_BNI
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = p_pin
+        return tmessage
+    }
+
+    public static func queueBNI(service_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.REQUEST_TICKET_BNI
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.CATEGORY_ID] = service_id
+        return tmessage
+    }
+
+    public static func isiPulsaBNI(value: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.REQUEST_TOP_UP_BNI
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.VALUE] = value
+        return tmessage
+    }
+    
+    public static func getCustomerInfo(rek: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.GET_CUSTOMER_INFO
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.CARD_ID] = rek
+        return tmessage
+    }
+    
+    public static func getRequestSecondContactCenter(p_channel: String, category_id: String, area_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.REQUEST_SECOND_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.CHANNEL] = p_channel
+        tmessage.mBodies[CoreMessage_TMessageKey.BUSINESS_ENTITY] = ""
+        tmessage.mBodies[CoreMessage_TMessageKey.CATEGORY_ID] = category_id
+        tmessage.mBodies[CoreMessage_TMessageKey.WORKING_AREA] = area_id
+        return tmessage;
+    }
+
+    public static func respondSecondContactCenter(l_pin: String, type: String, ticket_id: String) -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.RESPOND_SECOND_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID()
+        tmessage.mPIN = me
+        tmessage.mBodies[CoreMessage_TMessageKey.L_PIN] = l_pin
+        tmessage.mBodies[CoreMessage_TMessageKey.TYPE] = type
+        tmessage.mBodies[CoreMessage_TMessageKey.CALL_CENTER_ID] = ticket_id
+        return tmessage;
+    }
+
+    public static func getWorkingAreaContactCenter() -> TMessage {
+        let tmessage = TMessage()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        tmessage.mCode = CoreMessage_TMessageCode.GET_WORKING_AREA_CONTACT_CENTER
+        tmessage.mStatus = CoreMessage_TMessageUtil.getTID();
+        tmessage.mPIN = me
+        return tmessage;
+    }
 }

+ 25 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageCode.swift

@@ -14,6 +14,8 @@ public class CoreMessage_TMessageCode {
     
     public static let PUSH_INCOMING_EMAIL      = "PIM";
     
+    public static let WEB_LOGIN_QR = "WLQR";
+    
     public static let REQUEST_NPM                        = "B0";
     public static let PUSH_NPM                           = "B1";
     public static let CHANGE_USER_ID                   = "B2";
@@ -684,6 +686,19 @@ public class CoreMessage_TMessageCode {
     public static let EMAIL_CONTACT_CENTER = "CC07";
     public static let QUEUING_CONTACT_CENTER = "CC08";
     public static let STATUS_CONTACT_CENTER = "CC09";
+    public static let DRAW_CONTACT_CENTER = "CC10";
+    public static let ADD_CALL_CONTACT_CENTER = "CC11";
+    public static let END_CALL_CONTACT_CENTER = "CC12";
+    public static let IS_ACTIVE_CALL_CONTACT_CENTER = "CC13";
+    public static let INVITE_TO_ROOM_CONTACT_CENTER = "CC14";
+    public static let ACCEPT_CONTACT_CENTER = "CC15";
+    public static let PUSH_MEMBER_ROOM_CONTACT_CENTER = "CC16";
+    public static let INVITE_END_CONTACT_CENTER = "CC17";
+    public static let INVITE_EXIT_CONTACT_CENTER = "CC18";
+    public static let REQUEST_SECOND_CONTACT_CENTER = "CC19"; // sync
+    public static let PUSH_SECOND_CONTACT_CENTER = "CC20";
+    public static let RESPOND_SECOND_CONTACT_CENTER = "CC21"; // sync
+    public static let GET_WORKING_AREA_CONTACT_CENTER = "CC22"; // sync
     public static let PUSH_SUBSCRIPTION_SIZE = "PS01";
     
     public static let SIGN_UP_API = "SUA01";
@@ -696,6 +711,16 @@ public class CoreMessage_TMessageCode {
     public static let PUSH_DISCUSSION_COMMENT = "PDC";
     
     public static let SEND_OTP_CHANGE_DEVICE = "SOTD";
+    public static let SEND_OTP_CHANGE_DEVICE_GASPOL = "SOTG";
     public static let SEND_OTP_CHANGE_PROFILE = "SOTP";
     public static let SEND_VERIFY_CHANGE_DEVICE = "SVTD";
+    
+    public static let PULL_GROUP_CATEGORY = "PGC";
+    public static let GET_SERVICE_BNI = "GBNI";
+    public static let PUSH_SERVICE_BNI = "PBNI";
+    public static let REQUEST_TICKET_BNI = "RTB";
+    public static let REQUEST_TOP_UP_BNI = "RTP";
+    public static let PULL_FLOATING_BUTTON = "PFB";
+    
+    public static let GET_CUSTOMER_INFO = "GCI"; // sync
 }

+ 8 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageKey.swift

@@ -48,6 +48,7 @@ public class CoreMessage_TMessageKey {
     public static let AUTO_QUOTE     = "Bw"
     public static let KEY               = "Bt"
     public static let VALUE           = "Bu"
+    public static let PLATFORM       = "BX"
     public static let VB_STATUS        = "Bx"
     public static let OPPOSITE_PIN   = "OP1"
     public static let CHAT_ID        = "BA"
@@ -85,6 +86,7 @@ public class CoreMessage_TMessageKey {
     public static let GROUP_NAME         = "A05"
     public static let MESSAGE_SCOPE_ID = "A06"
     public static let MESSAGE_TEXT     = "A07"
+    public static let MESSAGE_TEXT_ENG     = "A07_eng"
     public static let MESSAGE_IMAGE     = "A08"
     //    public static let MESSAGE_STATUS     = "A09"
     public static let CONNECTED         = "A10"
@@ -212,7 +214,7 @@ public class CoreMessage_TMessageKey {
     public static let ITEM_REQUIREMENT     = "A115"
     
     public static let CONTACT                = "A116"
-    public static let ACCOUNT_TYPE         = "A117"
+    public static let BUSINESS_CATEGORY         = "A117"
     public static let CREDENTIAL           = "A118"
     
     public static let WISHLIST_ID          = "A119"
@@ -331,6 +333,7 @@ public class CoreMessage_TMessageKey {
     public static let ROC_SIZE = "ROS"
     public static let IS_EDUCATION = "IED"
     public static let IS_SUB_ACCOUNT = "ISA"
+    public static let IS_CHANGE_PROFILE = "ICP";
     public static let LEVEL_EDU = "lvledu"
     public static let MATERI_EDU = "mtredu"
     public static let FINALTEST_EDU = "tstedu"
@@ -475,4 +478,8 @@ public class CoreMessage_TMessageKey {
     public static let MEMBER_DATA = "MDT";
     public static let GET_LIST_LS = "GTS";
     
+    public static let PHONE_NUMBER = "PHN";
+    public static let WORKING_AREA = "WKA";
+    public static let IS_SECOND_LAYER = "ISL";
+    
 }

+ 19 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/CoreMessage_TMessageUtil.swift

@@ -99,7 +99,10 @@ public class Database {
                                 "'real_name' TEXT," +
                                 "'is_sub_account' TEXT DEFAULT (0)," +
                                 "'last_sign' TEXT," +
-                                "'android_id' TEXT" +
+                                "'android_id' TEXT," +
+                                "'is_change_profile' TEXT DEFAULT (0)," +
+                                "'area' TEXT DEFAULT (0)," +
+                                "'is_second_layer' TEXT DEFAULT (0)" +
                                 ")", values: nil)
         
         try fmdb.executeUpdate("CREATE INDEX IF NOT EXISTS index_message_id on BUDDY (msisdn)", values: nil)
@@ -374,6 +377,21 @@ public class Database {
         "'value' text," +
         "'type' text," +
         "'sq_no' integer)", values: nil)
+        
+        try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'SERVICE_BANK' (" +
+        "'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL," +
+        "'service_id' text NOT NULL UNIQUE," +
+        "'service_name' text," +
+        "'description' text," +
+        "'parent' text," +
+        "'is_tablet' text)", values: nil)
+        
+        try fmdb.executeUpdate("CREATE TABLE IF NOT EXISTS 'WORKING_AREA' (" +
+        "'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL," +
+        "'area_id' text NOT NULL UNIQUE," +
+        "'name' text," +
+        "'parent' text," +
+        "'level' text)", values: nil)
     }
     
     public func executes(fmdb: FMDatabase, queries: [String]) {

+ 359 - 47
appbuilder-ios/NexilisLite/NexilisLite/Source/Download.swift

@@ -10,9 +10,9 @@ import Foundation
 import UIKit
 import SDWebImage
 
-public extension Date {
+extension Date {
     
-    func currentTimeMillis() -> Int {
+    public func currentTimeMillis() -> Int {
         return Int(self.timeIntervalSince1970 * 1000)
     }
     
@@ -26,12 +26,12 @@ public extension Date {
         return Int64((self.timeIntervalSince1970 * 1000.0).rounded())
     }
     
-    init(milliseconds:Int64) {
+    public init(milliseconds:Int64) {
         self = Date(timeIntervalSince1970: TimeInterval(milliseconds) / 1000)
     }
 }
 
-public extension String {
+extension String {
     
     func toNormalString() -> String {
         let _source = self.replacingOccurrences(of: "+", with: "%20")
@@ -60,13 +60,13 @@ public extension String {
         return source
     }
     
-    func matches(_ regex: String) -> Bool {
+    public func matches(_ regex: String) -> Bool {
         return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil
     }
     
 }
 
-public extension Int {
+extension Int {
     
     func toHex() -> String {
         return String(format: "%02X", self)
@@ -74,8 +74,8 @@ public extension Int {
     
 }
 
-public extension UIApplication {
-    static var appVersion: String? {
+extension UIApplication {
+    public static var appVersion: String? {
         return Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
     }
     
@@ -95,9 +95,9 @@ public extension UIApplication {
     }
 }
 
-public extension UIView {
+extension UIView {
     
-    func anchor(top: NSLayoutYAxisAnchor? = nil,
+    public func anchor(top: NSLayoutYAxisAnchor? = nil,
                 left: NSLayoutXAxisAnchor? = nil,
                 bottom: NSLayoutYAxisAnchor? = nil,
                 right: NSLayoutXAxisAnchor? = nil,
@@ -109,6 +109,10 @@ public extension UIView {
                 centerY: NSLayoutYAxisAnchor? = nil,
                 width: CGFloat = 0,
                 height: CGFloat = 0,
+                minHeight: CGFloat = 0,
+                maxHeight: CGFloat = 0,
+                minWidth: CGFloat = 0,
+                maxWidth: CGFloat = 0,
                 dynamicLeft: Bool = false,
                 dynamicRight: Bool = false) {
         
@@ -140,17 +144,35 @@ public extension UIView {
         if let centerY = centerY {
             centerYAnchor.constraint(equalTo: centerY).isActive = true
         }
-        if height != 0 {
-            heightAnchor.constraint(equalToConstant: height).isActive = true
+        if height != 0 || minHeight != 0 || maxHeight != 0 {
+            if minHeight != 0 && maxHeight != 0 {
+                heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight).isActive = true
+                heightAnchor.constraint(lessThanOrEqualToConstant: maxHeight).isActive = true
+            } else if minHeight != 0 && maxHeight == 0 {
+                heightAnchor.constraint(greaterThanOrEqualToConstant: minHeight).isActive = true
+            } else if minHeight == 0 && maxHeight != 0 {
+                heightAnchor.constraint(lessThanOrEqualToConstant: maxHeight).isActive = true
+            } else {
+                heightAnchor.constraint(equalToConstant: height).isActive = true
+            }
         }
-        if width != 0 {
-            widthAnchor.constraint(equalToConstant: width).isActive = true
+        if width != 0 || minWidth != 0 || maxWidth != 0 {
+            if minWidth != 0 && maxWidth != 0 {
+                heightAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
+                heightAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
+            } else if minWidth != 0 && maxWidth == 0 {
+                heightAnchor.constraint(greaterThanOrEqualToConstant: minWidth).isActive = true
+            } else if minWidth == 0 && maxWidth != 0 {
+                heightAnchor.constraint(lessThanOrEqualToConstant: maxWidth).isActive = true
+            } else {
+                widthAnchor.constraint(equalToConstant: width).isActive = true
+            }
         }
     }
     
 }
 
-public extension UIViewController {
+extension UIViewController {
     
     var previousViewController: UIViewController? {
         guard let navigationController = navigationController else { return nil }
@@ -160,7 +182,7 @@ public extension UIViewController {
     
 }
 
-public extension UIImage {
+extension UIImage {
     func resize(target: CGSize) -> UIImage {
         // Determine the scale factor that preserves aspect ratio
         let widthRatio = target.width / size.width
@@ -190,14 +212,14 @@ public extension UIImage {
     }
 }
 
-public extension UIImage {
+extension UIImage {
     
     var isPortrait:  Bool    { size.height > size.width }
     var isLandscape: Bool    { size.width > size.height }
     var breadth:     CGFloat { min(size.width, size.height) }
     var breadthSize: CGSize  { .init(width: breadth, height: breadth) }
     var breadthRect: CGRect  { .init(origin: .zero, size: breadthSize) }
-    var circleMasked: UIImage? {
+    public var circleMasked: UIImage? {
         guard let cgImage = cgImage?
                 .cropping(to: .init(origin: .init(x: isLandscape ? ((size.width-size.height)/2).rounded(.down) : 0,
                                                   y: isPortrait  ? ((size.height-size.width)/2).rounded(.down) : 0),
@@ -217,7 +239,7 @@ extension NSObject {
     
     private static var urlStore = [String:String]()
 
-    public func getImage(name url: String, placeholderImage: UIImage? = nil, isCircle: Bool = false,  completion: @escaping (Bool, Bool, UIImage?)->()) {
+    public func getImage(name url: String, placeholderImage: UIImage? = nil, isCircle: Bool = false, tableView: UITableView? = nil, indexPath: IndexPath? = nil, completion: @escaping (Bool, Bool, UIImage?)->()) {
         let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
         type(of: self).urlStore[tmpAddress] = url
         if url.isEmpty {
@@ -238,6 +260,11 @@ extension NSObject {
                     }
                     
                     DispatchQueue.main.async {
+                        if tableView != nil {
+                            tableView!.beginUpdates()
+                            tableView!.reloadRows(at: [indexPath!], with: .none)
+                            tableView!.endUpdates()
+                        }
                         if type(of: self).urlStore[tmpAddress] == name {
                             let image = UIImage(contentsOfFile: file.path)?.sd_resizedImage(with: CGSize(width: 400, height: 400), scaleMode: .aspectFill)
                             completion(true, true, isCircle ? image?.circleMasked : image)
@@ -303,6 +330,10 @@ extension UIColor {
         return renderColor(hex: "#FFA03E")
     }
     
+    public static var orangeBNI: UIColor {
+        return renderColor(hex: "#EE6600")
+    }
+    
     public static var greenColor: UIColor {
         return renderColor(hex: "#C7EA46")
     }
@@ -405,6 +436,7 @@ extension UIViewController {
         
         toastContainer.addSubview(toastLabel)
         controller.view.addSubview(toastContainer)
+        controller.view.bringSubviewToFront(toastContainer)
         
         toastLabel.translatesAutoresizingMaskIntoConstraints = false
         toastContainer.translatesAutoresizingMaskIntoConstraints = false
@@ -476,7 +508,7 @@ extension UITextView {
 
 }
 
-public extension String {
+extension String {
     
     func substring(from: Int?, to: Int?) -> String {
         if let start = from {
@@ -542,7 +574,7 @@ public extension String {
         return length
     }
     
-    func richText(isUcList: Bool = false, isEditing: Bool = false, first: Int = 0, last: Int = 0) -> NSAttributedString {
+    public func richText(isUcList: Bool = false, isEditing: Bool = false, first: Int = 0, last: Int = 0) -> NSAttributedString {
         var font = UIFont.systemFont(ofSize: 12)
         if isUcList {
             font = UIFont.systemFont(ofSize: 10)
@@ -705,7 +737,7 @@ public extension String {
     
 }
 
-public extension UIFont {
+extension UIFont {
     var bold: UIFont {
         return with(traits: .traitBold)
     } // bold
@@ -723,8 +755,8 @@ public extension UIFont {
     } // with(traits:)
 }
 
-public extension UILabel {
-    func set(image: UIImage, with text: String, size: CGFloat, y: CGFloat) {
+extension UILabel {
+    public func set(image: UIImage, with text: String, size: CGFloat, y: CGFloat) {
         let attachment = NSTextAttachment()
         attachment.image = image
         attachment.bounds = CGRect(x: 0, y: y, width: size, height: size)
@@ -739,7 +771,7 @@ public extension UILabel {
         self.attributedText = mutableAttributedString
     }
     
-    func setAttributeText(image: UIImage, with textMutable: NSAttributedString, size: CGFloat, y: CGFloat) {
+    public func setAttributeText(image: UIImage, with textMutable: NSAttributedString, size: CGFloat, y: CGFloat) {
         let attachment = NSTextAttachment()
         attachment.image = image
         attachment.bounds = CGRect(x: 0, y: y, width: size, height: size)
@@ -774,22 +806,22 @@ extension Bundle {
     
 }
 
-extension UIFont {
-    
-    static func register(from url: URL) throws {
-        guard let fontDataProvider = CGDataProvider(url: url as CFURL) else {
-            throw fatalError("Could not create font data provider for \(url).")
-        }
-        let font = CGFont(fontDataProvider)
-        var error: Unmanaged<CFError>?
-        guard CTFontManagerRegisterGraphicsFont(font!, &error) else {
-            throw error!.takeUnretainedValue()
-        }
-    }
-    
-}
-
-public extension UIButton {
+//extension UIFont {
+//    
+//    static func register(from url: URL) throws {
+//        guard let fontDataProvider = CGDataProvider(url: url as CFURL) else {
+//            throw fatalError("Could not create font data provider for \(url).")
+//        }
+//        let font = CGFont(fontDataProvider)
+//        var error: Unmanaged<CFError>?
+//        guard CTFontManagerRegisterGraphicsFont(font!, &error) else {
+//            throw error!.takeUnretainedValue()
+//        }
+//    }
+//    
+//}
+
+extension UIButton {
     private func actionHandleBlock(action:(() -> Void)? = nil) {
         struct __ {
             static var action :(() -> Void)?
@@ -805,20 +837,24 @@ public extension UIButton {
         self.actionHandleBlock()
     }
     
-    func actionHandle(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
+    public func actionHandle(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
         self.actionHandleBlock(action: action)
         self.addTarget(self, action: #selector(self.triggerActionHandleBlock), for: control)
     }
 }
 
-public extension UINavigationController {
+extension UINavigationController {
     func replaceAllViewController(with viewController: UIViewController, animated: Bool) {
         pushViewController(viewController, animated: animated)
         viewControllers.removeSubrange(1...viewControllers.count - 2)
     }
+    
+    var rootViewController : UIViewController? {
+        return viewControllers.first
+    }
 }
 
-public extension UIImageView {
+extension UIImageView {
 
     private static var taskKey = 0
     private static var urlKey = 0
@@ -920,13 +956,13 @@ public extension UIImageView {
 
 extension UITextField {
 
-    enum PaddingSide {
+    public enum PaddingSide {
         case left(CGFloat)
         case right(CGFloat)
         case both(CGFloat)
     }
 
-    func addPadding(_ padding: PaddingSide) {
+    public func addPadding(_ padding: PaddingSide) {
 
         self.leftViewMode = .always
         self.layer.masksToBounds = true
@@ -956,7 +992,7 @@ extension UITextField {
     }
 }
 
-public class ImageCache {
+class ImageCache {
     private let cache = NSCache<NSString, UIImage>()
     private var observer: NSObjectProtocol!
 
@@ -982,3 +1018,279 @@ public class ImageCache {
         cache.setObject(image, forKey: key as NSString)
     }
 }
+
+//let alert = UIAlertController(title: "", message: "\n\n\n\n\n\n\n\n\n\n".localized(), preferredStyle: .alert)
+//let newWidth = UIScreen.main.bounds.width * 0.90 - 270
+//// update width constraint value for main view
+//if let viewWidthConstraint = alert.view.constraints.filter({ return $0.firstAttribute == .width }).first{
+//    viewWidthConstraint.constant = newWidth
+//}
+//// update width constraint value for container view
+//if let containerViewWidthConstraint = alert.view.subviews.first?.constraints.filter({ return $0.firstAttribute == .width }).first {
+//    containerViewWidthConstraint.constant = newWidth
+//}
+//let titleFont = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18), NSAttributedString.Key.foregroundColor: UIColor.black]
+//let titleAttrString = NSMutableAttributedString(string: m["MERNAM"]!.localized(), attributes: titleFont)
+//alert.setValue(titleAttrString, forKey: "attributedTitle")
+//alert.view.subviews.first?.subviews.first?.subviews.first?.backgroundColor = .lightGray
+//alert.view.tintColor = .black
+//if fileType != BroadcastViewController.FILE_TYPE_CHAT{
+//    let height:NSLayoutConstraint = NSLayoutConstraint(item: alert.view!, attribute: NSLayoutConstraint.Attribute.height, relatedBy: NSLayoutConstraint.Relation.equal, toItem: nil, attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: UIScreen.main.bounds.height * 0.6)
+//    alert.view.addConstraint(height)
+//}
+//
+//var containerView = UIView(frame: CGRect(x: 20, y: 60, width: alert.view.bounds.size.width * 0.9 - 40, height: 350))
+//if fileType == BroadcastViewController.FILE_TYPE_CHAT {
+//    containerView = UIView(frame: CGRect(x: 20, y: 60, width: alert.view.bounds.size.width * 0.9 - 40, height: 100))
+//}
+//alert.view.addSubview(containerView)
+//containerView.layer.cornerRadius = 10.0
+//containerView.clipsToBounds = true
+//
+//let buttonClose = UIButton(type: .close)
+//buttonClose.frame = CGRect(x: alert.view.bounds.size.width * 0.9 - 50, y: 15, width: 30, height: 30)
+//buttonClose.layer.cornerRadius = 15.0
+//buttonClose.clipsToBounds = true
+//buttonClose.backgroundColor = .secondaryColor.withAlphaComponent(0.5)
+//buttonClose.actionHandle(controlEvents: .touchUpInside,
+// ForAction:{() -> Void in
+//    alert.dismiss(animated: true, completion: nil)
+// })
+//alert.view.addSubview(buttonClose)
+//
+//let titleBroadcast = UILabel()
+//containerView.addSubview(titleBroadcast)
+//titleBroadcast.translatesAutoresizingMaskIntoConstraints = false
+//NSLayoutConstraint.activate([
+//    titleBroadcast.topAnchor.constraint(equalTo: containerView.topAnchor),
+//    titleBroadcast.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
+//    titleBroadcast.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
+//])
+//titleBroadcast.font = UIFont.systemFont(ofSize: 18)
+//titleBroadcast.numberOfLines = 0
+//titleBroadcast.text = m[CoreMessage_TMessageKey.TITLE]
+//titleBroadcast.textColor = .black
+//
+//let descBroadcast = UILabel()
+//containerView.addSubview(descBroadcast)
+//descBroadcast.translatesAutoresizingMaskIntoConstraints = false
+//NSLayoutConstraint.activate([
+//    descBroadcast.topAnchor.constraint(equalTo: titleBroadcast.bottomAnchor, constant: 10),
+//    descBroadcast.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
+//    descBroadcast.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
+//])
+//descBroadcast.font = UIFont.systemFont(ofSize: 15)
+//descBroadcast.numberOfLines = 0
+//descBroadcast.text = m[CoreMessage_TMessageKey.MESSAGE_TEXT_ENG]
+//descBroadcast.textColor = .black
+//
+//let stringLink = m[CoreMessage_TMessageKey.LINK] ?? ""
+//let linkBroadcast = UILabel()
+//if !stringLink.isEmpty {
+//    containerView.addSubview(linkBroadcast)
+//    linkBroadcast.translatesAutoresizingMaskIntoConstraints = false
+//    NSLayoutConstraint.activate([
+//        linkBroadcast.topAnchor.constraint(equalTo: descBroadcast.bottomAnchor, constant: 10),
+//        linkBroadcast.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
+//        linkBroadcast.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
+//    ])
+//    linkBroadcast.font = UIFont.systemFont(ofSize: 15)
+//    linkBroadcast.isUserInteractionEnabled = true
+//    linkBroadcast.numberOfLines = 0
+//    let attributedString = NSMutableAttributedString(string: stringLink, attributes:[NSAttributedString.Key.link: URL(string: stringLink)!])
+//    linkBroadcast.attributedText = attributedString
+//    let tap = ObjectGesture(target: self, action: #selector(tapLinkBroadcast))
+//    tap.message_id = stringLink
+//    linkBroadcast.addGestureRecognizer(tap)
+//}
+//
+//let dottedLine = UIView()
+//containerView.addSubview(dottedLine)
+//dottedLine.translatesAutoresizingMaskIntoConstraints = false
+//var constraintDottedLine = dottedLine.topAnchor.constraint(equalTo: descBroadcast.bottomAnchor, constant: 20)
+//if !stringLink.isEmpty{
+//    constraintDottedLine = dottedLine.topAnchor.constraint(equalTo: linkBroadcast.bottomAnchor, constant: 20)
+//}
+//NSLayoutConstraint.activate([
+//    constraintDottedLine,
+//    dottedLine.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
+//    dottedLine.widthAnchor.constraint(equalToConstant: alert.view.bounds.size.width * 0.9 - 40),
+//    dottedLine.heightAnchor.constraint(equalToConstant: 2)
+//])
+//dottedLine.backgroundColor = .black.withAlphaComponent(0.1)
+//let shapeLayer = CAShapeLayer()
+//shapeLayer.strokeColor = UIColor.black.withAlphaComponent(0.2).cgColor
+//shapeLayer.lineWidth = 2
+//// passing an array with the values [2,3] sets a dash pattern that alternates between a 2-user-space-unit-long painted segment and a 3-user-space-unit-long unpainted segment
+//shapeLayer.lineDashPattern = [2,3]
+//
+//let path = CGMutablePath()
+//path.addLines(between: [CGPoint(x: 0, y: 0),
+//                        CGPoint(x: alert.view.bounds.size.width * 0.9 - 20, y: 0)])
+//shapeLayer.path = path
+//dottedLine.layer.addSublayer(shapeLayer)
+//
+//let thumb = m[CoreMessage_TMessageKey.THUMB_ID] ?? ""
+//let image = m[CoreMessage_TMessageKey.IMAGE_ID] ?? ""
+//let video = m[CoreMessage_TMessageKey.VIDEO_ID] ?? ""
+//let file = m[CoreMessage_TMessageKey.FILE_ID] ?? ""
+//if fileType != BroadcastViewController.FILE_TYPE_CHAT {
+//    let imageBroadcast = UIImageView()
+//    containerView.addSubview(imageBroadcast)
+//    imageBroadcast.translatesAutoresizingMaskIntoConstraints = false
+//    NSLayoutConstraint.activate([
+//        imageBroadcast.topAnchor.constraint(equalTo: dottedLine.bottomAnchor, constant: 20),
+//        imageBroadcast.widthAnchor.constraint(equalToConstant: alert.view.bounds.size.width * 0.9 - 40),
+//        imageBroadcast.heightAnchor.constraint(equalToConstant: 250)
+//    ])
+//    imageBroadcast.layer.cornerRadius = 10.0
+//    imageBroadcast.clipsToBounds = true
+//    if fileType != BroadcastViewController.FILE_TYPE_DOCUMENT {
+//        imageBroadcast.contentMode = .scaleAspectFill
+//        imageBroadcast.setImage(name: thumb)
+//
+//        if fileType == BroadcastViewController.FILE_TYPE_VIDEO {
+//            let imagePlay = UIImageView(image: UIImage(systemName: "play.circle.fill"))
+//            imageBroadcast.addSubview(imagePlay)
+//            imagePlay.clipsToBounds = true
+//            imagePlay.translatesAutoresizingMaskIntoConstraints = false
+//            imagePlay.centerYAnchor.constraint(equalTo: imageBroadcast.centerYAnchor).isActive = true
+//            imagePlay.centerXAnchor.constraint(equalTo: imageBroadcast.centerXAnchor).isActive = true
+//            imagePlay.widthAnchor.constraint(equalToConstant: 60).isActive = true
+//            imagePlay.heightAnchor.constraint(equalToConstant: 60).isActive = true
+//            imagePlay.tintColor = .gray.withAlphaComponent(0.5)
+//        }
+//    } else {
+//        imageBroadcast.image = UIImage(systemName: "doc.fill")
+//        imageBroadcast.tintColor = .mainColor
+//        imageBroadcast.contentMode = .scaleAspectFit
+//    }
+//
+//    imageBroadcast.actionHandle(controlEvents: .touchUpInside,
+//     ForAction:{() -> Void in
+//        let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+//        let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+//        let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+//        if fileType == BroadcastViewController.FILE_TYPE_IMAGE {
+//            if let dirPath = paths.first {
+//                let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(image)
+//                if FileManager.default.fileExists(atPath: imageURL.path) {
+//                    let image    = UIImage(contentsOfFile: imageURL.path)
+//                    let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+//                    previewImageVC.image = image
+//                    previewImageVC.isHiddenTextField = true
+//                    previewImageVC.modalPresentationStyle = .overFullScreen
+//                    previewImageVC.modalTransitionStyle  = .crossDissolve
+//                    let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                    if checkViewController != nil {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.present(previewImageVC, animated: true, completion: nil)
+//                    } else {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(previewImageVC, animated: true, completion: nil)
+//                    }
+//                } else {
+//                    Download().start(forKey: image) { (name, progress) in
+//                        guard progress == 100 else {
+//                            return
+//                        }
+//
+//                        DispatchQueue.main.async {
+//                            let image    = UIImage(contentsOfFile: imageURL.path)
+//                            let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
+//                            previewImageVC.image = image
+//                            previewImageVC.isHiddenTextField = true
+//                            previewImageVC.modalPresentationStyle = .overFullScreen
+//                            previewImageVC.modalTransitionStyle  = .crossDissolve
+//                            let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                            if checkViewController != nil {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.present(previewImageVC, animated: true, completion: nil)
+//                            } else {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(previewImageVC, animated: true, completion: nil)
+//                            }
+//                        }
+//                    }
+//                }
+//            }
+//        } else if fileType == BroadcastViewController.FILE_TYPE_VIDEO {
+//            if let dirPath = paths.first {
+//                let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(video)
+//                if FileManager.default.fileExists(atPath: videoURL.path) {
+//                    let player = AVPlayer(url: videoURL as URL)
+//                    let playerVC = AVPlayerViewController()
+//                    playerVC.player = player
+//                    playerVC.modalPresentationStyle = .custom
+//                    let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                    if checkViewController != nil {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.present(playerVC, animated: true, completion: nil)
+//                    } else {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(playerVC, animated: true, completion: nil)
+//                    }
+//                } else {
+//                    Download().start(forKey: video) { (name, progress) in
+//                        DispatchQueue.main.async {
+//                            guard progress == 100 else {
+//                                return
+//                            }
+//                            let player = AVPlayer(url: videoURL as URL)
+//                            let playerVC = AVPlayerViewController()
+//                            playerVC.player = player
+//                            playerVC.modalPresentationStyle = .custom
+//                            let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                            if checkViewController != nil {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.present(playerVC, animated: true, completion: nil)
+//                            } else {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(playerVC, animated: true, completion: nil)
+//                            }
+//                        }
+//                    }
+//                }
+//            }
+//        } else if fileType == BroadcastViewController.FILE_TYPE_DOCUMENT {
+//            if let dirPath = paths.first {
+//                let fileURL = URL(fileURLWithPath: dirPath).appendingPathComponent(file)
+//                if FileManager.default.fileExists(atPath: fileURL.path) {
+//                    previewItem = fileURL as NSURL
+//                    let previewController = QLPreviewController()
+//                    let rightBarButton = UIBarButtonItem()
+//                    previewController.navigationItem.rightBarButtonItem = rightBarButton
+//                    previewController.dataSource = self
+//                    previewController.modalPresentationStyle = .overFullScreen
+//
+//                    let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                    if checkViewController != nil {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.show(previewController, sender: nil)
+//                    } else {
+//                        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.show(previewController, sender: nil)
+//                    }
+//                } else {
+//                    Download().start(forKey: file) { (name, progress) in
+//                        DispatchQueue.main.async {
+//                            guard progress == 100 else {
+//                                return
+//                            }
+//                            previewItem = fileURL as NSURL
+//                            let previewController = QLPreviewController()
+//                            let rightBarButton = UIBarButtonItem()
+//                            previewController.navigationItem.rightBarButtonItem = rightBarButton
+//                            previewController.dataSource = self
+//                            previewController.modalPresentationStyle = .overFullScreen
+//
+//                            let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//                            if checkViewController != nil {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.show(previewController, sender: nil)
+//                            } else {
+//                                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.show(previewController, sender: nil)
+//                            }
+//                        }
+//                    }
+//                }
+//            }
+//        }
+//     })
+//}
+//
+//let checkViewController = UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController
+//if checkViewController != nil {
+//    UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.presentedViewController?.present(alert, animated: true, completion: nil)
+//} else {
+//    UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(alert, animated: true, completion: nil)
+//}

+ 416 - 199
appbuilder-ios/NexilisLite/NexilisLite/Source/FloatingButton/FloatingButton.swift

@@ -6,20 +6,33 @@
 //
 
 import UIKit
+import nuSDKService
+import NotificationBannerSwift
+
 
 public class FloatingButton: UIView {
     
     var groupView: UIStackView!
-    var button_cc: UIButton!
-    var button_chat: UIButton!
-    var button_call: UIButton!
-    var button_streaming: UIButton!
-    var nexilis_button: UIButton!
+    var scrollView: UIScrollView!
+    var button_fb1: UIButton!
+    var button_fb2: UIButton!
+    var button_fb3: UIButton!
+    var button_fb4: UIButton!
+    var nexilis_button: UIImageView!
+    var nexilis_pin: UIImageView!
+    var leadingConstraintPin: NSLayoutConstraint!
+    var bottomConstraintPin: NSLayoutConstraint!
+    var trailingConstraintPin: NSLayoutConstraint!
+    var topConstraintPin: NSLayoutConstraint!
     var lastPosY: CGFloat?
+    var lastImageButton = ""
+    var iconCC = ""
     
     let indicatorCounterFB = UIView()
-    let labelCounter = UILabel()
-    var topConstraint = NSLayoutConstraint()
+    let labelCounterFB = UILabel()
+    let indicatorCounterFBBig = UIImageView()
+    
+    var datePull: Date?
     
     var panGesture: UIPanGestureRecognizer?
     
@@ -36,12 +49,16 @@ public class FloatingButton: UIView {
     }
     
     private func commonInit() {
+        backgroundColor = .clear
+        frame = CGRect(x: UIScreen.main.bounds.width - 50, y: (UIScreen.main.bounds.height / 2) - 50, width: 50.0, height: 50.0)
+        
         panGesture = UIPanGestureRecognizer(target: self, action: #selector(draggedView(_:)))
         addGestureRecognizer(panGesture!)
         
-        nexilis_button = UIButton()
+        nexilis_button = UIImageView()
         nexilis_button.translatesAutoresizingMaskIntoConstraints = false
-        nexilis_button.setImage(UIImage(named: "pb_ball", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
+        nexilis_button.isUserInteractionEnabled = true
+        nexilis_button.image = UIImage(named: "pb_button", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
         
         let qmeraTap = UITapGestureRecognizer(target: self, action: #selector(qmeraTap))
         qmeraTap.numberOfTouchesRequired = 1
@@ -52,56 +69,197 @@ public class FloatingButton: UIView {
         
         addSubview(nexilis_button)
         
-        nexilis_button.widthAnchor.constraint(equalTo: widthAnchor, multiplier: 1).isActive = true
-        nexilis_button.heightAnchor.constraint(equalTo: widthAnchor, multiplier: 1).isActive = true
+        nexilis_button.widthAnchor.constraint(equalToConstant: 50.0).isActive = true
+        nexilis_button.heightAnchor.constraint(equalToConstant: 50.0).isActive = true
         nexilis_button.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
         nexilis_button.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
         
+        scrollView = UIScrollView()
+        scrollView.translatesAutoresizingMaskIntoConstraints = false
+        scrollView.layer.borderWidth = 1.0
+        scrollView.layer.borderColor = UIColor.white.cgColor
+        scrollView.layer.cornerRadius = 8.0
+        scrollView.layer.masksToBounds = true
+        scrollView.backgroundColor = .black.withAlphaComponent(0.25)
+        addSubview(scrollView)
+        
+        scrollView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
+        scrollView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
+        scrollView.topAnchor.constraint(equalTo: topAnchor).isActive = true
+        scrollView.bottomAnchor.constraint(equalTo: nexilis_button.topAnchor).isActive = true
+        
         groupView = UIStackView()
         groupView.translatesAutoresizingMaskIntoConstraints = false
         groupView.axis = .vertical
         groupView.distribution = .fillEqually
+
+        scrollView.addSubview(groupView)
+
+        groupView.widthAnchor.constraint(equalToConstant: 40).isActive = true
+        groupView.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 5).isActive = true
+        groupView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -5).isActive = true
+        groupView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: 6).isActive = true
         
-        addSubview(groupView)
-        
-        groupView.widthAnchor.constraint(equalTo: widthAnchor).isActive = true
-        groupView.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
-        groupView.topAnchor.constraint(equalTo: topAnchor).isActive = true
-        groupView.bottomAnchor.constraint(equalTo: nexilis_button.topAnchor).isActive = true
-        
-        button_cc = UIButton()
-        button_cc.translatesAutoresizingMaskIntoConstraints = false
-        button_cc.setImage(UIImage(named: "pb_button_cc", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
-        
-        groupView.addArrangedSubview(button_cc)
-        
-        button_chat = UIButton()
-        button_chat.translatesAutoresizingMaskIntoConstraints = false
-        button_chat.setImage(UIImage(named: "pb_button_chat", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
-        
-        groupView.addArrangedSubview(button_chat)
-        
-        button_call = UIButton()
-        button_call.translatesAutoresizingMaskIntoConstraints = false
-        button_call.setImage(UIImage(named: "pb_button_call", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
-        
-        groupView.addArrangedSubview(button_call)
-        
-        button_streaming = UIButton()
-        button_streaming.translatesAutoresizingMaskIntoConstraints = false
-        button_streaming.setImage(UIImage(named: "pb_button_stream", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
-        
-        groupView.addArrangedSubview(button_streaming)
-        
-        button_chat.addTarget(self, action: #selector(chatTap), for: .touchUpInside)
-        button_call.addTarget(self, action: #selector(callTap), for: .touchUpInside)
-        button_streaming.addTarget(self, action: #selector(streamTap), for: .touchUpInside)
-        button_cc.addTarget(self, action: #selector(ccTap), for: .touchUpInside)
+        pullButton()
         
         let center: NotificationCenter = NotificationCenter.default
+        center.addObserver(self, selector: #selector(imageFBUpdate(notification:)), name: NSNotification.Name(rawValue: "imageFBUpdate"), object: nil)
         center.addObserver(self, selector: #selector(checkCounter), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
         center.addObserver(self, selector: #selector(checkCounter), name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil)
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(hideButton))
+        tapGesture.cancelsTouchesInView = false
+        UIApplication.shared.windows.first?.rootViewController?.view.addGestureRecognizer(tapGesture)
+    }
+    
+    private func pullButton() {
+        if datePull == nil || Int(Date().timeIntervalSince(datePull!)) >= 60 {
+            datePull = Date()
+        } else if Int(Date().timeIntervalSince(datePull!)) < 60 {
+            return
+        }
+        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            return
+        }
+        DispatchQueue.global().async { [self] in
+            if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.pullFloatingButton(), timeout: 30 * 1000){
+                if response.isOk() {
+                    let data = response.getBody(key: CoreMessage_TMessageKey.DATA, default_value: "")
+                    if !data.isEmpty {
+                        if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                            DispatchQueue.main.async { [self] in
+                                groupView.subviews.forEach({ $0.removeFromSuperview() })
+                                if jsonArray.count == 0 {
+                                    getDefaultButton()
+                                } else {
+                                    for json in jsonArray {
+                                        if (json["package_id"] as! String).contains("_fb1") {
+                                            button_fb1 = UIButton()
+                                            button_fb1.heightAnchor.constraint(equalToConstant: 40).isActive = true
+                                            button_fb1.translatesAutoresizingMaskIntoConstraints = false
+                                            DispatchQueue.global().async {
+                                                let data = try? Data(contentsOf: URL(string: "https://qmera.io/filepalio/image/\(json["icon"]!!)")!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                                                DispatchQueue.main.async { [self] in
+                                                    if data != nil {
+                                                        button_fb1.setImage(UIImage(data: data!), for: .normal)
+                                                    }
+                                                }
+                                            }
+                                            groupView.addArrangedSubview(button_fb1)
+                                            button_fb1.addTarget(self, action: #selector(fb1Tap), for: .touchUpOutside)
+                                        } else if (json["package_id"] as! String).contains("_fb2") {
+                                            button_fb2 = UIButton()
+                                            button_fb2.heightAnchor.constraint(equalToConstant: 40).isActive = true
+                                            button_fb2.translatesAutoresizingMaskIntoConstraints = false
+                                            DispatchQueue.global().async {
+                                                let data = try? Data(contentsOf: URL(string: "https://qmera.io/filepalio/image/\(json["icon"]!!)")!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                                                DispatchQueue.main.async { [self] in
+                                                    if data != nil {
+                                                        button_fb2.setImage(UIImage(data: data!), for: .normal)
+                                                    }
+                                                }
+                                            }
+                                            groupView.addArrangedSubview(button_fb2)
+                                            button_fb2.addTarget(self, action: #selector(fb2Tap), for: .touchUpOutside)
+                                            checkCounter()
+                                        } else if (json["package_id"] as! String).contains("_fb3") {
+                                            button_fb3 = UIButton()
+                                            button_fb3.heightAnchor.constraint(equalToConstant: 40).isActive = true
+                                            button_fb3.translatesAutoresizingMaskIntoConstraints = false
+                                            DispatchQueue.global().async {
+                                                let data = try? Data(contentsOf: URL(string: "https://qmera.io/filepalio/image/\(json["icon"]!!)")!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                                                DispatchQueue.main.async { [self] in
+                                                    if data != nil {
+                                                        button_fb3.setImage(UIImage(data: data!), for: .normal)
+                                                    }
+                                                }
+                                            }
+                                            groupView.addArrangedSubview(button_fb3)
+                                            button_fb3.addTarget(self, action: #selector(fb3Tap), for: .touchUpOutside)
+                                        } else if (json["package_id"] as! String).contains("_fb4") {
+                                            button_fb4 = UIButton()
+                                            button_fb4.heightAnchor.constraint(equalToConstant: 40).isActive = true
+                                            button_fb4.translatesAutoresizingMaskIntoConstraints = false
+                                            iconCC = json["icon"] as! String
+                                            DispatchQueue.global().async {
+                                                let data = try? Data(contentsOf: URL(string: "https://qmera.io/filepalio/image/\(self.iconCC)")!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                                                DispatchQueue.main.async { [self] in
+                                                    if data != nil {
+                                                        button_fb4.setImage(UIImage(data: data!), for: .normal)
+                                                    }
+                                                }
+                                            }
+                                    
+                                            groupView.addArrangedSubview(button_fb4)
+                                            button_fb4.addTarget(self, action: #selector(fb4Tap), for: .touchUpOutside)
+                                        } else {
+                                            let newButton = UIButton()
+                                            newButton.heightAnchor.constraint(equalToConstant: 40).isActive = true
+                                            newButton.translatesAutoresizingMaskIntoConstraints = false
+                                            DispatchQueue.global().async {
+                                                let data = try? Data(contentsOf: URL(string: "https://qmera.io/filepalio/image/\(json["icon"]!!)")!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
+                                                DispatchQueue.main.async {
+                                                    if data != nil {
+                                                        newButton.setImage(UIImage(data: data!), for: .normal)
+                                                    }
+                                                }
+                                            }
+                                            groupView.addArrangedSubview(newButton)
+                                            newButton.restorationIdentifier = json["app_id"] as? String
+                                            newButton.addTarget(self, action: #selector(tapMoreApp(_:)), for: .touchUpOutside)
+                                        }
+                                    }
+                                }
+                                let countSubviewsAfter = groupView.subviews.count
+                                if countSubviewsAfter <= 4 {
+                                    scrollView.isScrollEnabled = false
+                                }
+                            }
+                        }
+                    } else {
+                        groupView.subviews.forEach({ $0.removeFromSuperview() })
+                        getDefaultButton()
+                    }
+                } else {
+                    groupView.subviews.forEach({ $0.removeFromSuperview() })
+                    getDefaultButton()
+                }
+            }
+        }
+    }
+    
+    func getDefaultButton() {
+        button_fb1 = UIButton()
+        button_fb1.heightAnchor.constraint(equalToConstant: 40).isActive = true
+        button_fb1.translatesAutoresizingMaskIntoConstraints = false
+        button_fb1.setImage(UIImage(named: "pb_button_cc", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
+        groupView.addArrangedSubview(button_fb1)
+        button_fb1.addTarget(self, action: #selector(fb1Tap), for: .touchUpOutside)
+        
+        button_fb2 = UIButton()
+        button_fb2.heightAnchor.constraint(equalToConstant: 40).isActive = true
+        button_fb2.translatesAutoresizingMaskIntoConstraints = false
+        button_fb2.setImage(UIImage(named: "pb_button_chat", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
+        groupView.addArrangedSubview(button_fb2)
+        button_fb2.addTarget(self, action: #selector(fb2Tap), for: .touchUpOutside)
         checkCounter()
+        
+        button_fb3 = UIButton()
+        button_fb3.heightAnchor.constraint(equalToConstant: 40).isActive = true
+        button_fb3.translatesAutoresizingMaskIntoConstraints = false
+        button_fb3.setImage(UIImage(named: "pb_button_call", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
+        groupView.addArrangedSubview(button_fb3)
+        button_fb3.addTarget(self, action: #selector(fb3Tap), for: .touchUpOutside)
+        
+        button_fb4 = UIButton()
+        button_fb4.heightAnchor.constraint(equalToConstant: 40).isActive = true
+        button_fb4.translatesAutoresizingMaskIntoConstraints = false
+        button_fb4.setImage(UIImage(named: "pb_button_stream", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), for: .normal)
+        groupView.addArrangedSubview(button_fb4)
+        button_fb4.addTarget(self, action: #selector(fb4Tap), for: .touchUpOutside)
     }
     
     @objc func draggedView(_ sender:UIPanGestureRecognizer){
@@ -121,8 +279,8 @@ public class FloatingButton: UIView {
             xPos = maximumx
         }
         if(isShow) {
-            let minimumy = (heightScreen / 5)
-            let maximumy = heightScreen - 120
+            let minimumy = CGFloat(120.5) //30
+            let maximumy = heightScreen - 100
             if(yPos < minimumy) {
                 yPos = minimumy
             }
@@ -148,20 +306,41 @@ public class FloatingButton: UIView {
         UserDefaults.standard.set(center.y, forKey: "ylastPosFB")
     }
     
+    @objc func imageFBUpdate(notification: NSNotification) {
+        
+    }
+    
     @objc func checkCounter() {
-        DispatchQueue.main.async {
-            let counter = self.queryCountCounter()
-            if counter != 0 && !self.indicatorCounterFB.isDescendant(of: self) {
-                self.addIndicatorCounterFB(counter: counter)
-                UIApplication.shared.applicationIconBadgeNumber = Int(counter)
-            } else if counter != 0 && self.indicatorCounterFB.isDescendant(of: self) {
-                self.indicatorCounterFB.removeConstraints(self.indicatorCounterFB.constraints)
-                self.indicatorCounterFB.removeFromSuperview()
-                self.addIndicatorCounterFB(counter: counter)
-                UIApplication.shared.applicationIconBadgeNumber = Int(counter)
-            } else if counter == 0 && self.indicatorCounterFB.isDescendant(of: self) {
-                self.indicatorCounterFB.removeFromSuperview()
-                UIApplication.shared.applicationIconBadgeNumber = 0
+        let counter = queryCountCounter()
+        if counter > 0 {
+            DispatchQueue.main.async { [self] in
+                if !indicatorCounterFB.isDescendant(of: button_fb2) {
+                    button_fb2.addSubview(indicatorCounterFB)
+                    indicatorCounterFB.layer.cornerRadius = 7.5
+                    indicatorCounterFB.layer.masksToBounds = true
+                    indicatorCounterFB.backgroundColor = .systemRed
+                    indicatorCounterFB.anchor(top: button_fb2.topAnchor, left: button_fb2.leftAnchor, height: 15, minWidth: 15, maxWidth: 20)
+                    indicatorCounterFB.addSubview(labelCounterFB)
+                    labelCounterFB.anchor(left: indicatorCounterFB.leftAnchor, right: indicatorCounterFB.rightAnchor, paddingLeft: 5, paddingRight: 5, centerX: indicatorCounterFB.centerXAnchor, centerY: indicatorCounterFB.centerYAnchor)
+                    labelCounterFB.font = .systemFont(ofSize: 10)
+                    labelCounterFB.textColor = .white
+                }
+                if !indicatorCounterFBBig.isDescendant(of: nexilis_button){
+                    nexilis_button.addSubview(indicatorCounterFBBig)
+                    indicatorCounterFBBig.tintColor = .systemRed
+                    indicatorCounterFBBig.image = UIImage(systemName: "staroflife.circle.fill")
+                    indicatorCounterFBBig.anchor(top: nexilis_button.topAnchor, left: nexilis_button.leftAnchor, paddingTop: 5, paddingLeft: 5, width: 15, height: 15)
+                }
+                labelCounterFB.text = "\(counter)"
+            }
+        } else {
+            DispatchQueue.main.async { [self] in
+                if indicatorCounterFB.isDescendant(of: button_fb2) {
+                    indicatorCounterFB.removeFromSuperview()
+                }
+                if indicatorCounterFBBig.isDescendant(of: nexilis_button) {
+                    indicatorCounterFBBig.removeFromSuperview()
+                }
             }
         }
     }
@@ -176,148 +355,132 @@ public class FloatingButton: UIView {
         })
         return counter ?? 0
     }
-
-    private func setTopConstraint(constant: CGFloat) {
-        topConstraint.constant = constant
-    }
-    
-    private func addIndicatorCounterFB(counter: Int32) {
-        self.addSubview(indicatorCounterFB)
-        indicatorCounterFB.translatesAutoresizingMaskIntoConstraints = false
-        indicatorCounterFB.backgroundColor = .systemRed
-        indicatorCounterFB.layer.cornerRadius = 7.5
-        indicatorCounterFB.clipsToBounds = true
-        indicatorCounterFB.layer.borderWidth = 0.5
-        indicatorCounterFB.layer.borderColor = UIColor.secondaryColor.cgColor
-        if self.isShow {
-            self.topConstraint = indicatorCounterFB.topAnchor.constraint(equalTo: self.topAnchor, constant: 60)
-        } else {
-            self.topConstraint = indicatorCounterFB.topAnchor.constraint(equalTo: self.topAnchor, constant: 10)
-        }
-        NSLayoutConstraint.activate([
-            self.topConstraint,
-            indicatorCounterFB.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 3),
-            indicatorCounterFB.widthAnchor.constraint(greaterThanOrEqualToConstant: 15),
-            indicatorCounterFB.heightAnchor.constraint(equalToConstant: 15)
-        ])
-
-        indicatorCounterFB.addSubview(labelCounter)
-        labelCounter.translatesAutoresizingMaskIntoConstraints = false
-        NSLayoutConstraint.activate([
-            labelCounter.leadingAnchor.constraint(equalTo: indicatorCounterFB.leadingAnchor, constant: 2),
-            labelCounter.trailingAnchor.constraint(equalTo: indicatorCounterFB.trailingAnchor, constant: -2),
-            labelCounter.centerXAnchor.constraint(equalTo: indicatorCounterFB.centerXAnchor),
-        ])
-        labelCounter.font = UIFont.systemFont(ofSize: 11)
-        if counter > 99 {
-            labelCounter.text = "99+"
-        } else {
-            labelCounter.text = "\(counter)"
-        }
-        labelCounter.textColor = .secondaryColor
-        labelCounter.textAlignment = .center
-    }
     
     @objc func qmeraTap() {
         show(isShow: !isShow)
     }
     
-    @objc func qmeraLongPress() {
-        let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "settingNav") as! UINavigationController
-        navigationController.modalPresentationStyle = .fullScreen
-        navigationController.navigationBar.tintColor = .white
-        navigationController.navigationBar.barTintColor = .mainColor
-        navigationController.navigationBar.isTranslucent = false
-        let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
-        navigationController.navigationBar.titleTextAttributes = textAttributes
-        navigationController.view.backgroundColor = .mainColor
-        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
-    }
-    
-    @objc func ccTap() {
+    @objc func tapMoreApp(_ sender: UIButton) {
+        let id = sender.restorationIdentifier
         let isChangeProfile = UserDefaults.standard.bool(forKey: "is_change_profile")
         if !isChangeProfile {
             let alert = UIAlertController(title: "Change Profile".localized(), message: "You must change your name to use this feature".localized(), preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .destructive, handler: {_ in
-                 
-            }))
             alert.addAction(UIAlertAction(title: "Ok".localized(), style: UIAlertAction.Style.default, handler: {(_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changeNS") as! ChangeNamePassswordViewController
                 let navigationController = UINavigationController(rootViewController: controller)
-                navigationController.modalPresentationStyle = .fullScreen
+                navigationController.modalPresentationStyle = .custom
                 navigationController.navigationBar.tintColor = .white
                 navigationController.navigationBar.barTintColor = .mainColor
                 navigationController.navigationBar.isTranslucent = false
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
                 navigationController.navigationBar.titleTextAttributes = textAttributes
                 navigationController.view.backgroundColor = .mainColor
-                self.show(isShow: false)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.dismiss(animated: true, completion: nil)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
             }))
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(alert, animated: true, completion: nil)
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+            }
+            hideButton()
             return
         }
-        var isOfficer = false
-        Database.shared.database?.inTransaction({ (fmdb, rollback) in
-            let idMe = UserDefaults.standard.string(forKey: "me") as String?
-            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type, image_id FROM BUDDY where f_pin='\(idMe!)'"), cursorData.next() {
-                if cursorData.string(forColumnIndex: 0) == "24" {
-                    isOfficer = true
+        if id == nil {
+            return
+        }
+        if let url = URL(string: "itms-apps://apple.com/app/\(id!)") {
+            UIApplication.shared.open(url)
+        }
+        hideButton()
+    }
+    
+    @objc func fb1Tap() {
+        let isChangeProfile = UserDefaults.standard.bool(forKey: "is_change_profile")
+        if !isChangeProfile {
+            let alert = UIAlertController(title: "Change Profile".localized(), message: "You must change your name to use this feature".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "Ok".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changeNS") as! ChangeNamePassswordViewController
+                let navigationController = UINavigationController(rootViewController: controller)
+                navigationController.modalPresentationStyle = .custom
+                navigationController.navigationBar.tintColor = .white
+                navigationController.navigationBar.barTintColor = .mainColor
+                navigationController.navigationBar.isTranslucent = false
+                let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+                navigationController.navigationBar.titleTextAttributes = textAttributes
+                navigationController.view.backgroundColor = .mainColor
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
                 }
-                cursorData.close()
-                return
+            }))
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
             }
-        })
-        show(isShow: false)
-        if isOfficer {
-            let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "myHistoryCC") as! HistoryCCViewController
-            controller.isOfficer = true
-            let navigationController = UINavigationController(rootViewController: controller)
-            navigationController.navigationBar.tintColor = .white
-            navigationController.modalPresentationStyle = .fullScreen
-            navigationController.navigationBar.barTintColor = .mainColor
-            navigationController.navigationBar.isTranslucent = false
-            let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
-            navigationController.navigationBar.titleTextAttributes = textAttributes
-            navigationController.view.backgroundColor = .mainColor
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+            hideButton()
+            return
+        }
+        let isWaitingRequestCC = UserDefaults.standard.bool(forKey: "waitingRequestCC")
+        if isWaitingRequestCC {
+            let imageView = UIImageView(image: UIImage(systemName: "info.circle"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "You have requested Call Center, please wait for response.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .info, colors: nil, iconPosition: .center)
+            banner.show()
+            return
+        }
+        let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+        controller.isContactCenter = true
+        let navigationController = UINavigationController(rootViewController: controller)
+        navigationController.modalPresentationStyle = .custom
+        navigationController.navigationBar.tintColor = .white
+        navigationController.navigationBar.barTintColor = .mainColor
+        navigationController.navigationBar.isTranslucent = false
+        let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+        navigationController.navigationBar.titleTextAttributes = textAttributes
+        navigationController.view.backgroundColor = .mainColor
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
         } else {
-            let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
-            controller.isContactCenter = true
-            let navigationController = UINavigationController(rootViewController: controller)
-            navigationController.modalPresentationStyle = .fullScreen
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+            UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
         }
-        
+        hideButton()
     }
     
-    @objc func streamTap() {
+    @objc func fb2Tap() {
         let isChangeProfile = UserDefaults.standard.bool(forKey: "is_change_profile")
         if !isChangeProfile {
             let alert = UIAlertController(title: "Change Profile".localized(), message: "You must change your name to use this feature".localized(), preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .destructive, handler: {_ in
-                 
-            }))
             alert.addAction(UIAlertAction(title: "Ok".localized(), style: UIAlertAction.Style.default, handler: {(_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changeNS") as! ChangeNamePassswordViewController
                 let navigationController = UINavigationController(rootViewController: controller)
-                navigationController.modalPresentationStyle = .fullScreen
+                navigationController.modalPresentationStyle = .custom
                 navigationController.navigationBar.tintColor = .white
                 navigationController.navigationBar.barTintColor = .mainColor
                 navigationController.navigationBar.isTranslucent = false
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
                 navigationController.navigationBar.titleTextAttributes = textAttributes
                 navigationController.view.backgroundColor = .mainColor
-                self.show(isShow: false)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.dismiss(animated: true, completion: nil)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
             }))
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(alert, animated: true, completion: nil)
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+            }
+            hideButton()
             return
         }
-        show(isShow: false)
-        let navigationController = UINavigationController(rootViewController: QmeraCreateStreamingViewController())
+        let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
         navigationController.modalPresentationStyle = .fullScreen
         navigationController.navigationBar.tintColor = .white
         navigationController.navigationBar.barTintColor = .mainColor
@@ -325,34 +488,42 @@ public class FloatingButton: UIView {
         let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
         navigationController.navigationBar.titleTextAttributes = textAttributes
         navigationController.view.backgroundColor = .mainColor
-        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+        } else {
+            UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+        }
+        hideButton()
     }
     
-    @objc func callTap() {
+    @objc func fb3Tap() {
         let isChangeProfile = UserDefaults.standard.bool(forKey: "is_change_profile")
         if !isChangeProfile {
             let alert = UIAlertController(title: "Change Profile".localized(), message: "You must change your name to use this feature".localized(), preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .destructive, handler: {_ in
-                 
-            }))
             alert.addAction(UIAlertAction(title: "Ok".localized(), style: UIAlertAction.Style.default, handler: {(_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changeNS") as! ChangeNamePassswordViewController
                 let navigationController = UINavigationController(rootViewController: controller)
-                navigationController.modalPresentationStyle = .fullScreen
+                navigationController.modalPresentationStyle = .custom
                 navigationController.navigationBar.tintColor = .white
                 navigationController.navigationBar.barTintColor = .mainColor
                 navigationController.navigationBar.isTranslucent = false
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
                 navigationController.navigationBar.titleTextAttributes = textAttributes
                 navigationController.view.backgroundColor = .mainColor
-                self.show(isShow: false)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.dismiss(animated: true, completion: nil)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
             }))
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(alert, animated: true, completion: nil)
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+            }
+            hideButton()
             return
         }
-        show(isShow: false)
         let callContact = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactSID")
         let navigationController = UINavigationController(rootViewController: callContact)
         navigationController.modalPresentationStyle = .fullScreen
@@ -362,64 +533,110 @@ public class FloatingButton: UIView {
         let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
         navigationController.navigationBar.titleTextAttributes = textAttributes
         navigationController.view.backgroundColor = .mainColor
-        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+        } else {
+            UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+        }
+        hideButton()
     }
     
-    @objc func chatTap() {
+    @objc func fb4Tap() {
         let isChangeProfile = UserDefaults.standard.bool(forKey: "is_change_profile")
         if !isChangeProfile {
             let alert = UIAlertController(title: "Change Profile".localized(), message: "You must change your name to use this feature".localized(), preferredStyle: .alert)
-            alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .destructive, handler: {_ in
-                 
-            }))
             alert.addAction(UIAlertAction(title: "Ok".localized(), style: UIAlertAction.Style.default, handler: {(_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changeNS") as! ChangeNamePassswordViewController
                 let navigationController = UINavigationController(rootViewController: controller)
-                navigationController.modalPresentationStyle = .fullScreen
+                navigationController.modalPresentationStyle = .custom
                 navigationController.navigationBar.tintColor = .white
                 navigationController.navigationBar.barTintColor = .mainColor
                 navigationController.navigationBar.isTranslucent = false
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
                 navigationController.navigationBar.titleTextAttributes = textAttributes
                 navigationController.view.backgroundColor = .mainColor
-                self.show(isShow: false)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.dismiss(animated: true, completion: nil)
-                UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+                }
             }))
-            UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(alert, animated: true, completion: nil)
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+            }
+            hideButton()
             return
         }
-        show(isShow: false)
-        let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
-        navigationController.modalPresentationStyle = .fullScreen
+        let navigationController = UINavigationController(rootViewController: QmeraCreateStreamingViewController())
+        navigationController.modalPresentationStyle = .custom
         navigationController.navigationBar.tintColor = .white
         navigationController.navigationBar.barTintColor = .mainColor
         navigationController.navigationBar.isTranslucent = false
         let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
         navigationController.navigationBar.titleTextAttributes = textAttributes
         navigationController.view.backgroundColor = .mainColor
-        UIApplication.shared.windows.filter {$0.isKeyWindow}.first?.rootViewController?.present(navigationController, animated: true, completion: nil)
+        if UIApplication.shared.visibleViewController?.navigationController != nil {
+            UIApplication.shared.visibleViewController?.navigationController?.present(navigationController, animated: true, completion: nil)
+        } else {
+            UIApplication.shared.visibleViewController?.present(navigationController, animated: true, completion: nil)
+        }
+        hideButton()
     }
     
-    public func show(isShow: Bool) {
-        self.isShow = isShow
-        if self.isShow {
-            self.setTopConstraint(constant: 60)
+    @objc func qmeraLongPress() {
+        let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "settingNav") as! UINavigationController
+        navigationController.modalPresentationStyle = .custom
+        navigationController.navigationBar.tintColor = .white
+        navigationController.navigationBar.barTintColor = .mainColor
+        navigationController.navigationBar.isTranslucent = false
+        let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+        navigationController.navigationBar.titleTextAttributes = textAttributes
+        navigationController.view.backgroundColor = .mainColor
+        UIApplication.shared.rootViewController?.present(navigationController, animated: true, completion: nil)
+        hideButton()
+    }
+    
+    @objc func hideButton() {
+        if isShow {
+            show(isShow: false)
+        }
+        if self.frame.origin.x < UIScreen.main.bounds.width / 2 - 30 {
+            self.frame.origin.x = 0
         } else {
-            self.setTopConstraint(constant: 10)
+            self.frame.origin.x = UIScreen.main.bounds.width - 50
         }
-        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "updateShowButton"), object: nil, userInfo: nil)
+    }
+    
+    public func show(isShow: Bool) {
+        self.isShow = isShow
         if isShow {
-            let height = frame.width * 5
-            var yPosition = frame.origin.y - height + frame.width
-            if center.y < (UIScreen.main.bounds.height / 5) + 100 {
+            pullButton()
+            if indicatorCounterFBBig.isDescendant(of: nexilis_button) {
+                indicatorCounterFBBig.isHidden = true
+            }
+            let height = CGFloat(217) //40
+            var yPosition = frame.origin.y - height + 50
+            if yPosition <= 25 {
                 lastPosY = frame.origin.y
-                yPosition = UIScreen.main.bounds.height / 5 - 120
+                yPosition = 25
             }
             frame = CGRect(x: frame.origin.x, y: yPosition, width: frame.width, height: height)
+            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { [self] in
+                if isShow {
+                    let countSubviewsAfter = groupView.subviews.count
+                    if countSubviewsAfter > 5 {
+                        scrollView.flashScrollIndicators()
+                    }
+                }
+            })
         } else {
-            let height = frame.width * 5
-            var yPosition = frame.origin.y + height - frame.width
+            if indicatorCounterFBBig.isDescendant(of: nexilis_button) {
+                indicatorCounterFBBig.isHidden = false
+            }
+            let height = CGFloat(217) //40
+            var yPosition = frame.origin.y + height - 50
             if lastPosY != nil {
                 yPosition = lastPosY!
             }

+ 158 - 42
appbuilder-ios/NexilisLite/NexilisLite/Source/IncomingThread.swift

@@ -8,6 +8,8 @@
 
 import Foundation
 import UIKit
+import NotificationBannerSwift
+import nuSDKService
 
 class IncomingThread {
     
@@ -27,7 +29,7 @@ class IncomingThread {
     }
     
     func getQueue() -> TMessage {
-        while queue.isEmpty{
+        while queue.isEmpty || queue.count == 0 {
             semaphore.wait()
         }
         return queue.remove(at: 0)
@@ -49,7 +51,7 @@ class IncomingThread {
         print("incoming process", message.toLogString())
         if message.getCode() == CoreMessage_TMessageCode.LOGIN_FILE {
             loginFile(message: message)
-        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_MYSELF || message.getCode() == CoreMessage_TMessageCode.PUSH_MYSELF_ACK {
+        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_MYSELF || message.getCode() == CoreMessage_TMessageCode.PUSH_MYSELF_ACK || message.getCode() == CoreMessage_TMessageCode.PULL_MYSELF {
             pushMyself(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.PUSH_BUDDY {
             initBatchBuddy(message: message)
@@ -69,7 +71,7 @@ class IncomingThread {
             receiveMessage(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.UPDATE_CTEXT {
             receiveMessageStatus(message: message)
-        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_GROUP {
+        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_GROUP || message.getCode() == CoreMessage_TMessageCode.PUSH_GROUP_A {
             pushGroup(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.PUSH_GROUP_MEMBER {
             pushGroupMembers(message: message)
@@ -125,15 +127,19 @@ class IncomingThread {
             verifyOTP(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.REMOVE_FRIEND {
             deleteBuddy(message: message)
-        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.ACCEPT_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.END_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.TIMEOUT_CONTACT_CENTER {
+        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.ACCEPT_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.END_CALL_CENTER || message.getCode() == CoreMessage_TMessageCode.TIMEOUT_CONTACT_CENTER || message.getCode() == CoreMessage_TMessageCode.INVITE_TO_ROOM_CONTACT_CENTER || message.getCode() == CoreMessage_TMessageCode.ACCEPT_CONTACT_CENTER || message.getCode() == CoreMessage_TMessageCode.PUSH_MEMBER_ROOM_CONTACT_CENTER || message.getCode() == CoreMessage_TMessageCode.INVITE_END_CONTACT_CENTER || message.getCode() == CoreMessage_TMessageCode.INVITE_EXIT_CONTACT_CENTER {
             handleCallCenter(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.PUSH_DISCUSSION_COMMENT {
             if let delegate = Nexilis.shared.messageDelegate {
                 delegate.onReceiveComment(message: message)
             }
             ack(message: message)
+        } else if message.getCode() == CoreMessage_TMessageCode.APPROVE_FORM {
+            onApproveForm(message: message)
         } else if message.getCode() == CoreMessage_TMessageCode.END_CALL {
             endCall(message: message)
+        } else if message.getCode() == CoreMessage_TMessageCode.PUSH_SERVICE_BNI {
+            pushServiceBNI(message: message)
         } else {
             print("unprocessed code", message.getCode())
             ack(message: message)
@@ -157,6 +163,13 @@ class IncomingThread {
         ack(message: message)
     }
     
+    private func onApproveForm(message: TMessage) {
+        if let me = UserDefaults.standard.string(forKey: "me") {
+            _ = Nexilis.write(message: CoreMessage_TMessageBank.getPostRegistration(p_pin: me))
+        }
+        ack(message: message)
+    }
+    
     private func handleCallCenter(message: TMessage) -> Void {
         if let delegate = Nexilis.shared.messageDelegate {
             delegate.onReceive(message: message)
@@ -282,11 +295,6 @@ class IncomingThread {
             let url = documentDir.appendingPathComponent(key_filename)
             print("write file \(url.path)")
             try data.write(to: url, options: .atomic)
-            let image = UIImage(data: data)
-            let save = UserDefaults.standard.bool(forKey: "saveToGallery")
-            if save {
-                UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
-            }
             if let download = Nexilis.getDownload(forKey: key_filename) {
                 if let delegate = download.delegate {
                     delegate.onDownloadProgress(fileName: key_filename, progress: 100)
@@ -331,6 +339,35 @@ class IncomingThread {
         ack(message: message)
     }
     
+    private func pushServiceBNI(message: TMessage) -> Void {
+        let data = message.getBody(key: CoreMessage_TMessageKey.DATA)
+        if !data.isEmpty {
+            if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    do {
+                        for json in jsonArray {
+                            var parent = CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.PARENT_ID)
+                            if parent.isEmpty {
+                                parent = "-99"
+                            }
+                            _ = try Database.shared.insertRecord(fmdb: fmdb, table: "SERVICE_BANK", cvalues: [
+                                "service_id" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.CATEGORY_ID),
+                                "service_name" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.NAME),
+                                "description" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.DESCRIPTION),
+                                "parent" : parent,
+                                "is_tablet" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.PLATFORM)
+                            ], replace: true)
+                        }
+                        ack(message: message)
+                    } catch {
+                        rollback.pointee = true
+                        print(error)
+                    }
+                })
+            }
+        }
+    }
+    
     private func changeGroupInfo(message: TMessage) -> Void {
         let group_id = message.getBody(key: CoreMessage_TMessageKey.GROUP_ID)
         let group_name = message.getBody(key: CoreMessage_TMessageKey.GROUP_NAME)
@@ -444,7 +481,9 @@ class IncomingThread {
         ack(message: message)
     }
     
-    private func pushGroupMembers(message: TMessage) -> Void {
+    var listPushGroupMember: [TMessage] = []
+    
+    private func pushGroupMembers(message: TMessage, isFromIncoming: Bool = true) -> Void {
         let group_id = message.getBody(key: CoreMessage_TMessageKey.GROUP_ID)
         let f_pin = message.getBody(key: CoreMessage_TMessageKey.F_PIN)
         if(f_pin.isEmpty) {
@@ -480,32 +519,43 @@ class IncomingThread {
                 }
             }
         } else {
-            Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                do {
-                    let result = try Database.shared.insertRecord(fmdb: fmdb, table: "GROUPZ_MEMBER", cvalues: [
-                        "group_id" : group_id,
-                        "f_pin" : f_pin,
-                        "position" : message.getBody(key: CoreMessage_TMessageKey.POSITION),
-                        "user_id" : message.getBody(key: CoreMessage_TMessageKey.USER_ID),
-                        "ac" : message.getBody(key: CoreMessage_TMessageKey.AC),
-                        "ac_desc" : message.getBody(key: CoreMessage_TMessageKey.AC_DESC),
-                        "first_name" : message.getBody(key: CoreMessage_TMessageKey.FIRST_NAME),
-                        "last_name" : message.getBody(key: CoreMessage_TMessageKey.LAST_NAME),
-                        "msisdn" : message.getBody(key: CoreMessage_TMessageKey.MSISDN),
-                        "thumb_id" : message.getBody(key: CoreMessage_TMessageKey.THUMB_ID),
-                        "created_date" : message.getBody(key: CoreMessage_TMessageKey.CREATED_DATE)
-                    ], replace: true)
-                    if result > 0 {
-                        if let delegate = Nexilis.shared.groupDelegate {
-                            delegate.onMember(code: message.getCode(), f_pin: message.getPIN(), groupId: group_id, member: f_pin)
+            if isFromIncoming {
+                listPushGroupMember.append(message)
+            }
+            if listPushGroupMember.count == 1 || !isFromIncoming {
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    do {
+                        let result = try Database.shared.insertRecord(fmdb: fmdb, table: "GROUPZ_MEMBER", cvalues: [
+                            "group_id" : group_id,
+                            "f_pin" : f_pin,
+                            "position" : message.getBody(key: CoreMessage_TMessageKey.POSITION),
+                            "user_id" : message.getBody(key: CoreMessage_TMessageKey.USER_ID),
+                            "ac" : message.getBody(key: CoreMessage_TMessageKey.AC),
+                            "ac_desc" : message.getBody(key: CoreMessage_TMessageKey.AC_DESC),
+                            "first_name" : message.getBody(key: CoreMessage_TMessageKey.FIRST_NAME),
+                            "last_name" : message.getBody(key: CoreMessage_TMessageKey.LAST_NAME),
+                            "msisdn" : message.getBody(key: CoreMessage_TMessageKey.MSISDN),
+                            "thumb_id" : message.getBody(key: CoreMessage_TMessageKey.THUMB_ID),
+                            "created_date" : message.getBody(key: CoreMessage_TMessageKey.CREATED_DATE)
+                        ], replace: true)
+                        if result > 0 {
+                            if let delegate = Nexilis.shared.groupDelegate {
+                                delegate.onMember(code: message.getCode(), f_pin: message.getPIN(), groupId: group_id, member: f_pin)
+                            }
+                            self.listPushGroupMember.remove(at: 0)
+                            ack(message: message)
+                            if listPushGroupMember.count > 0 {
+                                dispatchQueue.asyncAfter(deadline: .now() + 1, execute: {
+                                    self.pushGroupMembers(message: self.listPushGroupMember[0], isFromIncoming: false)
+                                })
+                            }
                         }
-                        ack(message: message)
+                    } catch {
+                        rollback.pointee = true
+                        print(error)
                     }
-                } catch {
-                    rollback.pointee = true
-                    print(error)
-                }
-            })
+                })
+            }
         }
     }
     
@@ -612,11 +662,11 @@ class IncomingThread {
                 let url = documentDir.appendingPathComponent(thumb_id)
                 print("write thumb \(url.path)")
                 try data.write(to: url, options: .atomic)
-                let image = UIImage(data: data)
-                let save = UserDefaults.standard.bool(forKey: "saveToGallery")
-                if save {
-                    UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
-                }
+//                let image = UIImage(data: data)
+//                let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+//                if save {
+//                    UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
+//                }
             } catch {
                 print(error)
             }
@@ -736,6 +786,9 @@ class IncomingThread {
                             "is_sub_account" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.IS_SUB_ACCOUNT),
                             "last_sign" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.LAST_SIGN),
                             "android_id" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.ANDROID_ID),
+                            "is_change_profile" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.IS_CHANGE_PROFILE),
+                            "area" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.WORKING_AREA),
+                            "is_second_layer" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.IS_SECOND_LAYER),
                         ], replace: true)
                         _ = Database.shared.updateRecord(fmdb: fmdb, table: "GROUPZ_MEMBER", cvalues: [
                             "first_name" : CoreMessage_TMessageUtil.getString(json: json, key: CoreMessage_TMessageKey.FIRST_NAME),
@@ -765,7 +818,7 @@ class IncomingThread {
             do {
                 if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select * from BUDDY where f_pin = '\(l_pin)'"), cursor.next() {
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "BUDDY", _where: "f_pin = '\(l_pin)'")
-                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(l_pin)' or l_pin='\(l_pin)') and message_scope_id='3'")
+                    _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE", _where: "(f_pin='\(l_pin)' or l_pin='\(l_pin)') and (message_scope_id='3' or message_scope_id='18')")
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "MESSAGE_SUMMARY", _where: "l_pin='\(l_pin)'")
                     _ = Database.shared.deleteRecord(fmdb: fmdb, table: "POST", _where: "author_f_pin='\(l_pin)'")
                     cursor.close()
@@ -783,7 +836,7 @@ class IncomingThread {
     private func pushMyself(message: TMessage) -> Void {
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
             do {
-                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                let result = try Database.shared.insertRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
                     "f_pin" : message.getBody(key: CoreMessage_TMessageKey.F_PIN),
                     "upline_pin" : message.getBody(key: CoreMessage_TMessageKey.UPLINE_PIN),
                     "first_name" : message.getBody(key: CoreMessage_TMessageKey.FIRST_NAME),
@@ -841,8 +894,71 @@ class IncomingThread {
                     "is_sub_account" : message.getBody(key: CoreMessage_TMessageKey.IS_SUB_ACCOUNT),
                     "last_sign" : message.getBody(key: CoreMessage_TMessageKey.LAST_SIGN),
                     "android_id" : message.getBody(key: CoreMessage_TMessageKey.ANDROID_ID),
+                    "is_change_profile" : message.getBody(key: CoreMessage_TMessageKey.IS_CHANGE_PROFILE),
+                    "area" : message.getBody(key: CoreMessage_TMessageKey.WORKING_AREA),
+                    "is_second_layer" : message.getBody(key: CoreMessage_TMessageKey.IS_SECOND_LAYER),
                 ], replace: true)
                 ack(message: message)
+                let idMe = UserDefaults.standard.string(forKey: "me")!
+                if message.getBody(key: CoreMessage_TMessageKey.USER_TYPE) != "24" && message.getBody(key: CoreMessage_TMessageKey.F_PIN) == idMe {
+                    let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+                    if !onGoingCC.isEmpty {
+                        let requester = onGoingCC.components(separatedBy: ",")[0]
+                        let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                        let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+                        let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+                        let date = "\(Date().currentTimeMillis())"
+                        if officer == idMe {
+                            _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: requester))
+                        } else {
+                            if requester == idMe {
+                                _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: officer))
+                            } else {
+                                _ = Nexilis.write(message: CoreMessage_TMessageBank.leaveCCRoomInvite(ticket_id: complaintId))
+                            }
+                        }
+                        _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                            "type" : "0",
+                            "title" : "Contact Center".localized(),
+                            "time" : startTimeCC,
+                            "f_pin" : officer,
+                            "data" : complaintId,
+                            "time_end" : date,
+                            "complaint_id" : complaintId,
+                            "members" : "",
+                            "requester": requester
+                        ], replace: true)
+                        UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                        UserDefaults.standard.removeObject(forKey: "membersCC")
+                        UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "info.circle"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Call Center Session has ended".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .info, colors: nil, iconPosition: .center)
+                            banner.show()
+                            if UIApplication.shared.visibleViewController is UINavigationController {
+                                let nc = UIApplication.shared.visibleViewController as! UINavigationController
+                                if nc.visibleViewController is QmeraAudioViewController || nc.visibleViewController is QmeraVideoViewController {
+                                    API.terminateCall(sParty: nil)
+                                }
+                                if nc.visibleViewController is EditorPersonal{
+                                    let vc = nc.visibleViewController as! EditorPersonal
+                                    vc.timeoutCC.invalidate()
+                                }
+                                nc.visibleViewController?.dismiss(animated: true, completion: nil)
+                            } else {
+                                if UIApplication.shared.visibleViewController is QmeraAudioViewController || UIApplication.shared.visibleViewController is QmeraVideoViewController {
+                                    API.terminateCall(sParty: nil)
+                                }
+                                if UIApplication.shared.visibleViewController is EditorPersonal{
+                                    let vc = UIApplication.shared.visibleViewController as! EditorPersonal
+                                    vc.timeoutCC.invalidate()
+                                }
+                                UIApplication.shared.visibleViewController?.dismiss(animated: true, completion: nil)
+                            }
+                        }
+                    }
+                }
             } catch {
                 rollback.pointee = true
                 print(error)
@@ -1414,7 +1530,7 @@ class IncomingThread {
                     "created_by": form.createdBy,
                     "sq_no": form.sqNo
                 ],
-                replace: false)
+                replace: true)
         })
         let data = message.getBody(key: CoreMessage_TMessageKey.DATA)
         if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {

+ 68 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/CategoryCC.swift

@@ -0,0 +1,68 @@
+//
+//  CategoryCC.swift
+//  NexilisLite
+//
+//  Created by Qindi on 20/06/22.
+//
+
+import Foundation
+
+public class CategoryCC: Model {
+    public var id: String
+    public var service_id: String
+    public var service_name: String
+    public var parent: String
+    public var description: String
+    public var is_tablet: String
+    public var isActive: Bool
+    
+    public static var default_parent = "-99"
+    
+    public init(id: String, service_id: String, service_name: String,parent: String, description: String, is_tablet: String, isActive:Bool = false) {
+        self.id = id
+        self.service_id = service_id
+        self.service_name = service_name
+        self.parent = parent
+        self.description = description
+        self.is_tablet = is_tablet
+        self.isActive = isActive
+    }
+    
+    public static func == (lhs: CategoryCC, rhs: CategoryCC) -> Bool {
+        return lhs.service_id == rhs.service_id
+    }
+    
+    public static func getDatafromParent(parent: String) -> [CategoryCC] {
+        var data: [CategoryCC] = []
+        Database.shared.database?.inTransaction({ fmdb, rollback in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select id, service_id, service_name, description, parent, is_tablet from SERVICE_BANK where parent = '\(parent)'") {
+                while cursor.next() {
+                    data.append(CategoryCC(id: cursor.string(forColumnIndex: 0) ?? "",
+                                           service_id: cursor.string(forColumnIndex: 1) ?? "",
+                                           service_name: cursor.string(forColumnIndex: 2) ?? "",
+                                           parent: cursor.string(forColumnIndex: 4) ?? "",
+                                           description: cursor.string(forColumnIndex: 3) ?? "",
+                                           is_tablet: cursor.string(forColumnIndex: 5) ?? ""))
+                }
+                cursor.close()
+            }
+        })
+        return data
+    }
+    
+    public static func getDataFromServiceId(service_id: String) -> CategoryCC? {
+        var data: CategoryCC?
+        Database.shared.database?.inTransaction({ fmdb, rollback in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select id, service_id, service_name, description, parent, is_tablet from SERVICE_BANK where service_id = '\(service_id)'"), cursor.next() {
+                data = CategoryCC(id: cursor.string(forColumnIndex: 0) ?? "",
+                                  service_id: cursor.string(forColumnIndex: 1) ?? "",
+                                  service_name: cursor.string(forColumnIndex: 2) ?? "",
+                                  parent: cursor.string(forColumnIndex: 4) ?? "",
+                                  description: cursor.string(forColumnIndex: 3) ?? "",
+                                  is_tablet: cursor.string(forColumnIndex: 5) ?? "")
+                cursor.close()
+            }
+        })
+        return data
+    }
+}

+ 9 - 4
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/Chat.swift

@@ -25,10 +25,11 @@ public class Group: Model {
     public var topics: [Topic] = []
     public var childs: [Group] = []
     public var members: [Member] = []
+    public let level: String
     
     public var isSelected = false
     
-    public init(id: String, name: String, profile: String, quote: String, by: String, date: String, parent: String, chatId: String = "", groupType: String, isOpen: String, official: String, isEducation: String = "", isLounge: Bool = false) {
+    public init(id: String, name: String, profile: String, quote: String, by: String, date: String, parent: String, chatId: String = "", groupType: String, isOpen: String, official: String, isEducation: String = "", isLounge: Bool = false, level: String = "") {
         self.id = id
         self.name = name
         self.profile = profile
@@ -42,9 +43,10 @@ public class Group: Model {
         self.official = official
         self.isEducation = isEducation
         self.isLounge = isLounge
+        self.level = level
     }
     
-    public var isInternal: Bool {
+    var isInternal: Bool {
         return isEducation == "2" || isEducation == "3" || isEducation == "4"
     }
     
@@ -55,6 +57,9 @@ public class Group: Model {
     public static func == (lhs: Group, rhs: Group) -> Bool {
         return lhs.id == rhs.id
     }
+    
+    
+    
 }
 
 public class Topic: Model {
@@ -81,9 +86,9 @@ public class Topic: Model {
 
 public class Member: User {
     
-    var position: String
+    public var position: String
     
-    public override init(pin: String) {
+    override init(pin: String) {
         self.position = "0"
         super.init(pin: pin)
     }

+ 27 - 11
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/Model.swift

@@ -9,29 +9,42 @@ import Foundation
 
 public class User: Model {
     
-    public let pin: String
-    public let firstName: String
-    public let lastName: String
-    public let thumb: String
-    public var official: String?
-    public var userType: String?
+    let pin: String
+    let firstName: String
+    let lastName: String
+    var thumb: String
+    var official: String?
+    var userType: String?
+    var privacy_flag: String?
+    var offline_mode: String?
+    var ex_block: String?
+    var ex_offmp: String?
     
     var isSelected: Bool = false
     
-    public init(pin: String) {
+    init(pin: String) {
         self.pin = pin
         self.firstName = ""
         self.lastName = ""
         self.thumb = ""
         self.userType = ""
+        self.privacy_flag = ""
+        self.offline_mode = ""
+        self.ex_block = ""
+        self.ex_offmp = ""
     }
     
-    public init(pin: String, firstName: String, lastName: String, thumb: String, userType: String = "0") {
+    init(pin: String, firstName: String, lastName: String, thumb: String, userType: String = "0", privacy_flag: String = "", offline_mode: String = "", ex_block: String = "", official: String = "", ex_offmp: String = "") {
         self.pin = pin
         self.firstName = firstName
         self.lastName = lastName
         self.thumb = thumb
         self.userType = userType
+        self.privacy_flag = privacy_flag
+        self.offline_mode = offline_mode
+        self.official = official
+        self.ex_block = ex_block
+        self.ex_offmp = ex_offmp
     }
     
     public static func == (lhs: User, rhs: User) -> Bool {
@@ -42,7 +55,7 @@ public class User: Model {
         return "\(pin) \(firstName) \(lastName) \(thumb)"
     }
     
-    public var fullName: String {
+    var fullName: String {
         return "\(firstName) \(lastName)".trimmingCharacters(in: .whitespaces)
     }
     
@@ -52,12 +65,15 @@ public class User: Model {
         }
         var user: User?
         Database.shared.database?.inTransaction({ fmdb, rollback in
-            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, image_id, user_type from BUDDY where f_pin = '\(pin)'"), cursor.next() {
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, image_id, user_type, privacy_flag, offline_mode, ex_block from BUDDY where f_pin = '\(pin)'"), cursor.next() {
                 user = User(pin: cursor.string(forColumnIndex: 0) ?? "",
                             firstName: cursor.string(forColumnIndex: 1) ?? "",
                             lastName: cursor.string(forColumnIndex: 2) ?? "",
                             thumb: cursor.string(forColumnIndex: 3) ?? "",
-                            userType: cursor.string(forColumnIndex: 4) ?? "")
+                            userType: cursor.string(forColumnIndex: 4) ?? "",
+                            privacy_flag: cursor.string(forColumnIndex: 5) ?? "",
+                            offline_mode: cursor.string(forColumnIndex: 6) ?? "",
+                            ex_block: cursor.string(forColumnIndex: 7) ?? "")
                 cursor.close()
             }
         })

+ 47 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/Model/WorkingArea.swift

@@ -0,0 +1,47 @@
+//
+//  WorkingArea.swift
+//  NexilisLite
+//
+//  Created by Qindi on 19/07/22.
+//
+
+import Foundation
+
+public class WorkingArea: Model {
+    public var id: String
+    public var area_id: String
+    public var name: String
+    public var parent: String
+    public var level: String
+    public var description: String
+    
+    public init(id: String, area_id: String, name: String, parent: String, level: String) {
+        self.id = id
+        self.area_id = area_id
+        self.name = name
+        self.parent = parent
+        self.level = level
+        self.description = ""
+    }
+    
+    public static func == (lhs: WorkingArea, rhs: WorkingArea) -> Bool {
+        return lhs.area_id == rhs.area_id
+    }
+    
+    public static func getData(name: String) -> [WorkingArea] {
+        var data: [WorkingArea] = []
+        Database.shared.database?.inTransaction({ fmdb, rollback in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select id, area_id, name, parent, level from WORKING_AREA where name LIKE '%\(name.lowercased())%'") {
+                while cursor.next() {
+                    data.append(WorkingArea(id: cursor.string(forColumnIndex: 0) ?? "",
+                                           area_id: cursor.string(forColumnIndex: 1) ?? "",
+                                           name: cursor.string(forColumnIndex: 2) ?? "",
+                                           parent: cursor.string(forColumnIndex: 3) ?? "",
+                                           level: cursor.string(forColumnIndex: 4) ?? ""))
+                }
+                cursor.close()
+            }
+        })
+        return data
+    }
+}

+ 0 - 79
appbuilder-ios/NexilisLite/NexilisLite/Source/MultipartFormDataRequest.swift

@@ -1,79 +0,0 @@
-//
-//  MultipartFormDataRequest.swift
-//  NexilisLite
-//
-//  Created by Kevin Maulana on 13/04/22.
-//
-
-import Foundation
-
-public struct MultipartFormDataRequest {
-    private let boundary: String = UUID().uuidString
-    private var httpBody = NSMutableData()
-    let url: URL
-
-    public init(url: URL) {
-        self.url = url
-    }
-
-    public func addTextField(named name: String, value: String) {
-        httpBody.append(textFormField(named: name, value: value))
-    }
-
-    private func textFormField(named name: String, value: String) -> String {
-        var fieldString = "--\(boundary)\r\n"
-        fieldString += "Content-Disposition: form-data; name=\"\(name)\"\r\n"
-        fieldString += "Content-Type: text/plain; charset=ISO-8859-1\r\n"
-        fieldString += "Content-Transfer-Encoding: 8bit\r\n"
-        fieldString += "\r\n"
-        fieldString += "\(value)\r\n"
-
-        return fieldString
-    }
-
-    public func addDataField(named name: String, data: Data, mimeType: String) {
-        httpBody.append(dataFormField(named: name, data: data, mimeType: mimeType))
-    }
-
-    private func dataFormField(named name: String,
-                               data: Data,
-                               mimeType: String) -> Data {
-        let fieldData = NSMutableData()
-
-        fieldData.append("--\(boundary)\r\n")
-        fieldData.append("Content-Disposition: form-data; name=\"\(name)\"\r\n")
-        fieldData.append("Content-Type: \(mimeType)\r\n")
-        fieldData.append("\r\n")
-        fieldData.append(data)
-        fieldData.append("\r\n")
-
-        return fieldData as Data
-    }
-    
-    public func asURLRequest() -> URLRequest {
-        var request = URLRequest(url: url)
-
-        request.httpMethod = "POST"
-        request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
-
-        httpBody.append("--\(boundary)--")
-        request.httpBody = httpBody as Data
-        return request
-    }
-}
-
-extension NSMutableData {
-  public func append(_ string: String) {
-    if let data = string.data(using: .utf8) {
-      self.append(data)
-    }
-  }
-}
-
-extension URLSession {
-    public func dataTask(with request: MultipartFormDataRequest,
-                  completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void)
-    -> URLSessionDataTask {
-        return dataTask(with: request.asURLRequest(), completionHandler: completionHandler)
-    }
-}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 636 - 319
appbuilder-ios/NexilisLite/NexilisLite/Source/Network.swift


+ 1 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/OutgoingThread.swift

@@ -85,7 +85,7 @@ class OutgoingThread {
     }
     
     func getQueue() -> TMessage {
-        while queue.isEmpty {
+        while queue.isEmpty || queue.count == 0 {
             print("QUEUE.wait")
             semaphore.wait()
         }

+ 1 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/TMessage.swift

@@ -33,7 +33,7 @@ public class TMessage {
     public static let TYPE_ALL         =  "2"
     public static let TYPE_NEED_ACK    =  "3"
     
-    public init() {
+    init() {
         mBodies[CoreMessage_TMessageKey.IMEI] = Nexilis.getCLMUserId()
 //        mBodies[CoreMessage_TMessageKey.VERCOD] = UIApplication.appVersion
         mBodies[CoreMessage_TMessageKey.VERCOD] = "1.8.5.10"

+ 4 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/Utils.swift

@@ -90,6 +90,10 @@ public final class Utils {
             }
         }
         else if !chat.file.isEmpty {
+            if chat.messageScope == "18" {
+                return "📄 Form"
+            }
+            print("KACAU \(chat.messageText)")
             return "📄 " + chat.messageText.components(separatedBy: "|")[0]
         } else if chat.attachmentFlag == "11" {
             return "❤️ " + "Sticker".localized()

+ 116 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/BNIView/BNIBookingWebView.swift

@@ -0,0 +1,116 @@
+//
+//  BNIBookingWebView.swift
+//  FMDB
+//
+//  Created by Qindi on 01/04/22.
+//
+
+import UIKit
+import WebKit
+
+class BNIBookingWebView: UIViewController, WKNavigationDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate, WKScriptMessageHandler {
+    var webView = WKWebView()
+    let closeButton = UIButton()
+    
+    override var preferredStatusBarStyle: UIStatusBarStyle {
+        return .default
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        view.addSubview(webView)
+        webView.translatesAutoresizingMaskIntoConstraints = false
+        webView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
+        webView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
+        webView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
+        webView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
+
+        webView.navigationDelegate = self
+        
+        Database().database?.inTransaction({ fmdb, rollback in
+            let idMe = UserDefaults.standard.string(forKey: "me") as String?
+            if let c = Database().getRecords(fmdb: fmdb, query: "select first_name || ' ' || last_name from BUDDY where f_pin = '\(idMe!)'"), c.next() {
+                let name = c.string(forColumnIndex: 0)!.trimmingCharacters(in: .whitespacesAndNewlines)
+                let url = URL(string: "https://sqappointment.murni.id:4200/bookingonline/#/?userid=\(name)")!
+                webView.load(URLRequest(url: url))
+                c.close()
+            }
+        })
+        webView.allowsBackForwardNavigationGestures = true
+        webView.scrollView.delegate = self
+        
+        let contentController = webView.configuration.userContentController
+        contentController.add(self, name: "sendQueueBNI")
+        let source: String = "var meta = document.createElement('meta');" +
+            "meta.name = 'viewport';" +
+            "meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';" +
+            "var head = document.getElementsByTagName('head')[0];" +
+            "head.appendChild(meta);"
+        
+        let script: WKUserScript = WKUserScript(source: source, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
+        contentController.addUserScript(script)
+        
+        let refreshControl = UIRefreshControl()
+        refreshControl.addTarget(self, action: #selector(reloadWebView(_:)), for: .valueChanged)
+        webView.scrollView.addSubview(refreshControl)
+        
+        webView.isOpaque = false
+        webView.backgroundColor = .white
+        webView.scrollView.backgroundColor = .white
+        
+        closeButton.setTitle("Close", for: .normal)
+        closeButton.setTitleColor(.orange, for: .normal)
+        closeButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 15)
+        view.addSubview(closeButton)
+        closeButton.translatesAutoresizingMaskIntoConstraints = false
+        closeButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 30.0).isActive = true
+        closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20.0).isActive = true
+        closeButton.addTarget(self, action: #selector(close(sender:)), for: .touchUpInside)
+        
+        let swipeUp = UISwipeGestureRecognizer(target: self, action: #selector(swipUpAction))
+        swipeUp.direction = .up
+        swipeUp.cancelsTouchesInView = false
+        swipeUp.delegate = self
+        webView.scrollView.addGestureRecognizer(swipeUp)
+    }
+    
+    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
+        return true
+    }
+    
+    @objc func swipUpAction() {
+        if !closeButton.isHidden {
+            closeButton.isHidden = true
+        }
+    }
+    
+    func scrollViewDidScroll(_ scrollView: UIScrollView) {
+        if (scrollView.contentOffset.y <= 0) {
+            if closeButton.isHidden {
+                closeButton.isHidden = false
+            }
+        }
+    }
+    
+    @objc func close(sender: UIButton) {
+        self.dismiss(animated: true)
+    }
+    
+    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
+        if message.name == "sendQueueBNI" {
+            guard let dict = message.body as? [String: AnyObject],
+                  let param1 = dict["param1"] as? String else {
+                return
+            }
+            DispatchQueue.global().async {
+                let _ = Nexilis.writeSync(message: CoreMessage_TMessageBank.queueBNI(service_id: param1), timeout: 30 * 1000)
+            }
+        }
+    }
+    
+    @objc func reloadWebView(_ sender: UIRefreshControl) {
+        webView.reload()
+        sender.endRefreshing()
+    }
+}

+ 106 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/BNIView/WorkingAreaPicker.swift

@@ -0,0 +1,106 @@
+//
+//  WorkingAreaPicker.swift
+//  NexilisLite
+//
+//  Created by Qindi on 26/07/22.
+//
+
+import UIKit
+
+class WorkingAreaPicker: UIViewController, UISearchBarDelegate, UITableViewDelegate, UITableViewDataSource {
+    let subContainerView = UIView()
+    lazy var searchBar:UISearchBar = UISearchBar()
+    var dataWorkingArea: [WorkingArea] = []
+    let tableView = UITableView()
+    
+    public var selectedData: ((WorkingArea) -> ())?
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        self.view.backgroundColor = .black.withAlphaComponent(0.3)
+        
+        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
+        tapGesture.cancelsTouchesInView = false
+        view.addGestureRecognizer(tapGesture)
+        
+        let containerView = UIView()
+        view.addSubview(containerView)
+        containerView.anchor(centerX: view.centerXAnchor, centerY: view.centerYAnchor, width: view.bounds.width - 40, height: view.bounds.height - 100)
+        containerView.backgroundColor = .white.withAlphaComponent(0.9)
+        
+        subContainerView.backgroundColor = .clear
+        containerView.addSubview(subContainerView)
+        subContainerView.anchor(top: containerView.topAnchor, left: containerView.leftAnchor, bottom: containerView.bottomAnchor, right: containerView.rightAnchor, paddingTop: 20.0, paddingLeft: 10.0, paddingBottom: 20.0, paddingRight: 10.0)
+        
+        let buttonClose = UIButton(type: .close)
+        buttonClose.frame.size = CGSize(width: 30, height: 30)
+        buttonClose.layer.cornerRadius = 15.0
+        buttonClose.clipsToBounds = true
+        buttonClose.backgroundColor = .secondaryColor.withAlphaComponent(0.5)
+        buttonClose.addTarget(self, action: #selector(close), for: .touchUpInside)
+        containerView.addSubview(buttonClose)
+        buttonClose.anchor(top: containerView.topAnchor, right: containerView.rightAnchor, width: 30, height: 30)
+        
+        let titleWA = UILabel()
+        titleWA.font = .systemFont(ofSize: 18, weight: .bold)
+        titleWA.text = "Working Area".localized()
+        titleWA.textAlignment = .center
+        subContainerView.addSubview(titleWA)
+        titleWA.anchor(top: subContainerView.topAnchor, left: subContainerView.leftAnchor, right: subContainerView.rightAnchor)
+        
+        searchBar.searchBarStyle = UISearchBar.Style.default
+        searchBar.placeholder = " Search..."
+        searchBar.sizeToFit()
+        searchBar.backgroundColor = .clear
+        searchBar.barTintColor = .clear
+        searchBar.isTranslucent = true
+        searchBar.setBackgroundImage(UIImage(), for: .any, barMetrics: .default)
+        searchBar.delegate = self
+        subContainerView.addSubview(searchBar)
+        searchBar.anchor(top: titleWA.bottomAnchor, left: subContainerView.leftAnchor, right: subContainerView.rightAnchor, paddingTop: 10.0)
+        
+        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cellWorkingArea")
+        tableView.dataSource = self
+        tableView.delegate = self
+        tableView.backgroundColor = .clear
+        self.view.addSubview(tableView)
+        tableView.anchor(top: searchBar.bottomAnchor, left: subContainerView.leftAnchor, bottom: subContainerView.bottomAnchor, right: subContainerView.rightAnchor, paddingBottom: 10.0)
+        
+        dataWorkingArea = WorkingArea.getData(name: "")
+        
+    }
+    
+    @objc func dismissKeyboard() {
+        //Causes the view (or one of its embedded text fields) to resign the first responder status.
+        view.endEditing(true)
+    }
+    
+    @objc func close() {
+        self.dismiss(animated: true)
+    }
+    
+    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
+        DispatchQueue.main.async {
+            self.dataWorkingArea = WorkingArea.getData(name: searchText)
+            self.tableView.reloadData()
+        }
+    }
+    
+    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        self.selectedData?(self.dataWorkingArea[indexPath.row])
+        self.dismiss(animated: true)
+    }
+
+    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        return dataWorkingArea.count
+    }
+
+    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let cell = tableView.dequeueReusableCell(withIdentifier: "cellWorkingArea", for: indexPath as IndexPath)
+        cell.textLabel!.text = dataWorkingArea[indexPath.row].name
+        cell.backgroundColor = .clear
+        return cell
+    }
+
+}

+ 22 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/AudioViewController.swift

@@ -16,7 +16,8 @@ class CallProviderDelegate: NSObject {
     
     static let providerConfiguration: CXProviderConfiguration = {
         let providerConfiguration = CXProviderConfiguration()
-        providerConfiguration.maximumCallsPerCallGroup = 1
+        providerConfiguration.maximumCallsPerCallGroup = 4
+        providerConfiguration.maximumCallGroups = 5
         providerConfiguration.supportsVideo = true
         providerConfiguration.supportedHandleTypes = [.phoneNumber]
         //        providerConfiguration.ringtoneSound = "call_in.mp3"
@@ -39,6 +40,9 @@ class CallProviderDelegate: NSObject {
             update.remoteHandle = CXHandle(type: .phoneNumber, value: user.fullName)
         }
         update.hasVideo = hasVideo
+        update.supportsGrouping = true
+        update.supportsUngrouping = true
+        update.supportsHolding = true
         
         provider.reportNewIncomingCall(with: uuid, update: update) { error in
             if error == nil {
@@ -81,13 +85,18 @@ extension CallProviderDelegate: CXProviderDelegate {
         call.hasConnectedDidChange = { [weak self] in
             self?.provider.reportOutgoingCall(with: call.uuid, connectedAt: call.connectDate)
         }
-        do {
-            try AVAudioSession.sharedInstance().setCategory(.playAndRecord)
-            try AVAudioSession.sharedInstance().setMode(call.hasVideo ? .videoChat : .voiceChat)
-            try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
-        } catch {
+        if self.callManager.calls.count == 0 {
+            do {
+                try AVAudioSession.sharedInstance().setCategory(.playAndRecord)
+                try AVAudioSession.sharedInstance().setMode(call.hasVideo ? .videoChat : .voiceChat)
+                try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
+            } catch {
+            }
+        } else if self.callManager.calls.count > 0 {
+            self.callManager.setOnHoldStatus(for: self.callManager.calls.last!, to: true)
         }
         action.fulfill()
+        print("JUMLAH START CALL \(self.callManager.calls.count)")
         self.callManager.addCall(call)
         if self.callManager.calls.count > 1 {
             API.initiateCCall(sParty: call.handle)
@@ -110,7 +119,11 @@ extension CallProviderDelegate: CXProviderDelegate {
             controller.isOnGoing = true
             controller.isOutgoing = false
             controller.modalPresentationStyle = .overCurrentContext
-            UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
+            if UIApplication.shared.visibleViewController?.navigationController != nil {
+                UIApplication.shared.visibleViewController?.navigationController?.present(controller, animated: true, completion: nil)
+            } else {
+                UIApplication.shared.visibleViewController?.present(controller, animated: true, completion: nil)
+            }
         }
     }
     
@@ -123,7 +136,9 @@ extension CallProviderDelegate: CXProviderDelegate {
         call.endCall()
         action.fulfill()
         self.callManager.removeCall(call)
+        print("JUMLAH CALL \(self.callManager.calls.count)")
         if self.callManager.calls.count == 0, !call.isReceiveEnd {
+            print("MASUK TERMINATE CALL DELEGATE")
             API.terminateCall(sParty: nil)
             DispatchQueue.global().async {
                 if let pin = call.handle {

+ 641 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/GroupView.swift

@@ -0,0 +1,641 @@
+//
+//  QmeraAudioConference.swift
+//  NexilisLite
+//
+//  Created by Qindi on 14/04/22.
+//
+
+import UIKit
+import AVFoundation
+import nuSDKService
+import NotificationBannerSwift
+
+class QmeraAudioConference: UIViewController {
+    
+    let buttonSize: CGFloat = 70
+    
+//    lazy var data: String = "" {
+//        didSet {
+//            getUserData { user in
+//                self.user = user
+//            }
+//        }
+//    }
+    
+//    var user: User?
+    
+    var isAddCall = ""
+        
+    private var users: [User] = [] {
+        didSet {
+            DispatchQueue.main.async {
+                if oldValue.count > self.users.count { // remove
+                    let remove = oldValue.filter { !self.users.contains($0) }
+                    remove.forEach { user in
+                        if let subviews = self.profiles.subviews as? [ProfileView] {
+                            subviews.forEach { p in
+                                if p.user == user {
+                                    self.profiles.removeArrangeSubview(view: p)
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    if let user = self.users.last {
+                        let profile = ProfileView(image: UIImage(systemName: "person.circle.fill"))
+                        profile.user = user
+                        self.profiles.addArrangedSubview(view: profile)
+                    }
+                }
+                self.name.text = self.users.map { $0.fullName }.joined(separator: ", ")
+            }
+        }
+    }
+    
+    var isOutgoing: Bool = true
+    
+    var isOnGoing: Bool = false
+    
+    var roomId = ""
+    
+    private var timer: Timer?
+    
+    private var firstCall: Bool = true
+    
+    private var isSpeaker: Bool = false
+    
+    let status: UILabel = {
+        let label = UILabel()
+        label.text = "Waiting for participant..."
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = .white
+        label.textAlignment = .center
+        return label
+    }()
+    
+    let profiles: GroupView = {
+        let groupView = GroupView()
+        groupView.spacing = 50
+        groupView.maxUser = 3
+        return groupView
+    }()
+    
+    let name: UILabel = {
+        let label = UILabel()
+        label.text = "uwitan"
+        label.font = UIFont.systemFont(ofSize: 14)
+        label.textColor = .white
+        label.textAlignment = .center
+        return label
+    }()
+    
+    let end: UIButton = {
+        let button = UIButton()
+        button.setImage(UIImage(systemName: "phone.down"), for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageView?.tintColor = .white
+        button.setBackgroundColor(.red, for: .normal)
+        button.setBackgroundColor(.white, for: .highlighted)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+        return button
+    }()
+    
+    let reject: UIButton = {
+        let button = UIButton()
+        let image = UIImage(systemName: "xmark")
+        button.setImage(image, for: .normal)
+        let selectedImage = image?.withTintColor(.mainColor)
+        button.setImage(selectedImage, for: .selected)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageView?.tintColor = .white
+        button.setBackgroundColor(.red, for: .normal)
+        button.setBackgroundColor(.white, for: .highlighted)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+        return button
+    }()
+    
+    let accept: UIButton = {
+        let button = UIButton()
+        let image = UIImage(systemName: "checkmark")
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageView?.tintColor = .white
+        button.setBackgroundColor(.greenColor, for: .normal)
+        button.setBackgroundColor(.white, for: .highlighted)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+        return button
+    }()
+    
+    let invite: UIButton = {
+        let button = UIButton()
+        let image = UIImage(systemName: "person.badge.plus")
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageView?.tintColor = .mainColor
+        button.setBackgroundColor(.white, for: .normal)
+        button.setBackgroundColor(.mainColor, for: .highlighted)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+        return button
+    }()
+    
+    let speaker: UIButton = {
+        let button = UIButton()
+        button.setImage(UIImage(systemName: "speaker.slash")?.withTintColor(.mainColor, renderingMode: .alwaysOriginal), for: .normal)
+        button.setImage(UIImage(systemName: "speaker.wave.3")?.withTintColor(.white, renderingMode: .alwaysOriginal), for: .selected)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.setBackgroundColor(.white, for: .normal)
+        button.setBackgroundColor(.mainColor, for: .highlighted)
+        button.setBackgroundColor(.mainColor, for: .selected)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.imageEdgeInsets = UIEdgeInsets(top: 15, left: 15, bottom: 15, right: 15)
+        return button
+    }()
+    
+    let stack: UIStackView = {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.distribution = .fillEqually
+        return stackView
+    }()
+    
+    public override func viewWillDisappear(_ animated: Bool) {
+        if self.isMovingFromParent {
+            NotificationCenter.default.removeObserver(self)
+        }
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark))
+        effectView.frame = view.frame
+        view.insertSubview(effectView, at: 0)
+        
+        view.addSubview(status)
+        view.addSubview(profiles)
+        view.addSubview(name)
+        
+        status.anchor(left: view.leftAnchor, bottom: profiles.topAnchor, right: view.rightAnchor, paddingBottom: 30, centerX: view.centerXAnchor)
+        profiles.anchor(centerX: view.centerXAnchor, centerY: view.centerYAnchor, width: 150, height: 150)
+        name.anchor(top: profiles.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor, paddingTop: 5, paddingLeft: 20, paddingRight: 20, centerX: view.centerXAnchor)
+        definesPresentationContext = true
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onStatusCall(_:)), name: NSNotification.Name(rawValue: "onStatusCall"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
+        
+        if isOutgoing {
+            users.append(User.getData(pin: UserDefaults.standard.string(forKey: "me")!)!)
+            API.initiateCR(sConfRoom: roomId)
+            outgoingView()
+        } else {
+            users.append(User.getData(pin: UserDefaults.standard.string(forKey: "me")!)!)
+            API.joinCR(sConfRoom: roomId)
+            ongoingView()
+        }
+    }
+    
+    override func viewWillLayoutSubviews() {
+        super.viewWillLayoutSubviews()
+        end.circle()
+        reject.circle()
+        accept.circle()
+        invite.circle()
+        speaker.circle()
+    }
+    
+//    private func getUserData(completion: @escaping (User?) -> ()) {
+//        if let user = self.user {
+//            completion(user)
+//            return
+//        }
+//        var user: User?
+//        DispatchQueue.global().async {
+//            Database.shared.database?.inTransaction({ fmdb, rollback in
+//                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select f_pin, first_name, last_name, image_id from BUDDY where f_pin = '\(self.data)'"), cursor.next() {
+//                    user = User(pin: cursor.string(forColumnIndex: 0) ?? "",
+//                                firstName: cursor.string(forColumnIndex: 1) ?? "",
+//                                lastName: cursor.string(forColumnIndex: 2) ?? "",
+//                                thumb: cursor.string(forColumnIndex: 3) ?? "")
+//                    cursor.close()
+//                }
+//            })
+//        }
+//        completion(user)
+//    }
+    
+    private func outgoingView() {
+        view.addSubview(end)
+        end.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize, height: buttonSize)
+        
+        end.addTarget(self, action: #selector(didEnd(sender:)), for: .touchUpInside)
+    }
+    
+    private func incomingView() {
+        status.text = "Incoming..."
+        
+        stack.spacing = buttonSize
+        
+        view.addSubview(stack)
+        
+        stack.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize * 3, height: buttonSize)
+        
+        stack.addArrangedSubview(reject)
+        stack.addArrangedSubview(accept)
+        
+        reject.addTarget(self, action: #selector(didReject(sender:)), for: .touchUpInside)
+        accept.addTarget(self, action: #selector(didAccept(sender:)), for: .touchUpInside)
+    }
+    
+    private func ongoingView() {
+        status.text = "Connecting..."
+        
+        stack.spacing = buttonSize / 2
+        
+        view.addSubview(stack)
+        
+        stack.anchor(bottom: view.bottomAnchor, paddingBottom: 60, centerX: view.centerXAnchor, width: buttonSize * 4, height: buttonSize)
+        
+        stack.addArrangedSubview(invite)
+        stack.addArrangedSubview(end)
+        stack.addArrangedSubview(speaker)
+        
+        invite.addTarget(self, action: #selector(didInvite(sender:)), for: .touchUpInside)
+        end.addTarget(self, action: #selector(didEnd(sender:)), for: .touchUpInside)
+        speaker.addTarget(self, action: #selector(didSpeaker(sender:)), for: .touchUpInside)
+    }
+    
+    
+    // MARK: - Action
+    
+    @objc func didSpeaker(sender: Any?) {
+        isSpeaker = !isSpeaker
+        speaker.isSelected = isSpeaker
+        do {
+            if "iPhone 6" == UIDevice.current.modelName {
+                try AVAudioSession.sharedInstance().overrideOutputAudioPort(isSpeaker ? .speaker : .none)
+            }
+        } catch {
+        }
+    }
+    
+    @objc func didInvite(sender: Any?) {
+        let controller = QmeraCallContactViewController()
+        controller.isDismiss = { user in
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            if !onGoingCC.isEmpty {
+                DispatchQueue.global().async {
+                    _ = Nexilis.write(message: CoreMessage_TMessageBank.getCCRoomInvite(l_pin: user.pin, ticket_id: onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2], channel: "1"))
+                }
+                DispatchQueue.main.async {
+                    self.isAddCall = user.pin
+                }
+            } else {
+                self.users.append(user)
+                // Start Calling
+                Nexilis.shared.callManager.startCall(handle: user.pin)
+            }
+        }
+        controller.selectedUser.append(contentsOf: users)
+        present(UINavigationController(rootViewController: controller), animated: true, completion: nil)
+    }
+    
+    @objc func didEnd(sender: Any?) {
+        let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+        if !onGoingCC.isEmpty {
+            if sender != nil && sender is Bool {
+                self.dismiss(animated: false, completion: nil)
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+                let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+                DispatchQueue.global().async {
+                    let date = "\(Date().currentTimeMillis())"
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                                "type" : "1",
+                                "title" : "Contact Center".localized(),
+                                "time" : startTimeCC,
+                                "f_pin" : officer,
+                                "data" : complaintId,
+                                "time_end" : date,
+                                "complaint_id" : complaintId,
+                                "members" : "",
+                                "requester": requester
+                            ], replace: true)
+                        } catch {
+                            rollback.pointee = true
+                            print(error)
+                        }
+                    })
+                    UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                    UserDefaults.standard.removeObject(forKey: "membersCC")
+                    UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                    UserDefaults.standard.removeObject(forKey: "waitingRequestCC")
+                }
+                return
+            }
+            let alert = UIAlertController(title: "Interaction with Call Center is in progress".localized(), message: "Are you sure you want to end the Call Center?".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "No".localized(), style: UIAlertAction.Style.default, handler: nil))
+            alert.addAction(UIAlertAction(title: "Yes".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                self.dismiss(animated: false, completion: nil)
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+                let idMe = UserDefaults.standard.string(forKey: "me")!
+                let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+                DispatchQueue.global().async {
+                    let date = "\(Date().currentTimeMillis())"
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                                "type" : "1",
+                                "title" : "Contact Center".localized(),
+                                "time" : startTimeCC,
+                                "f_pin" : officer,
+                                "data" : complaintId,
+                                "time_end" : date,
+                                "complaint_id" : complaintId,
+                                "members" : "",
+                                "requester": requester
+                            ], replace: true)
+                        } catch {
+                            rollback.pointee = true
+                            print(error)
+                        }
+                    })
+                    if requester == idMe {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: officer))
+                    } else {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: requester))
+                    }
+                    UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                    UserDefaults.standard.removeObject(forKey: "membersCC")
+                    UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                    UserDefaults.standard.removeObject(forKey: "waitingRequestCC")
+                }
+//                if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
+//                    Nexilis.shared.callManager.end(call: call)
+//                }
+            }))
+            self.present(alert, animated: true, completion: nil)
+        } else {
+//            if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
+//                Nexilis.shared.callManager.end(call: call)
+//            }
+            dismiss(animated: false, completion: nil)
+        }
+    }
+    
+    @objc func didReject(sender: Any?) {
+        didEnd(sender: sender)
+    }
+    
+    @objc func didAccept(sender: Any?) {
+        NSLayoutConstraint.deactivate(stack.constraints)
+        stack.subviews.forEach { subview in
+            subview.removeFromSuperview()
+        }
+        ongoingView()
+        UIView.animate(withDuration: 0.3, animations: {
+            self.view.layoutIfNeeded()
+        })
+    }
+    
+    // MARK: - Communication
+    
+    @objc func onReceiveMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            if let dataMessage = data["message"] as? TMessage {
+                if (dataMessage.getCode() == CoreMessage_TMessageCode.PUSH_MEMBER_ROOM_CONTACT_CENTER) {
+                    let data = dataMessage.getBody(key: CoreMessage_TMessageKey.DATA)
+                    if !data.isEmpty {
+                        if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                            var members = ""
+                            let idMe = UserDefaults.standard.string(forKey: "me")!
+                            for json in jsonArray {
+                                if "\(json)" != idMe {
+                                    if members.isEmpty {
+                                        members = "\(json)"
+                                    } else {
+                                        members += ",\(json)"
+                                    }
+                                }
+                            }
+                            UserDefaults.standard.set(members, forKey: "inEditorPersonal")
+                        }
+                    }
+                    self.users.append(User.getData(pin: dataMessage.getPIN())!)
+                    // Start Calling
+                    if !self.isAddCall.isEmpty && self.isAddCall == dataMessage.getPIN(){
+//                        Nexilis.shared.callManager.startCall(handle: dataMessage.getPIN())
+                        API.initiateCCall(sParty: dataMessage.getPIN())
+                    }
+                }
+            }
+        }
+    }
+    
+    private func checkParticipant(fPin: String) {
+        if let user = User.getData(pin: fPin), !self.users.contains(user) {
+            self.users.append(user)
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            if !onGoingCC.isEmpty {
+                DispatchQueue.main.async {
+                    var members = ""
+                    for user in self.users {
+                        if user.pin == UserDefaults.standard.string(forKey: "me")! {
+                            continue
+                        } else if members.isEmpty {
+                            members = "\(user.pin)"
+                        } else {
+                            members = ",\(user.pin)"
+                        }
+                    }
+                    UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                }
+            }
+        }
+    }
+    
+    @objc func onStatusCall(_ notification: NSNotification) {
+        if let data = notification.userInfo,
+           let state = data["state"] as? Int,
+           let message = data["message"] as? String {
+            let arrayMessage = message.split(separator: ",")
+            print("UYY \(state) \(message)")
+            if state == 22 {
+                if users.count == 1 && firstCall {
+                    DispatchQueue.main.async {
+                        self.ongoingView()
+                        let connectDate = Date()
+                        self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
+                            let format = Utils.callDurationFormatter.string(from: Date().timeIntervalSince(connectDate))
+                            self.status.text = format
+                        }
+                        self.timer?.fire()
+                        self.firstCall = false
+                    }
+                }
+                if let _ = User.getData(pin: String(arrayMessage[1])) {
+                    checkParticipant(fPin: String(arrayMessage[1]))
+                } else {
+                    if isOutgoing {
+                        Nexilis.addFriend(fpin: String(arrayMessage[1])) { result in
+                            if result {
+                                self.checkParticipant(fPin: String(arrayMessage[1]))
+                            }
+                        }
+                    } else {
+                        DispatchQueue.main.asyncAfter(wallDeadline: .now() + 1, execute: {
+                            Nexilis.addFriend(fpin: String(arrayMessage[1])) { result in
+                                if result {
+                                    self.checkParticipant(fPin: String(arrayMessage[1]))
+                                }
+                            }
+                        })
+                    }
+                }
+            }
+//            if state == 23 {
+//                if users.count == 1 {
+//                    DispatchQueue.main.async {
+//                        self.status.text = "Ringing..."
+//                    }
+//                }
+//            } else if state == 22 {
+//                if users.count == 1 && firstCall {
+//                    DispatchQueue.main.async {
+//                        self.ongoingView()
+//                        let connectDate = Date()
+//                        self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
+//                            let format = Utils.callDurationFormatter.string(from: Date().timeIntervalSince(connectDate))
+//                            self.status.text = format
+//                        }
+//                        self.timer?.fire()
+//                        self.firstCall = false
+//                    }
+//                }
+//                if (!isOutgoing || !firstCall), users.count >= 1, let user = User.getData(pin: String(arrayMessage[1])), !users.contains(user) {
+//                    self.users.append(user)
+//                    let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+//                    if !onGoingCC.isEmpty {
+//                        DispatchQueue.main.async {
+//                            var members = ""
+//                            for user in self.users {
+//                                if members.isEmpty {
+//                                    members = "\(user.pin)"
+//                                } else {
+//                                    members = ",\(user.pin)"
+//                                }
+//                            }
+//                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+//                        }
+//                    }
+//                }
+//            } else if state == 28 {
+//                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+//                if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
+//                    users.remove(at: index)
+//                    if !onGoingCC.isEmpty && users.count != 0 {
+//                        let requester = onGoingCC.components(separatedBy: ",")[0]
+//                        let officer = onGoingCC.components(separatedBy: ",")[1]
+//                        if pin == requester || pin == officer {
+//                            DispatchQueue.main.async {
+//                                self.timer?.invalidate()
+//                                self.timer = nil
+//                                self.status.text = "Call Center Session has ended..."
+//                                self.end.isEnabled = false
+//                            }
+//                            DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+//                                self.didEnd(sender: nil)
+//                            }
+//                            return
+//                        }
+//                    } else if !onGoingCC.isEmpty  && users.count == 0 {
+//                        DispatchQueue.main.async {
+//                            self.timer?.invalidate()
+//                            self.timer = nil
+//                            self.status.text = "Call Center Session has ended..."
+//                            self.end.isEnabled = false
+//                        }
+//                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+//                            self.didEnd(sender: true)
+//                        }
+//                        return
+//                    }
+//                }
+//                if users.count == 0 {
+//                    DispatchQueue.main.async {
+//                        self.dismiss(animated: false, completion: nil)
+//                    }
+//                }
+//            } else if state == -3 { // Offline
+//                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+//                if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
+//                    users.remove(at: index)
+//                    if !onGoingCC.isEmpty && users.count != 0 {
+//                        DispatchQueue.main.async {
+//                            var members = ""
+//                            for user in self.users {
+//                                if members.isEmpty {
+//                                    members = "\(user.pin)"
+//                                } else {
+//                                    members = ",\(user.pin)"
+//                                }
+//                            }
+//                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+//                        }
+//                    }
+//                }
+//                if users.count == 0 {
+//                    DispatchQueue.main.async {
+//                        self.status.text = "Offline..."
+//                        self.end.isEnabled = false
+//                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+//                            self.didEnd(sender: nil)
+//                        }
+//                    }
+//                }
+//            } else if state == -4 { // Busy
+//                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+//                if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
+//                    users.remove(at: index)
+//                    if !onGoingCC.isEmpty && users.count != 0 {
+//                        DispatchQueue.main.async {
+//                            var members = ""
+//                            for user in self.users {
+//                                if members.isEmpty {
+//                                    members = "\(user.pin)"
+//                                } else {
+//                                    members = ",\(user.pin)"
+//                                }
+//                            }
+//                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+//                        }
+//                    }
+//                }
+//                if users.count == 0 {
+//                    DispatchQueue.main.async {
+//                        self.status.text = "Busy..."
+//                        self.end.isEnabled = false
+//                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
+//                            self.didEnd(sender: nil)
+//                        }
+//                    }
+//                }
+//            }
+        }
+    }
+
+}

+ 305 - 13
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraAudioViewController.swift

@@ -8,6 +8,7 @@
 import UIKit
 import AVFoundation
 import nuSDKService
+import NotificationBannerSwift
 
 class QmeraAudioViewController: UIViewController {
     
@@ -22,6 +23,8 @@ class QmeraAudioViewController: UIViewController {
     }
     
     var user: User?
+    
+    var isAddCall = ""
         
     private var users: [User] = [] {
         didSet {
@@ -162,6 +165,51 @@ class QmeraAudioViewController: UIViewController {
         return stackView
     }()
     
+    let poweredByView: UIStackView = {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 5
+        return stackView
+    }()
+    
+    let poweredByLabel: UILabel = {
+        let label = UILabel()
+        label.text = "Powered by Nexilis"
+        return label
+    }()
+    
+    let qmeraLogo: UIButton = {
+        let image = UIImage(named: "Q-Button-PNG", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let button = UIButton()
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.frame.size.width = 30
+        button.frame.size.height = 30
+        return button
+    }()
+    
+    let nexilisLogo: UIButton = {
+        let image = UIImage(named: "pb_powered", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let button = UIButton()
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.frame.size.width = 30
+        button.frame.size.height = 30
+        return button
+    }()
+    
+    public override func viewWillDisappear(_ animated: Bool) {
+        if self.isMovingFromParent {
+            NotificationCenter.default.removeObserver(self)
+        }
+    }
+    
     
     override func viewDidLoad() {
         super.viewDidLoad()
@@ -180,11 +228,23 @@ class QmeraAudioViewController: UIViewController {
         definesPresentationContext = true
         
         NotificationCenter.default.addObserver(self, selector: #selector(onStatusCall(_:)), name: NSNotification.Name(rawValue: "onStatusCall"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
         
         if let u = self.user {
             self.users.append(u)
             if isOutgoing {
-                Nexilis.shared.callManager.startCall(handle: u.pin)
+                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+                if onGoingCC.isEmpty {
+                    Nexilis.shared.callManager.startCall(handle: u.pin)
+                } else {
+                    do {
+                        try AVAudioSession.sharedInstance().setCategory(.playAndRecord)
+                        try AVAudioSession.sharedInstance().setMode(.voiceChat)
+                        try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none)
+                    } catch {
+                    }
+                    API.initiateCCall(sParty: u.pin)
+                }
             }
         }
         
@@ -265,6 +325,22 @@ class QmeraAudioViewController: UIViewController {
         invite.addTarget(self, action: #selector(didInvite(sender:)), for: .touchUpInside)
         end.addTarget(self, action: #selector(didEnd(sender:)), for: .touchUpInside)
         speaker.addTarget(self, action: #selector(didSpeaker(sender:)), for: .touchUpInside)
+        
+        self.view.addSubview(poweredByView)
+        self.poweredByView.translatesAutoresizingMaskIntoConstraints = false
+        let constraintRightPowered =  self.poweredByView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0)
+        let constraintBottomPowered = self.poweredByView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0)
+        NSLayoutConstraint.activate([
+            constraintRightPowered,
+            constraintBottomPowered,
+            poweredByView.heightAnchor.constraint(equalToConstant: 40.0),
+            nexilisLogo.widthAnchor.constraint(equalToConstant: 30.0),
+            nexilisLogo.heightAnchor.constraint(equalToConstant: 30.0)
+        ])
+        
+        poweredByView.addArrangedSubview(poweredByLabel)
+        poweredByView.addArrangedSubview(nexilisLogo)
+        
     }
     
     
@@ -286,19 +362,121 @@ class QmeraAudioViewController: UIViewController {
     @objc func didInvite(sender: Any?) {
         let controller = QmeraCallContactViewController()
         controller.isDismiss = { user in
-            self.users.append(user)
-            // Start Calling
-            Nexilis.shared.callManager.startCall(handle: user.pin)
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            if !onGoingCC.isEmpty {
+                DispatchQueue.global().async {
+                    _ = Nexilis.write(message: CoreMessage_TMessageBank.getCCRoomInvite(l_pin: user.pin, ticket_id: onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2], channel: "1"))
+                }
+                DispatchQueue.main.async {
+                    self.isAddCall = user.pin
+                }
+            } else {
+                self.users.append(user)
+                // Start Calling
+                Nexilis.shared.callManager.startCall(handle: user.pin)
+            }
         }
         controller.selectedUser.append(contentsOf: users)
         present(UINavigationController(rootViewController: controller), animated: true, completion: nil)
     }
     
     @objc func didEnd(sender: Any?) {
-        if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
-            Nexilis.shared.callManager.end(call: call)
+        poweredByView.isHidden = true
+        let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+        if !onGoingCC.isEmpty {
+            if sender != nil && sender is Bool {
+                self.dismiss(animated: false, completion: nil)
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+                let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+                DispatchQueue.global().async {
+                    if sender as! Bool == true {
+                        let date = "\(Date().currentTimeMillis())"
+                        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                            do {
+                                _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                                    "type" : "1",
+                                    "title" : "Contact Center".localized(),
+                                    "time" : startTimeCC,
+                                    "f_pin" : officer,
+                                    "data" : complaintId,
+                                    "time_end" : date,
+                                    "complaint_id" : complaintId,
+                                    "members" : "",
+                                    "requester": requester
+                                ], replace: true)
+                            } catch {
+                                rollback.pointee = true
+                                print(error)
+                            }
+                        })
+                    }
+                    UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                    UserDefaults.standard.removeObject(forKey: "membersCC")
+                    UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                    UserDefaults.standard.removeObject(forKey: "waitingRequestCC")
+                }
+                return
+            }
+            let alert = UIAlertController(title: "Interaction with Call Center is in progress".localized(), message: "Are you sure you want to end the Call Center?".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "No".localized(), style: UIAlertAction.Style.default, handler: nil))
+            alert.addAction(UIAlertAction(title: "Yes".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                self.dismiss(animated: false, completion: nil)
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+                let idMe = UserDefaults.standard.string(forKey: "me")!
+                let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+                DispatchQueue.global().async {
+                    let date = "\(Date().currentTimeMillis())"
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        do {
+                            _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                                "type" : "1",
+                                "title" : "Contact Center".localized(),
+                                "time" : startTimeCC,
+                                "f_pin" : officer,
+                                "data" : complaintId,
+                                "time_end" : date,
+                                "complaint_id" : complaintId,
+                                "members" : "",
+                                "requester": requester
+                            ], replace: true)
+                        } catch {
+                            rollback.pointee = true
+                            print(error)
+                        }
+                    })
+                    if officer == idMe {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: requester))
+                    } else {
+                        if requester == idMe {
+                            _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: officer))
+                        } else {
+                            _ = Nexilis.write(message: CoreMessage_TMessageBank.leaveCCRoomInvite(ticket_id: complaintId))
+                        }
+                    }
+                    UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                    UserDefaults.standard.removeObject(forKey: "membersCC")
+                    UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                    UserDefaults.standard.removeObject(forKey: "waitingRequestCC")
+                }
+                if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
+                    Nexilis.shared.callManager.end(call: call)
+                } else {
+                    API.terminateCall(sParty: nil)
+                }
+            }))
+            self.present(alert, animated: true, completion: nil)
+        } else {
+            if let user = self.user, let call = Nexilis.shared.callManager.call(with: user.pin) {
+                Nexilis.shared.callManager.end(call: call)
+            } else {
+                API.terminateCall(sParty: nil)
+            }
+            dismiss(animated: false, completion: nil)
         }
-        dismiss(animated: false, completion: nil)
     }
     
     @objc func didReject(sender: Any?) {
@@ -316,9 +494,41 @@ class QmeraAudioViewController: UIViewController {
         })
     }
     
-    
     // MARK: - Communication
     
+    @objc func onReceiveMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            if let dataMessage = data["message"] as? TMessage {
+                if (dataMessage.getCode() == CoreMessage_TMessageCode.PUSH_MEMBER_ROOM_CONTACT_CENTER) {
+                    let data = dataMessage.getBody(key: CoreMessage_TMessageKey.DATA)
+                    if !data.isEmpty {
+                        if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                            var members = ""
+                            let idMe = UserDefaults.standard.string(forKey: "me")!
+                            for json in jsonArray {
+                                if "\(json)" != idMe {
+                                    if members.isEmpty {
+                                        members = "\(json)"
+                                    } else {
+                                        members += ",\(json)"
+                                    }
+                                }
+                            }
+                            UserDefaults.standard.set(members, forKey: "inEditorPersonal")
+                        }
+                    }
+                    self.users.append(User.getData(pin: dataMessage.getPIN())!)
+                    // Start Calling
+                    if !self.isAddCall.isEmpty && self.isAddCall == dataMessage.getPIN() {
+//                        Nexilis.shared.callManager.startCall(handle: dataMessage.getPIN())
+                        API.initiateCCall(sParty: dataMessage.getPIN())
+                    }
+                }
+            }
+        }
+    }
+    
     @objc func onStatusCall(_ notification: NSNotification) {
         if let data = notification.userInfo,
            let state = data["state"] as? Int,
@@ -344,12 +554,66 @@ class QmeraAudioViewController: UIViewController {
                         self.firstCall = false
                     }
                 }
-                if !isOutgoing, users.count >= 1, let user = User.getData(pin: String(arrayMessage[1])), !users.contains(user) {
+                if (!isOutgoing || !firstCall), users.count >= 1, let user = User.getData(pin: String(arrayMessage[1])), !users.contains(user) {
                     self.users.append(user)
+                    let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+                    if !onGoingCC.isEmpty {
+                        DispatchQueue.main.async {
+                            var members = ""
+                            for user in self.users {
+                                if members.isEmpty {
+                                    members = "\(user.pin)"
+                                } else {
+                                    members = ",\(user.pin)"
+                                }
+                            }
+                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                        }
+                    }
                 }
             } else if state == 28 {
+                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
                 if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
                     users.remove(at: index)
+                    if !onGoingCC.isEmpty && users.count != 0 {
+                        let requester = onGoingCC.components(separatedBy: ",")[0]
+                        let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                        if pin == requester || pin == officer {
+                            DispatchQueue.main.async {
+                                if self.viewIfLoaded?.window != nil {
+                                    let imageView = UIImageView(image: UIImage(systemName: "info.circle"))
+                                    imageView.tintColor = .white
+                                    let banner = FloatingNotificationBanner(title: "Call Center Session has ended".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .info, colors: nil, iconPosition: .center)
+                                    banner.show()
+                                }
+                                self.timer?.invalidate()
+                                self.timer = nil
+                                self.status.text = "Call Center Session has ended..."
+                                self.end.isEnabled = false
+                            }
+                            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                                self.didEnd(sender: true)
+                            }
+                            return
+                        }
+                    } else if !onGoingCC.isEmpty && users.count == 0 {
+                        DispatchQueue.main.async {
+                            if self.viewIfLoaded?.window != nil {
+                                let imageView = UIImageView(image: UIImage(systemName: "info.circle"))
+                                imageView.tintColor = .white
+                                let banner = FloatingNotificationBanner(title: "Call Center Session has ended".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .info, colors: nil, iconPosition: .center)
+                                banner.show()
+                            }
+                            self.timer?.invalidate()
+                            self.timer = nil
+                            self.status.text = "Call Center Session has ended..."
+                            self.end.isEnabled = false
+                        }
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                            self.didEnd(sender: true)
+                        }
+                        return
+                    }
                 }
                 if users.count == 0 {
                     DispatchQueue.main.async {
@@ -357,28 +621,56 @@ class QmeraAudioViewController: UIViewController {
                     }
                 }
             } else if state == -3 { // Offline
+                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
                 if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
                     users.remove(at: index)
+                    if !onGoingCC.isEmpty && users.count != 0 {
+                        DispatchQueue.main.async {
+                            var members = ""
+                            for user in self.users {
+                                if members.isEmpty {
+                                    members = "\(user.pin)"
+                                } else {
+                                    members = ",\(user.pin)"
+                                }
+                            }
+                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                        }
+                    }
                 }
                 if users.count == 0 {
                     DispatchQueue.main.async {
                         self.status.text = "Offline..."
                         self.end.isEnabled = false
-                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                            self.didEnd(sender: nil)
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                            self.didEnd(sender: false)
                         }
                     }
                 }
             } else if state == -4 { // Busy
+                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
                 if let pin = arrayMessage.first, let index = users.firstIndex(of: User(pin: String(pin))) {
                     users.remove(at: index)
+                    if !onGoingCC.isEmpty && users.count != 0 {
+                        DispatchQueue.main.async {
+                            var members = ""
+                            for user in self.users {
+                                if members.isEmpty {
+                                    members = "\(user.pin)"
+                                } else {
+                                    members = ",\(user.pin)"
+                                }
+                            }
+                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                        }
+                    }
                 }
                 if users.count == 0 {
                     DispatchQueue.main.async {
                         self.status.text = "Busy..."
                         self.end.isEnabled = false
-                        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
-                            self.didEnd(sender: nil)
+                        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
+                            self.didEnd(sender: false)
                         }
                     }
                 }

+ 106 - 12
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraCallContactViewController.swift

@@ -34,17 +34,32 @@ class QmeraCallContactViewController: UITableViewController {
     
     var selectedUser: [User] = []
     
+    var isInviteCC = false
+    
+    var listFriends = false
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         
         title = "Friends".localized()
         
-        navigationController?.navigationBar.prefersLargeTitles = true
+        if !isInviteCC {
+            navigationController?.navigationBar.prefersLargeTitles = true
+        } else {
+            let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+            navigationController?.navigationBar.titleTextAttributes = textAttributes
+            navigationController?.navigationBar.barTintColor = .mainColor
+        }
         
         tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier")
         
         definesPresentationContext = true
         
+        if !listFriends {
+            let buttonAddFriend = UIBarButtonItem(image: UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(addFriend(sender:)))
+            navigationItem.rightBarButtonItem = buttonAddFriend
+        }
+        
         searchController.delegate = self
         searchController.searchResultsUpdater = self
         searchController.searchBar.delegate = self
@@ -60,22 +75,49 @@ class QmeraCallContactViewController: UITableViewController {
         
     }
     
+    @objc func addFriend(sender: UIBarButtonItem) {
+        let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
+        if let vc = controller.viewControllers.first as? AddFriendTableViewController {
+            vc.isDismiss = {
+                self.users.removeAll()
+                self.getContacts { users in
+                    self.users.append(contentsOf: users)
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                    }
+                }
+            }
+        }
+        self.navigationController?.present(controller, animated: true, completion: nil)
+    }
+    
     private func getContacts(completion: @escaping ([User]) -> ()) {
         var contacts: [User] = []
         DispatchQueue.global().async {
+            var query = "SELECT f_pin, first_name, last_name, image_id, user_type, official_account, ex_offmp FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' and official_account<>'1' order by 2 collate nocase asc"
+            if self.listFriends {
+                query = "SELECT f_pin, first_name, last_name, image_id, user_type, official_account, ex_offmp FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' order by 2 collate nocase asc"
+            }
             Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, image_id FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' and official_account<>'1' order by 2 collate nocase asc") {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: query) {
                     while cursor.next() {
                         let user = User(pin: cursor.string(forColumnIndex: 0) ?? "",
                                         firstName: cursor.string(forColumnIndex: 1) ?? "",
                                         lastName: cursor.string(forColumnIndex: 2) ?? "",
-                                        thumb: cursor.string(forColumnIndex: 3) ?? "")
+                                        thumb: cursor.string(forColumnIndex: 3) ?? "",
+                                        userType: cursor.string(forColumnIndex: 4) ?? "",
+                                        official: cursor.string(forColumnIndex: 5) ?? "",
+                                        ex_offmp: cursor.string(forColumnIndex: 6) ?? "")
                         contacts.append(user)
                     }
                     cursor.close()
                 }
             })
-            completion(contacts.filter { !self.selectedUser.contains($0) })
+            var dataContacts = contacts.filter { !self.selectedUser.contains($0) }
+            if self.listFriends {
+                dataContacts.sort(by: { Int($0.official!)! > Int($1.official!)! })
+            }
+            completion(dataContacts)
         }
     }
     
@@ -94,17 +136,41 @@ class QmeraCallContactViewController: UITableViewController {
     }
     
     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        tableView.deselectRow(at: indexPath, animated: false)
         let user: User
         if isFilltering {
             user = fillteredUser[indexPath.row]
         } else {
             user = users[indexPath.row]
         }
-        user.isSelected = true
-        tableView.reloadRows(at: [indexPath], with: .none)
-        dismiss(animated: true) {
-            self.isDismiss?(user)
+        if !listFriends {
+            tableView.deselectRow(at: indexPath, animated: false)
+            user.isSelected = true
+            tableView.reloadRows(at: [indexPath], with: .none)
+        }
+        if isInviteCC {
+            if !listFriends {
+                self.isDismiss?(user)
+                self.navigationController?.popViewController(animated: true)
+            } else {
+                let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
+                controller.data = user.pin
+                controller.flag = .friend
+                controller.isBNI = true
+                controller.isDismiss = {
+                    self.getContacts { users in
+                        self.users.removeAll()
+                        self.users.append(contentsOf: users)
+                        DispatchQueue.main.async {
+                            self.tableView.reloadData()
+                        }
+                    }
+                }
+                show(controller, sender: nil)
+            }
+        } else {
+            dismiss(animated: true) {
+                self.isDismiss?(user)
+            }
         }
     }
     
@@ -125,11 +191,24 @@ class QmeraCallContactViewController: UITableViewController {
             user = users[indexPath.row]
         }
         content.imageProperties.maximumSize = CGSize(width: 44, height: 44)
-        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
             content.image = image
-            tableView.reloadRows(at: [indexPath], with: .none)
         }
-        content.text = user.fullName
+        if (user.official == "1") {
+            content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(user.fullName)", size: 15, y: -4)
+        }
+        else if user.userType == "23" {
+            content.attributedText = self.set(image: UIImage(named: "ic_internal", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(user.fullName)", size: 15, y: -4)
+        } else if user.userType == "24" {
+            let dataCategory = CategoryCC.getDataFromServiceId(service_id: user.ex_offmp!)
+            if dataCategory != nil {
+                content.attributedText = self.set(image: UIImage(named: "pb_call_center", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(user.fullName) (\(dataCategory!.service_name))", size: 15, y: -4)
+            } else {
+                content.attributedText = self.set(image: UIImage(named: "pb_call_center", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(user.fullName)", size: 15, y: -4)
+            }
+        } else {
+            content.text = user.fullName
+        }
         cell.contentConfiguration = content
         cell.accessoryType = user.isSelected ? .checkmark : .none
         return cell
@@ -143,4 +222,19 @@ extension QmeraCallContactViewController: UISearchControllerDelegate, UISearchBa
         filterContentForSearchText(searchController.searchBar.text!)
     }
     
+    func set(image: UIImage, with text: String, size: CGFloat, y: CGFloat) -> NSAttributedString {
+        let attachment = NSTextAttachment()
+        attachment.image = image
+        attachment.bounds = CGRect(x: 0, y: y, width: size, height: size)
+        let attachmentStr = NSAttributedString(attachment: attachment)
+        
+        let mutableAttributedString = NSMutableAttributedString()
+        mutableAttributedString.append(attachmentStr)
+        
+        let textString = NSAttributedString(string: text)
+        mutableAttributedString.append(textString)
+        
+        return mutableAttributedString
+    }
+    
 }

+ 437 - 93
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/QmeraVideoViewController.swift

@@ -13,6 +13,7 @@
 import UIKit
 import nuSDKService
 import AVFoundation
+import NotificationBannerSwift
 
 class QmeraVideoViewController: UIViewController {
     var dataPerson: [[String: String?]] = []
@@ -27,6 +28,13 @@ class QmeraVideoViewController: UIViewController {
         UIImageView(),
         UIImageView()
     ]
+    var containerLabelName: [UIView] = [
+        UIView(),
+        UIView(),
+        UIView(),
+        UIView(),
+        UIView()
+    ]
     let myImage = UIImageView()
     let name = UILabel()
     let profileImage = UIImageView()
@@ -51,20 +59,61 @@ class QmeraVideoViewController: UIViewController {
     var isAutoAccept = false
     var wbTimer = Timer()
     var wbBlink = false
+    var showNotifCCEnd = false
+    var transformZoomAfterNewUserMore2 = false
+    var isAddCall = ""
+    var users: [User] = []
+    let poweredByView: UIStackView = {
+        let stackView = UIStackView()
+        stackView.axis = .horizontal
+        stackView.spacing = 5
+        return stackView
+    }()
     
-    override func viewWillDisappear(_ animated: Bool) {
-        navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
-        navigationController?.navigationBar.shadowImage = nil
-        navigationController?.navigationBar.isTranslucent = false
-        navigationController?.view.backgroundColor = nil
-        let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
-        navigationController?.navigationBar.titleTextAttributes = textAttributes
-        navigationController?.navigationBar.topItem?.backBarButtonItem = nil
-        navigationController?.interactivePopGestureRecognizer?.isEnabled = true
-    }
+    let poweredByLabel: UILabel = {
+        let label = UILabel()
+        label.text = "Powered by Nexilis"
+        return label
+    }()
+    
+    let qmeraLogo: UIButton = {
+        let image = UIImage(named: "Q-Button-PNG", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let button = UIButton()
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.frame.size.width = 30
+        button.frame.size.height = 30
+        return button
+    }()
     
-    override func viewDidDisappear(_ animated: Bool) {
-        NotificationCenter.default.removeObserver(self, name: NSNotification.Name(rawValue: "onStatusCall"), object: nil)
+    let nexilisLogo: UIButton = {
+        let image = UIImage(named: "pb_powered", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+        let button = UIButton()
+        button.setImage(image, for: .normal)
+        button.imageView?.contentMode = .scaleAspectFit
+        button.imageEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
+        button.contentVerticalAlignment = .fill
+        button.contentHorizontalAlignment = .fill
+        button.frame.size.width = 30
+        button.frame.size.height = 30
+        return button
+    }()
+    
+    override func viewWillDisappear(_ animated: Bool) {
+        if self.isMovingFromParent {
+            navigationController?.navigationBar.setBackgroundImage(nil, for: .default)
+            navigationController?.navigationBar.shadowImage = nil
+            navigationController?.navigationBar.isTranslucent = false
+            navigationController?.view.backgroundColor = .mainColor
+            let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
+            navigationController?.navigationBar.titleTextAttributes = textAttributes
+            navigationController?.navigationBar.topItem?.backBarButtonItem = nil
+            navigationController?.interactivePopGestureRecognizer?.isEnabled = true
+            NotificationCenter.default.removeObserver(self)
+        }
     }
 
     override func viewDidLoad() {
@@ -83,11 +132,12 @@ class QmeraVideoViewController: UIViewController {
         addZoomView()
         addCameraView()
         addListRemoteView()
-//        addBackgroundIncoming()
+        addBackgroundIncoming()
         addProfileNameCalling()
         Calling()
         addToolbar()
         NotificationCenter.default.addObserver(self, selector: #selector(self.onStatusCall(_:)), name: NSNotification.Name(rawValue: "onStatusCall"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
         if isAutoAccept {
             didTapAcceptCallButton()
         }
@@ -176,20 +226,28 @@ class QmeraVideoViewController: UIViewController {
         ])
         myImage.backgroundColor = .lightGray
         myImage.tintColor = .secondaryColor
-        let idMe = UserDefaults.standard.string(forKey: "me") as String?
-        Database().database?.inTransaction({ fmdb, rollback in
-            if let c = Database().getRecords(fmdb: fmdb, query: "select image_id from BUDDY where f_pin = '\(idMe!)'"), c.next() {
-                let image = c.string(forColumnIndex: 0)!
-                if image.isEmpty {
-                    myImage.image = UIImage(systemName: "person")
-                    myImage.contentMode = .scaleAspectFit
-                } else {
-                    myImage.setImage(name: image)
-                    myImage.contentMode = .scaleAspectFill
-                }
-                c.close()
-            }
-        })
+        let image = dataPerson[0]["picture"]!!
+        if image.isEmpty {
+            myImage.image = UIImage(systemName: "person")
+            myImage.contentMode = .scaleAspectFit
+        } else {
+            myImage.setImage(name: image)
+            myImage.contentMode = .scaleAspectFill
+        }
+//        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+//        Database().database?.inTransaction({ fmdb, rollback in
+//            if let c = Database().getRecords(fmdb: fmdb, query: "select image_id from BUDDY where f_pin = '\(idMe!)'"), c.next() {
+//                let image = c.string(forColumnIndex: 0)!
+//                if image.isEmpty {
+//                    myImage.image = UIImage(systemName: "person")
+//                    myImage.contentMode = .scaleAspectFit
+//                } else {
+//                    myImage.setImage(name: image)
+//                    myImage.contentMode = .scaleAspectFill
+//                }
+//                c.close()
+//            }
+//        })
     }
     
     func addProfileNameCalling() {
@@ -292,19 +350,78 @@ class QmeraVideoViewController: UIViewController {
         }
     }
     
+    @objc func onReceiveMessage(notification: NSNotification) {
+        DispatchQueue.main.async {
+            let data:[AnyHashable : Any] = notification.userInfo!
+            if let dataMessage = data["message"] as? TMessage {
+                if (dataMessage.getCode() == CoreMessage_TMessageCode.PUSH_MEMBER_ROOM_CONTACT_CENTER) {
+                    let data = dataMessage.getBody(key: CoreMessage_TMessageKey.DATA)
+                    if !data.isEmpty {
+                        if let jsonArray = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: JSONSerialization.ReadingOptions()) as? [AnyObject] {
+                            var members = ""
+                            let idMe = UserDefaults.standard.string(forKey: "me")!
+                            for json in jsonArray {
+                                if "\(json)" != idMe {
+                                    if members.isEmpty {
+                                        members = "\(json)"
+                                    } else {
+                                        members += ",\(json)"
+                                    }
+                                }
+                            }
+                            UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                        }
+                    }
+                    // Start Calling
+                    if !self.isAddCall.isEmpty && self.isAddCall == dataMessage.getPIN(){
+                        let user = User.getData(pin: dataMessage.getPIN())!
+                        var dataPerson: [String: String?] = [:]
+                        dataPerson["f_pin"] = user.pin
+                        dataPerson["name"] = user.fullName
+                        dataPerson["picture"] = user.thumb
+                        dataPerson["isOfficial"] = user.official
+                        dataPerson["deviceId"] = ""
+                        dataPerson["isOffline"] = ""
+                        dataPerson["user_type"] = user.userType
+                        self.dataPerson.append(dataPerson)
+                        self.users.append(user)
+                        API.initiateCCall(sParty: dataMessage.getPIN(), nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
+                    }
+                }
+            }
+        }
+    }
+    
     @objc func didTapDeclineCallButton(sender: AnyObject){
-        endAllCall()
-        if isInisiator {
-            self.navigationController?.popViewController(animated: true)
+        let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+        if !onGoingCC.isEmpty {
+            let alert = UIAlertController(title: "Interaction with Call Center is in progress".localized(), message: "Are you sure you want to end the Call Center?".localized(), preferredStyle: .alert)
+            alert.addAction(UIAlertAction(title: "No".localized(), style: UIAlertAction.Style.default, handler: nil))
+            alert.addAction(UIAlertAction(title: "Yes".localized(), style: UIAlertAction.Style.default, handler: {(_) in
+                if(!self.wbRoomId.isEmpty){
+                    DispatchQueue.main.async {
+                        self.wbTimer.invalidate()
+                        _ = Nexilis.getWhiteboardDelegate()?.terminate()
+                    }
+                }
+                self.endAllCall()
+                self.dismiss(animated: true, completion: nil)
+            }))
+            self.present(alert, animated: true, completion: nil)
         } else {
-            self.dismiss(animated: true, completion: nil)
+            endAllCall()
+            if isInisiator {
+                self.navigationController?.popViewController(animated: true)
+            } else {
+                self.dismiss(animated: true, completion: nil)
+            }
         }
     }
     
     @objc func didTapAcceptCallButton(){
         API.receiveCCall(sParty: dataPerson[0]["f_pin"]!, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: listRemoteViewFix, ivLocalView: cameraView,ivRemoteZ: zoomView)
         DispatchQueue.main.async {
-//            self.myImage.removeFromSuperview()
+            self.myImage.removeFromSuperview()
             self.name.removeFromSuperview()
             self.profileImage.removeFromSuperview()
             self.labelIncomingOutgoing.removeFromSuperview()
@@ -322,6 +439,7 @@ class QmeraVideoViewController: UIViewController {
                 self.buttonAddParticipant.isHidden = false
                 self.buttonSpeaker.isHidden = false
                 self.buttonWB.isHidden = false
+                self.poweredByView.isHidden = false
             }
         }
     }
@@ -449,6 +567,22 @@ class QmeraVideoViewController: UIViewController {
         buttonWB.isHidden = true
         buttonWB.addTarget(self, action: #selector(didTapWBButton), for: .touchUpInside)
         
+        self.view.addSubview(poweredByView)
+        self.poweredByView.translatesAutoresizingMaskIntoConstraints = false
+        let constraintRightPowered =  self.poweredByView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: -10.0)
+        let constraintBottomPowered = self.poweredByView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor, constant: -10.0)
+        NSLayoutConstraint.activate([
+            constraintRightPowered,
+            constraintBottomPowered,
+            poweredByView.heightAnchor.constraint(equalToConstant: 40.0),
+            nexilisLogo.widthAnchor.constraint(equalToConstant: 30.0),
+            nexilisLogo.heightAnchor.constraint(equalToConstant: 30.0)
+        ])
+        
+        poweredByView.addArrangedSubview(poweredByLabel)
+        poweredByView.addArrangedSubview(nexilisLogo)
+        poweredByView.isHidden = true
+        
         stackViewToolbar.addArrangedSubview(buttonAddParticipant)
         stackViewToolbar.addArrangedSubview(buttonDecline)
         stackViewToolbar.addArrangedSubview(buttonSpeaker)
@@ -457,6 +591,48 @@ class QmeraVideoViewController: UIViewController {
     }
     
     func endAllCall() {
+        let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+        if !onGoingCC.isEmpty {
+            let requester = onGoingCC.components(separatedBy: ",")[0]
+            let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+            let complaintId = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2]
+            let idMe = UserDefaults.standard.string(forKey: "me")!
+            let startTimeCC = UserDefaults.standard.string(forKey: "startTimeCC") ?? ""
+            DispatchQueue.global().async {
+                let date = "\(Date().currentTimeMillis())"
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    do {
+                        _ = try Database.shared.insertRecord(fmdb: fmdb, table: "CALL_CENTER_HISTORY", cvalues: [
+                            "type" : "2",
+                            "title" : "Contact Center".localized(),
+                            "time" : startTimeCC,
+                            "f_pin" : officer,
+                            "data" : complaintId,
+                            "time_end" : date,
+                            "complaint_id" : complaintId,
+                            "members" : "",
+                            "requester": requester
+                        ], replace: true)
+                    } catch {
+                        rollback.pointee = true
+                        print(error)
+                    }
+                })
+                if officer == idMe {
+                    _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: requester))
+                } else {
+                    if requester == idMe {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.endCallCenter(complaint_id: complaintId, l_pin: officer))
+                    } else {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.leaveCCRoomInvite(ticket_id: complaintId))
+                    }
+                }
+                UserDefaults.standard.removeObject(forKey: "onGoingCC")
+                UserDefaults.standard.removeObject(forKey: "membersCC")
+                UserDefaults.standard.removeObject(forKey: "startTimeCC")
+                UserDefaults.standard.removeObject(forKey: "waitingRequestCC")
+            }
+        }
         API.terminateCall(sParty: nil)
         cameraView.image = nil
         zoomView.image = nil
@@ -464,21 +640,21 @@ class QmeraVideoViewController: UIViewController {
     }
     
     func setSpeaker(isSpeaker: Bool) {
-//        DispatchQueue.main.async {
-//            if (isSpeaker) {
-//                self.buttonSpeaker.backgroundColor = .secondaryColor
-//                self.buttonSpeaker.tintColor = .mainColor
-//                self.buttonSpeaker.setImage(UIImage(systemName: "speaker.wave.2", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30, weight: .medium, scale: .default)), for: .normal)
-//            } else {
-//                self.buttonSpeaker.backgroundColor = .mainColor
-//                self.buttonSpeaker.tintColor = .secondaryColor
-//                self.buttonSpeaker.setImage(UIImage(systemName: "speaker.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30, weight: .medium, scale: .default)), for: .normal)
-//            }
-//            self.isSpeaker = isSpeaker
-//        }
+        DispatchQueue.main.async {
+            if (isSpeaker) {
+                self.buttonSpeaker.backgroundColor = .lightGray
+                self.buttonSpeaker.tintColor = .mainColor
+                self.buttonSpeaker.setImage(UIImage(systemName: "speaker.wave.2", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30, weight: .medium, scale: .default)), for: .normal)
+            } else {
+                self.buttonSpeaker.backgroundColor = .secondaryColor
+                self.buttonSpeaker.tintColor = .mainColor
+                self.buttonSpeaker.setImage(UIImage(systemName: "speaker.slash", withConfiguration: UIImage.SymbolConfiguration(pointSize: 30, weight: .medium, scale: .default)), for: .normal)
+            }
+            self.isSpeaker = isSpeaker
+        }
 //        do {
-//            try AVAudioSession.sharedInstance().setCategory(isSpeaker ? .playAndRecord : .ambient)
-//            try AVAudioSession.sharedInstance().setMode(isSpeaker ? .videoChat : .default)
+//            try AVAudioSession.sharedInstance().setCategory(isSpeaker ? .playAndRecord : .playAndRecord, options: isSpeaker ? .defaultToSpeaker : .duckOthers)
+//            try AVAudioSession.sharedInstance().setMode(isSpeaker ? .videoChat : .videoChat)
 //            try AVAudioSession.sharedInstance().overrideOutputAudioPort(isSpeaker ? .speaker : .none)
 //        } catch {
 //
@@ -494,12 +670,22 @@ class QmeraVideoViewController: UIViewController {
             contactViewController.isAddParticipantVideo = true
             contactViewController.connectedCall = dataPerson
             contactViewController.isDismiss = { data in
-                DispatchQueue.main.async {
-                    self.dataPerson.append(data)
-                    API.initiateCCall(sParty: data["f_pin"] as? String, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
+                let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+                if !onGoingCC.isEmpty {
+                    DispatchQueue.global().async {
+                        _ = Nexilis.write(message: CoreMessage_TMessageBank.getCCRoomInvite(l_pin: data["f_pin"]!!, ticket_id: onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[2], channel: "2"))
+                    }
+                    DispatchQueue.main.async {
+                        self.isAddCall = data["f_pin"]!!
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        self.dataPerson.append(data)
+                        API.initiateCCall(sParty: data["f_pin"] as? String, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivRemoteView: self.listRemoteViewFix, ivLocalView: self.cameraView, ivRemoteZ: self.zoomView)
+                    }
                 }
             }
-            present(contactViewController, animated: true, completion: nil)
+            present(UINavigationController(rootViewController: contactViewController), animated: true, completion: nil)
         }
     }
     
@@ -529,8 +715,11 @@ class QmeraVideoViewController: UIViewController {
         let arrayMessage = message.split(separator: ",")
         if(state == 35){
             DispatchQueue.main.async {
-                if self.dataPerson.count == 2 {
-                    self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 1.9).rotated(by: (CGFloat.pi * 3)/2)
+                if self.dataPerson.count > 1 {
+                    if !self.transformZoomAfterNewUserMore2 {
+                        self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 1.9).rotated(by: (CGFloat.pi * 3)/2)
+                        self.transformZoomAfterNewUserMore2 = true
+                    }
                 }
             }
         }
@@ -547,7 +736,7 @@ class QmeraVideoViewController: UIViewController {
                 }
             }
         }
-        else if (state == 32){
+        else if (state == 32) {
             let channel = arrayMessage[3]
             remoteChannel[String(channel)] = String(arrayMessage[5])
             DispatchQueue.main.async {
@@ -558,6 +747,10 @@ class QmeraVideoViewController: UIViewController {
                         self.listRemoteViewFix[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 160)
                         self.listRemoteViewFix[i].backgroundColor = .clear
                         self.listRemoteViewFix[i].makeRoundedView(radius: 8.0)
+                        self.scrollRemoteView.addSubview(self.containerLabelName[i])
+                        self.containerLabelName[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 30)
+                        self.containerLabelName[i].backgroundColor = .orangeBNI.withAlphaComponent(0.5)
+                        self.containerLabelName[i].makeRoundedView(radius: 8.0)
                         if i == 0 {
                             if self.dataPerson[0]["user_type"] == "2" {
                                 self.listRemoteViewFix[0].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.3).rotated(by: (CGFloat.pi)/2)
@@ -572,6 +765,7 @@ class QmeraVideoViewController: UIViewController {
                             }
                         }
                         let pictureImage = self.dataPerson[i]["picture"]!
+                        let namePerson = self.dataPerson[i]["name"]!
                         if (pictureImage != "" && pictureImage != nil) {
                             self.listRemoteViewFix[i].setImage(name: pictureImage!)
                             self.listRemoteViewFix[i].contentMode = .scaleAspectFill
@@ -580,6 +774,12 @@ class QmeraVideoViewController: UIViewController {
                             self.listRemoteViewFix[i].backgroundColor = UIColor.systemGray6
                             self.listRemoteViewFix[i].contentMode = .scaleAspectFit
                         }
+                        let labelName = UILabel()
+                        self.containerLabelName[i].addSubview(labelName)
+                        labelName.anchor(left: self.containerLabelName[i].leftAnchor, right: self.containerLabelName[i].rightAnchor, paddingLeft: 5, paddingRight: 5, centerX: self.containerLabelName[i].centerXAnchor, centerY: self.containerLabelName[i].centerYAnchor)
+                        labelName.text = namePerson
+                        labelName.textAlignment = .center
+                        labelName.textColor = .white
                     }
                     self.scrollRemoteView.contentSize.height = CGFloat(170 * 2)
                 } else if self.dataPerson.count > 1 {
@@ -592,12 +792,17 @@ class QmeraVideoViewController: UIViewController {
                     self.listRemoteViewFix[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 160)
                     self.listRemoteViewFix[i].backgroundColor = .clear
                     self.listRemoteViewFix[i].makeRoundedView(radius: 8.0)
+                    self.scrollRemoteView.addSubview(self.containerLabelName[i])
+                    self.containerLabelName[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 30)
+                    self.containerLabelName[i].backgroundColor = .orangeBNI.withAlphaComponent(0.5)
+                    self.containerLabelName[i].makeRoundedView(radius: 8.0)
                     if arrayMessage[5] == "2" {
                         self.listRemoteViewFix[i].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.3).rotated(by: (CGFloat.pi)/2)
                     } else {
                         self.listRemoteViewFix[i].transform = CGAffineTransform.init(scaleX: 1.4, y: 1.3).rotated(by: (CGFloat.pi * 3 )/2)
                     }
                     let pictureImage = self.dataPerson[self.dataPerson.count - 1]["picture"]!
+                    let namePerson = self.dataPerson[self.dataPerson.count - 1]["name"]!
                     if (pictureImage != "" && pictureImage != nil) {
                         self.listRemoteViewFix[i].setImage(name: pictureImage!)
                         self.listRemoteViewFix[i].contentMode = .scaleAspectFill
@@ -607,6 +812,12 @@ class QmeraVideoViewController: UIViewController {
                         self.listRemoteViewFix[i].contentMode = .scaleAspectFit
                     }
                     self.scrollRemoteView.contentSize.height = CGFloat(170 * (i + 1))
+                    let labelName = UILabel()
+                    self.containerLabelName[i].addSubview(labelName)
+                    labelName.anchor(left: self.containerLabelName[i].leftAnchor, right: self.containerLabelName[i].rightAnchor, paddingLeft: 5, paddingRight: 5, centerX: self.containerLabelName[i].centerXAnchor, centerY: self.containerLabelName[i].centerYAnchor)
+                    labelName.text = namePerson
+                    labelName.textAlignment = .center
+                    labelName.textColor = .white
                 }
             }
             if arrayMessage[5] == "2" && self.dataPerson.count == 1 {
@@ -634,8 +845,6 @@ class QmeraVideoViewController: UIViewController {
             DispatchQueue.main.async {
                 if self.isInisiator && self.name.isDescendant(of: self.view) {
                     self.didTapAcceptCallButton()
-                }
-                if (!self.isSpeaker) {
                     self.setSpeaker(isSpeaker: true)
                 }
                 let indexPerson = self.dataPerson.firstIndex(where: {$0["f_pin"]!! == arrayMessage[1]})
@@ -644,6 +853,47 @@ class QmeraVideoViewController: UIViewController {
                 }
             }
         } else if (state == 38 || state == 28) {
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            if !onGoingCC.isEmpty {
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let officer = onGoingCC.isEmpty ? "" : onGoingCC.components(separatedBy: ",")[1]
+                if arrayMessage[0] == requester || arrayMessage[0] == officer {
+                    DispatchQueue.main.async {
+                        if !self.showNotifCCEnd{
+                            let imageView = UIImageView(image: UIImage(systemName: "info.circle"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Call Center Session has ended".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .info, colors: nil, iconPosition: .center)
+                            banner.show()
+                            self.showNotifCCEnd = true
+                        }
+                        if self.stackViewToolbar.isDescendant(of: self.view){
+                            self.stackViewToolbar.removeFromSuperview()
+                        }
+                        if self.stackViewToolbar2.isDescendant(of: self.view){
+                            self.stackViewToolbar2.removeFromSuperview()
+                        }
+                        if self.buttonWB.isDescendant(of: self.view){
+                            self.buttonWB.removeFromSuperview()
+                        }
+                        if self.buttonDecline.isDescendant(of: self.view) {
+                            self.buttonDecline.removeFromSuperview()
+                        }
+                        if self.buttonAccept.isDescendant(of: self.view) {
+                            self.buttonAccept.removeFromSuperview()
+                        }
+                        if self.wbVC != nil{
+                            self.wbVC!.close?()
+                        }
+                        self.wbTimer.invalidate()
+                        _ = Nexilis.getWhiteboardDelegate()?.terminate()
+                    }
+                    DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
+                        self.endAllCall()
+                        self.dismiss(animated: true, completion: nil)
+                    }
+                    return
+                }
+            }
             DispatchQueue.main.async {
                 if (self.dataPerson.count == 1) {
                     if self.labelIncomingOutgoing.isDescendant(of: self.view) {
@@ -664,9 +914,6 @@ class QmeraVideoViewController: UIViewController {
                     if self.buttonAccept.isDescendant(of: self.view) {
                         self.buttonAccept.removeFromSuperview()
                     }
-                    if self.isSpeaker {
-                        self.setSpeaker(isSpeaker: false)
-                    }
                     if self.wbVC != nil{
                         self.wbVC!.close?()
                     }
@@ -686,45 +933,98 @@ class QmeraVideoViewController: UIViewController {
                         if (self.dataPerson.count == 2) {
                             self.scrollRemoteView.subviews.forEach({ $0.removeFromSuperview() })
                         } else {
-                            self.scrollRemoteView.subviews[indexPerson!].removeFromSuperview()
-                            let viewAfterRemote = self.listRemoteViewFix[indexPerson! + 1]
-                            viewAfterRemote.frame.origin.y = viewAfterRemote.frame.origin.y - 170
-                            UIView.animate(withDuration: 0.35, animations: {
-                                self.scrollRemoteView.layoutIfNeeded()
-                            })
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            if indexPerson! + 1 <= self.listRemoteViewFix.count {
+                                let viewAfterRemote = self.listRemoteViewFix[indexPerson! + 1]
+                                let viewAfterName = self.containerLabelName[indexPerson! + 1]
+                                viewAfterRemote.frame.origin.y = viewAfterRemote.frame.origin.y - 170
+                                viewAfterName.frame.origin.y = viewAfterName.frame.origin.y - 170
+                                UIView.animate(withDuration: 0.35, animations: {
+                                    self.scrollRemoteView.layoutIfNeeded()
+                                })
+                            }
                         }
+                        self.dataPerson.remove(at: indexPerson!)
                     }
-                    self.dataPerson.remove(at: indexPerson!)
+                    if !onGoingCC.isEmpty {
+                        if let pin = arrayMessage.first, let index = self.users.firstIndex(of: User(pin: String(pin))) {
+                            self.users.remove(at: index)
+                        }
+                    }
+                    
                     if self.dataPerson.count == 1 {
+                        self.transformZoomAfterNewUserMore2 = false
                         self.zoomView.transform   = CGAffineTransform.init(scaleX: 1.9, y: 1.9).rotated(by: (CGFloat.pi)/2)
                     }
                 }
             }
         } else if (state == -3) {
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
             DispatchQueue.main.async {
-                if self.labelIncomingOutgoing.isDescendant(of: self.view) {
-                    self.labelIncomingOutgoing.text = "Offline".localized()
-                }
-                if self.buttonDecline.isDescendant(of: self.view) {
-                    self.buttonDecline.removeFromSuperview()
-                }
-                if self.buttonAccept.isDescendant(of: self.view) {
-                    self.buttonAccept.removeFromSuperview()
+                if (self.dataPerson.count == 1) {
+                    if self.labelIncomingOutgoing.isDescendant(of: self.view) {
+                        self.labelIncomingOutgoing.text = "Offline".localized()
+                    }
+                    if self.buttonDecline.isDescendant(of: self.view) {
+                        self.buttonDecline.removeFromSuperview()
+                    }
+                    if self.buttonAccept.isDescendant(of: self.view) {
+                        self.buttonAccept.removeFromSuperview()
+                    }
+                } else {
+                    let indexPerson = self.dataPerson.firstIndex(where: {$0["f_pin"]!! == arrayMessage[0]})
+                    if indexPerson != nil {
+                        if (self.dataPerson.count == 2) {
+                            self.scrollRemoteView.subviews.forEach({ $0.removeFromSuperview() })
+                        } else {
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            if indexPerson! + 1 <= self.listRemoteViewFix.count {
+                                let viewAfterRemote = self.listRemoteViewFix[indexPerson! + 1]
+                                let viewAfterName = self.containerLabelName[indexPerson! + 1]
+                                viewAfterRemote.frame.origin.y = viewAfterRemote.frame.origin.y - 170
+                                viewAfterName.frame.origin.y = viewAfterName.frame.origin.y - 170
+                                UIView.animate(withDuration: 0.35, animations: {
+                                    self.scrollRemoteView.layoutIfNeeded()
+                                })
+                            }
+                        }
+                    }
+                    if !onGoingCC.isEmpty {
+                        if let pin = arrayMessage.first, let index = self.users.firstIndex(of: User(pin: String(pin))) {
+                            self.users.remove(at: index)
+                            if !onGoingCC.isEmpty && self.users.count != 0 {
+                                DispatchQueue.main.async {
+                                    var members = ""
+                                    for user in self.users {
+                                        if members.isEmpty {
+                                            members = "\(user.pin)"
+                                        } else {
+                                            members = ",\(user.pin)"
+                                        }
+                                    }
+                                    UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                                }
+                            }
+                        }
+                    }
+                    self.dataPerson.remove(at: indexPerson!)
                 }
             }
-            if isSpeaker {
-                setSpeaker(isSpeaker: false)
-            }
-            DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
-                self.endAllCall()
-                if self.isInisiator {
-                    self.navigationController?.popViewController(animated: true)
-                } else {
-                    self.dismiss(animated: true, completion: nil)
+            if (self.dataPerson.count == 1) {
+                DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
+                    self.endAllCall()
+                    if self.isInisiator && onGoingCC.isEmpty {
+                        self.navigationController?.popViewController(animated: true)
+                    } else {
+                        self.dismiss(animated: true, completion: nil)
+                    }
                 }
             }
         } else if (state == -4) {
-            DispatchQueue.main.async {
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            DispatchQueue.main.async { [self] in
                 if (self.dataPerson.count == 1) {
                     if self.labelIncomingOutgoing.isDescendant(of: self.view) {
                         self.labelIncomingOutgoing.text = "Busy".localized()
@@ -741,24 +1041,44 @@ class QmeraVideoViewController: UIViewController {
                         if (self.dataPerson.count == 2) {
                             self.scrollRemoteView.subviews.forEach({ $0.removeFromSuperview() })
                         } else {
-                            self.scrollRemoteView.subviews[indexPerson!].removeFromSuperview()
-                            let viewAfterRemote = self.listRemoteViewFix[indexPerson! + 1]
-                            viewAfterRemote.frame.origin.y = viewAfterRemote.frame.origin.y - 170
-                            UIView.animate(withDuration: 0.35, animations: {
-                                self.scrollRemoteView.layoutIfNeeded()
-                            })
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            self.scrollRemoteView.subviews[indexPerson! + indexPerson!].removeFromSuperview()
+                            if indexPerson! + 1 <= self.listRemoteViewFix.count {
+                                let viewAfterRemote = self.listRemoteViewFix[indexPerson! + 1]
+                                let viewAfterName = self.containerLabelName[indexPerson! + 1]
+                                viewAfterRemote.frame.origin.y = viewAfterRemote.frame.origin.y - 170
+                                viewAfterName.frame.origin.y = viewAfterName.frame.origin.y - 170
+                                UIView.animate(withDuration: 0.35, animations: {
+                                    self.scrollRemoteView.layoutIfNeeded()
+                                })
+                            }
+                        }
+                    }
+                    if !onGoingCC.isEmpty {
+                        if let pin = arrayMessage.first, let index = self.users.firstIndex(of: User(pin: String(pin))) {
+                            self.users.remove(at: index)
+                            if !onGoingCC.isEmpty && users.count != 0 {
+                                DispatchQueue.main.async {
+                                    var members = ""
+                                    for user in self.users {
+                                        if members.isEmpty {
+                                            members = "\(user.pin)"
+                                        } else {
+                                            members = ",\(user.pin)"
+                                        }
+                                    }
+                                    UserDefaults.standard.set("\(members)", forKey: "membersCC")
+                                }
+                            }
                         }
                     }
                     self.dataPerson.remove(at: indexPerson!)
                 }
             }
             if (self.dataPerson.count == 1) {
-                if isSpeaker {
-                    setSpeaker(isSpeaker: false)
-                }
                 DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
                     self.endAllCall()
-                    if self.isInisiator {
+                    if self.isInisiator && onGoingCC.isEmpty {
                         self.navigationController?.popViewController(animated: true)
                     } else {
                         self.dismiss(animated: true, completion: nil)
@@ -774,7 +1094,12 @@ class QmeraVideoViewController: UIViewController {
                             self.listRemoteViewFix[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 160)
                             self.listRemoteViewFix[i].backgroundColor = .clear
                             self.listRemoteViewFix[i].makeRoundedView(radius: 8.0)
+                            self.scrollRemoteView.addSubview(self.containerLabelName[i])
+                            self.containerLabelName[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 30)
+                            self.containerLabelName[i].backgroundColor = .orangeBNI.withAlphaComponent(0.5)
+                            self.containerLabelName[i].makeRoundedView(radius: 8.0)
                             let pictureImage = self.dataPerson[i]["picture"]!
+                            let namePerson = self.dataPerson[i]["name"]!
                             if (pictureImage != "" && pictureImage != nil) {
                                 self.listRemoteViewFix[i].setImage(name: pictureImage!)
                                 self.listRemoteViewFix[i].contentMode = .scaleAspectFill
@@ -783,6 +1108,12 @@ class QmeraVideoViewController: UIViewController {
                                 self.listRemoteViewFix[i].backgroundColor = UIColor.systemGray6
                                 self.listRemoteViewFix[i].contentMode = .scaleAspectFit
                             }
+                            let labelName = UILabel()
+                            self.containerLabelName[i].addSubview(labelName)
+                            labelName.anchor(left: self.containerLabelName[i].leftAnchor, right: self.containerLabelName[i].rightAnchor, paddingLeft: 5, paddingRight: 5, centerX: self.containerLabelName[i].centerXAnchor, centerY: self.containerLabelName[i].centerYAnchor)
+                            labelName.text = namePerson
+                            labelName.textAlignment = .center
+                            labelName.textColor = .white
                         }
                         self.scrollRemoteView.contentSize.height = CGFloat(170 * 2)
                     } else {
@@ -791,7 +1122,12 @@ class QmeraVideoViewController: UIViewController {
                         self.listRemoteViewFix[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 160)
                         self.listRemoteViewFix[i].backgroundColor = .clear
                         self.listRemoteViewFix[i].makeRoundedView(radius: 8.0)
+                        self.scrollRemoteView.addSubview(self.containerLabelName[i])
+                        self.containerLabelName[i].frame = CGRect(x: 0, y: 170 * i, width: 120, height: 30)
+                        self.containerLabelName[i].backgroundColor = .orangeBNI.withAlphaComponent(0.5)
+                        self.containerLabelName[i].makeRoundedView(radius: 8.0)
                         let pictureImage = self.dataPerson[self.dataPerson.count - 1]["picture"]!
+                        let namePerson = self.dataPerson[self.dataPerson.count - 1]["name"]!
                         if (pictureImage != "" && pictureImage != nil) {
                             self.listRemoteViewFix[i].setImage(name: pictureImage!)
                             self.listRemoteViewFix[i].contentMode = .scaleAspectFill
@@ -801,6 +1137,12 @@ class QmeraVideoViewController: UIViewController {
                             self.listRemoteViewFix[i].contentMode = .scaleAspectFit
                         }
                         self.scrollRemoteView.contentSize.height = CGFloat(170 * (i + 1))
+                        let labelName = UILabel()
+                        self.containerLabelName[i].addSubview(labelName)
+                        labelName.anchor(left: self.containerLabelName[i].leftAnchor, right: self.containerLabelName[i].rightAnchor, paddingLeft: 5, paddingRight: 5, centerX: self.containerLabelName[i].centerXAnchor, centerY: self.containerLabelName[i].centerYAnchor)
+                        labelName.text = namePerson
+                        labelName.textAlignment = .center
+                        labelName.textColor = .white
                     }
                 }
             }
@@ -836,9 +1178,11 @@ extension QmeraVideoViewController : WhiteboardReceiver {
         DispatchQueue.main.async {
             self.wbBlink = !self.wbBlink
             if(self.wbBlink){
+                print("set wb blink on")
                 self.buttonWB.backgroundColor = .green
             }
             else {
+                print("set wb blink off")
                 self.buttonWB.backgroundColor = .lightGray
             }
             self.buttonWB.setNeedsDisplay()

+ 21 - 21
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/VideoViewController.swift

@@ -322,27 +322,27 @@ class VideoViewController: UIViewController {
     }
     
     func setSpeaker(isSpeaker: Bool) {
-        DispatchQueue.main.async {
-            if (isSpeaker) {
-                self.buttonSpeaker.backgroundColor = UIColor.systemGray4
-                
-            } else {
-                self.buttonSpeaker.backgroundColor = UIColor.systemBlue
-            }
-            self.isSpeaker = isSpeaker
-        }
-        let session = AVAudioSession.sharedInstance()
-        var _: Error?
-        if isSpeaker {
-            try? session.setCategory(AVAudioSession.Category.playAndRecord)
-            try? session.setMode(AVAudioSession.Mode.voiceChat)
-            try? session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
-        } else {
-            try? session.setCategory(.ambient)
-            try? session.setMode(.default)
-            try? session.overrideOutputAudioPort(.none)
-        }
-        try? session.setActive(true)
+//        DispatchQueue.main.async {
+//            if (isSpeaker) {
+//                self.buttonSpeaker.backgroundColor = UIColor.systemGray4
+//                
+//            } else {
+//                self.buttonSpeaker.backgroundColor = UIColor.systemBlue
+//            }
+//            self.isSpeaker = isSpeaker
+//        }
+//        let session = AVAudioSession.sharedInstance()
+//        var _: Error?
+//        if isSpeaker {
+//            try? session.setCategory(AVAudioSession.Category.playAndRecord)
+//            try? session.setMode(AVAudioSession.Mode.voiceChat)
+//            try? session.overrideOutputAudioPort(AVAudioSession.PortOverride.speaker)
+//        } else {
+//            try? session.setCategory(.ambient)
+//            try? session.setMode(.default)
+//            try? session.overrideOutputAudioPort(.none)
+//        }
+//        try? session.setActive(true)
     }
     
     func setMicrophone(isMute: Bool) {

+ 143 - 41
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Call/WhiteboardViewController.swift

@@ -10,6 +10,7 @@ import AVKit
 import AVFoundation
 import QuickLook
 import ReadabilityKit
+import Photos
 
 public class EditorGroup: UIViewController {
     @IBOutlet var viewButton: UIView!
@@ -38,7 +39,7 @@ public class EditorGroup: UIViewController {
     var reffId: String?
     var stickers = [String]()
     public var unique_l_pin = ""
-    var fromNotification = false
+    public var fromNotification = false
     var isHistoryCC = false
     var complaintId = ""
     var counter = 0
@@ -57,14 +58,27 @@ public class EditorGroup: UIViewController {
     let containerPreviewReply = UIView()
     var bottomAnchorPreviewReply = NSLayoutConstraint()
     let containerAction = UIView()
+    var allowTyping = true
     
-    public override func viewWillDisappear(_ animated: Bool) {
+    public override func viewDidDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
             UserDefaults.standard.removeObject(forKey: "inEditorGroup")
             NotificationCenter.default.removeObserver(self)
         }
     }
     
+    deinit {
+        UserDefaults.standard.removeObject(forKey: "inEditorGroup")
+        NotificationCenter.default.removeObserver(self)
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshView"), object: nil, userInfo: nil)
+    }
+    
+    public override func viewDidAppear(_ animated: Bool) {
+        if self.navigationController?.isNavigationBarHidden ?? false {
+            self.navigationController?.setNavigationBarHidden(false, animated: false)
+        }
+    }
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
         
@@ -160,14 +174,14 @@ public class EditorGroup: UIViewController {
                         })
                         NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
                     }
-//                    if self.fromNotification {
-//                        self.didTapExit()
-//                    } else {
-//                        self.navigationController?.popViewController(animated: true)
-//                    }
-                    self.dataMessages.removeAll()
-                    self.dataDates.removeAll()
-                    self.tableChatView.reloadData()
+                    if self.fromNotification {
+                        self.didTapExit()
+                    } else {
+                        self.navigationController?.popViewController(animated: true)
+                    }
+//                    self.dataMessages.removeAll()
+//                    self.dataDates.removeAll()
+//                    self.tableChatView.reloadData()
                 }))
                 self.present(alert, animated: true, completion: nil)
             }),
@@ -229,8 +243,14 @@ public class EditorGroup: UIViewController {
         }
         
         if fromNotification {
-            let backButton = UIBarButtonItem(image: UIImage(systemName: "chevron.backward"), style: .plain, target: self, action: #selector(didTapExit))
-            self.navigationItem.leftBarButtonItem = backButton
+            let imageButton = UIImageView(frame: CGRect(x: 0, y: 0, width: 20, height: 44))
+            imageButton.image = UIImage(systemName: "chevron.backward")?.withTintColor(.white)
+            imageButton.contentMode = .right
+            let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapExit))
+            imageButton.isUserInteractionEnabled = true
+            imageButton.addGestureRecognizer(tapGestureRecognizer)
+            let leftItem = UIBarButtonItem(customView: imageButton)
+            self.navigationItem.leftBarButtonItem = leftItem
         }
         
         changeAppBar()
@@ -314,12 +334,6 @@ public class EditorGroup: UIViewController {
                 dataGroup["parent"] = cursorGroup.string(forColumnIndex: 4)
                 dataTopic["title"] = "Lounge".localized()
                 dataTopic["chat_id"] = ""
-                if dataGroup["official"] as! String == "1" {
-                    if let cursorImage = Database.shared.getRecords(fmdb: fmdb, query: "SELECT image_id FROM GROUPZ where group_type = 1 AND official = 1"), cursorImage.next() {
-                        dataGroup["image_id"] = cursorImage.string(forColumnIndex: 0)
-                        cursorImage.close()
-                    }
-                }
                 cursorGroup.close()
             } else if let cursorTopic = Database.shared.getRecords(fmdb: fmdb, query: "SELECT group_id, title FROM DISCUSSION_FORUM where chat_id = '\(unique_l_pin)'"), cursorTopic.next() {
                 dataGroup["group_id"] = cursorTopic.string(forColumnIndex: 0)
@@ -397,9 +411,9 @@ public class EditorGroup: UIViewController {
                     row["chat_date"] = chatDate(stringDate: row["server_date"] as! String, messageId: row["message_id"] as! String)
                     dataMessages.append(row)
                 }
-                if isHistoryCC {
-                    dataMessages.remove(at: 0)
-                }
+//                if isHistoryCC {
+//                    dataMessages.remove(at: 0)
+//                }
                 cursorData.close()
             }
         })
@@ -749,6 +763,12 @@ public class EditorGroup: UIViewController {
                     labelKicked.text = "You have left this group".localized()
                 } else if data["member"] != nil {
                     labelKicked.text = "You have been removed from this group".localized()
+                } else if data["code"] as! String == CoreMessage_TMessageCode.UPDATE_CHAT {
+                    dataGroup.removeAll()
+                    dataTopic.removeAll()
+                    getDataGroup(unique_l_pin: unique_l_pin)
+                    changeAppBar()
+                    return
                 } else {
                     return
                 }
@@ -911,6 +931,9 @@ public class EditorGroup: UIViewController {
     }
     
     @objc func profilePersonTapped(_ sender: ObjectGesture) {
+        if isHistoryCC {
+            return
+        }
         let idMe = UserDefaults.standard.string(forKey: "me") as String?
         if sender.message_id == idMe {
             let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "profileView") as! ProfileViewController
@@ -941,7 +964,7 @@ public class EditorGroup: UIViewController {
     }
     
     @objc func seeProfileTapped() {
-        if removed || copySession || forwardSession || deleteSession || (dataGroup["official"] as? String == "1" && (dataGroup["parent"] as? String)!.isEmpty) {
+        if isHistoryCC || removed || copySession || forwardSession || deleteSession || (dataGroup["official"] as? String == "1" && (dataGroup["parent"] as? String)!.isEmpty) {
             return
         }
         dismissKeyboard()
@@ -999,14 +1022,16 @@ public class EditorGroup: UIViewController {
             
             let duration: CGFloat = info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber as! CGFloat
             
-            self.constraintViewTextField.constant = keyboardHeight - 60
-            UIView.animate(withDuration: TimeInterval(duration), animations: {
-                self.view.layoutIfNeeded()
-            })
-            if (self.currentIndexpath != nil) {
-                self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
-            } else {
-                self.tableChatView.scrollToBottom()
+            if self.constraintViewTextField.constant != keyboardHeight - 60 {
+                self.constraintViewTextField.constant = keyboardHeight - 60
+                UIView.animate(withDuration: TimeInterval(duration), animations: {
+                    self.view.layoutIfNeeded()
+                })
+                if (self.currentIndexpath != nil) {
+                    self.tableChatView.scrollToRow(at: IndexPath(row: self.currentIndexpath!.row, section: self.currentIndexpath!.section), at: .none, animated: false)
+                } else {
+                    self.tableChatView.scrollToBottom()
+                }
             }
         }
     }
@@ -1047,7 +1072,7 @@ public class EditorGroup: UIViewController {
         }
         let message_text = message_text.trimmingCharacters(in: .whitespacesAndNewlines)
         let idMe = UserDefaults.standard.string(forKey: "me") as String?
-        let message = CoreMessage_TMessageBank.sendMessage(l_pin: dataGroup["group_id"] as! String, message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: dataTopic["chat_id"] as! String, is_call_center: is_call_center, call_center_id: call_center_id)
+        let message = CoreMessage_TMessageBank.sendMessage(l_pin: dataGroup["group_id"] as! String, message_scope_id: message_scope_id, status: status, message_text: message_text, credential: credential, attachment_flag: attachment_flag, ex_blog_id: ex_blog_id, message_large_text: message_large_text, ex_format: ex_format, image_id: image_id, audio_id: audio_id, video_id: video_id, file_id: file_id, thumb_id: thumb_id, reff_id: reff_id, read_receipts: read_receipts, chat_id: dataTopic["chat_id"] as! String, is_call_center: is_call_center, call_center_id: call_center_id, opposite_pin: "")
         Nexilis.addQueueMessage(message: message)
         let messageId = String(message.mBodies[CoreMessage_TMessageKey.MESSAGE_ID]!)
         var row: [String: Any?] = [:]
@@ -1083,6 +1108,7 @@ public class EditorGroup: UIViewController {
             textFieldSend.textColor = UIColor.lightGray
         } else if constraintViewTextField.constant != 0 {
             textFieldSend.text = ""
+            heightTextFieldSend.constant = 40
         }
         deleteReplyView()
         deleteLinkPreview()
@@ -1233,12 +1259,58 @@ public class EditorGroup: UIViewController {
         }
         if let index = dataMessages.firstIndex(where: {$0["message_id"] as? String == message_id}) {
             dataMessages[index]["status"] = "4"
+            let auto = UserDefaults.standard.bool(forKey: "autoDownload")
+            if auto {
+                if dataMessages[index]["image_id"] as? String != nil && !((dataMessages[index]["image_id"] as? String)!.isEmpty) {
+                    Download().start(forKey:dataMessages[index]["image_id"] as! String) { (name, progress) in
+                        guard progress == 100 else {
+                            return
+                        }
+                        let nsDocumentDirectory = FileManager.SearchPathDirectory.documentDirectory
+                        let nsUserDomainMask = FileManager.SearchPathDomainMask.userDomainMask
+                        let paths = NSSearchPathForDirectoriesInDomains(nsDocumentDirectory, nsUserDomainMask, true)
+                        if let dirPath = paths.first {
+                            let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(self.dataMessages[index]["image_id"] as! String)
+                            let image    = UIImage(contentsOfFile: imageURL.path)
+                            let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+                            if save {
+                                UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
+                            }
+                        }
+                        DispatchQueue.main.async { [self] in
+                            let section = self.dataDates.firstIndex(of: dataMessages[index]["chat_date"] as! String)
+                            let row = self.dataMessages.filter({$0["chat_date"] as! String == dataMessages[index]["chat_date"] as! String}).firstIndex(where: { $0["message_id"] as? String == message_id})
+                            if row != nil && section != nil{
+                                tableChatView.reloadRows(at: [IndexPath(row: row!, section: section!)], with: .automatic)
+                            }
+                        }
+                    }
+                }  else if dataMessages[index]["video_id"] as? String != nil && !((dataMessages[index]["video_id"] as? String)!.isEmpty){
+                    let section = dataDates.firstIndex(of: dataMessages[index]["chat_date"] as! String)
+                    let row = dataMessages.filter({$0["chat_date"] as! String == dataMessages[index]["chat_date"] as! String}).firstIndex(where: { $0["message_id"] as? String == message_id})
+                    if row != nil && section != nil{
+                        let indexPath = IndexPath(row: row!, section: section!)
+                        if let cell = tableChatView.cellForRow(at: indexPath) {
+                            for view in cell.contentView.subviews {
+                                if view is UIImageView {
+                                    let objectTap = ObjectGesture()
+                                    objectTap.video_id = dataMessages[index]["video_id"] as! String
+                                    objectTap.imageView = view as! UIImageView
+                                    objectTap.indexPath = indexPath
+                                    contentMessageTapped(objectTap)
+                                    break
+                                }
+                            }
+                        }
+                    }
+                }
+            }
         }
     }
     
-    private func sendTyping(l_pin: String) {
+    private func sendTyping(l_pin: String, isTyping: Bool = false) {
         DispatchQueue.global().async {
-            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: "4", p_status: "1")
+            let tmessage = CoreMessage_TMessageBank.getUpdateTypingStatus(p_opposite: l_pin, p_scope: "4", p_status: isTyping ? "3": "4")
             _ = Nexilis.write(message: tmessage)
         }
     }
@@ -1310,7 +1382,7 @@ extension EditorGroup: ImageVideoPickerDelegate, PreviewAttachmentImageVideoDele
             if (textFieldSend.textColor != .lightGray) {
                 previewImageVC.currentTextTextField = textFieldSend.text
             }
-            previewImageVC.modalPresentationStyle = .fullScreen
+            previewImageVC.modalPresentationStyle = .custom
             previewImageVC.delegate = self
             self.present(previewImageVC, animated: true, completion: nil)
         }
@@ -1375,6 +1447,19 @@ extension EditorGroup: UIDocumentPickerDelegate, DocumentPickerDelegate, QLPrevi
 
 extension EditorGroup: UITextViewDelegate {
     public func textViewDidChange(_ textView: UITextView) {
+        if allowTyping {
+            allowTyping = false
+            if dataTopic["chat_id"] as! String == "" {
+                UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [dataGroup["group_id"] as! String])
+                sendTyping(l_pin: dataGroup["group_id"] as! String)
+            } else {
+                UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: [dataTopic["chat_id"] as! String])
+                sendTyping(l_pin: dataTopic["chat_id"] as! String)
+            }
+            DispatchQueue.main.asyncAfter(deadline: .now() + 4, execute: {
+                self.allowTyping = true
+            })
+        }
         if let selectedRange = textView.selectedTextRange {
             let cursorPosition = textView.offset(from: textView.beginningOfDocument, to: selectedRange.start)
             let beforeCursor = textView.text.substring(from: cursorPosition - cursorPosition, to: cursorPosition - 1).split(separator: " ").last
@@ -1601,7 +1686,7 @@ extension EditorGroup: UITextViewDelegate {
             previewImageVC.image = UIPasteboard.general.image
             previewImageVC.fromCopy = true
             previewImageVC.currentTextTextField = textFieldSend.text
-            previewImageVC.modalPresentationStyle = .fullScreen
+            previewImageVC.modalPresentationStyle = .custom
             previewImageVC.delegate = self
             self.present(previewImageVC, animated: true, completion: nil)
         }
@@ -2035,7 +2120,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 let dataProfile = getDataProfile(f_pin: dataMessages[i]["f_pin"] as! String, message_id: dataMessages[i]["message_id"] as! String)
                 text = text + "\n\n*[\(formatterDate.string(from: date as Date)) \(formatterTime.string(from: date as Date))] \(dataProfile["name"]!):*\n\(dataMessages[i]["message_text"] as! String)"
             }
-            text = text + "\n\n\nchat powered by Qmera"
+            text = text + "\n\n\nchat powered by Nexilis"
             DispatchQueue.main.async {
                 UIPasteboard.general.string = text
                 self.showToast(message: "Text coppied to clipboard".localized(), font: UIFont.systemFont(ofSize: 12, weight: .medium), controller: self)
@@ -2048,7 +2133,7 @@ extension EditorGroup: UIContextMenuInteractionDelegate {
                 return
             }
             let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
-            navigationController.modalPresentationStyle = .fullScreen
+            navigationController.modalPresentationStyle = .custom
             if let controller = navigationController.viewControllers.first as? ContactChatViewController {
                 controller.isChooser = { [weak self] scope, pin in
                     if scope == "3" {
@@ -2397,7 +2482,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     streamingController.data = json
                 }
                 let streamingNav = UINavigationController(rootViewController: streamingController)
-                streamingNav.modalPresentationStyle = .fullScreen
+                streamingNav.modalPresentationStyle = .custom
                 streamingNav.navigationBar.barTintColor = .mainColor
                 streamingNav.navigationBar.tintColor = .white
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
@@ -2428,7 +2513,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
         profileMessage.addGestureRecognizer(tapGestureRecognizer)
         
         let containerMessage = UIView()
-        if !copySession && !forwardSession && !deleteSession {
+        if !copySession && !forwardSession && !deleteSession && !isHistoryCC {
             let interaction = UIContextMenuInteraction(delegate: self)
             containerMessage.addInteraction(interaction)
             containerMessage.isUserInteractionEnabled = true
@@ -3187,7 +3272,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
                     previewImageVC.image = image
                     previewImageVC.isHiddenTextField = true
-                    previewImageVC.modalPresentationStyle = .fullScreen
+                    previewImageVC.modalPresentationStyle = .custom
                     previewImageVC.modalTransitionStyle  = .crossDissolve
                     self.present(previewImageVC, animated: true, completion: nil)
                 } else {
@@ -3208,6 +3293,13 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                             return
                         }
                         
+                        let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.image_id)
+                        let image    = UIImage(contentsOfFile: imageURL.path)
+                        let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+                        if save {
+                            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
+                        }
+                        
                         DispatchQueue.main.async {
                             activityIndicator.stopAnimating()
                             self.tableChatView.reloadRows(at: [sender.indexPath], with: .none)
@@ -3221,6 +3313,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                 if FileManager.default.fileExists(atPath: videoURL.path) {
                     let player = AVPlayer(url: videoURL as URL)
                     let playerVC = AVPlayerViewController()
+                    playerVC.modalPresentationStyle = .custom
                     playerVC.player = player
                     self.present(playerVC, animated: true, completion: nil)
                 } else {
@@ -3265,6 +3358,15 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                                 shapeLoading.strokeEnd = CGFloat(progress / 100)
                                 return
                             }
+                            let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+                            if save {
+                                let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.video_id)
+                                PHPhotoLibrary.shared().performChanges({
+                                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
+                                }) { saved, error in
+                                    
+                                }
+                            }
                             let idx = self.dataMessages.firstIndex(where: { $0["video_id"] as! String == sender.video_id})
                             if idx != nil {
                                 self.dataMessages[idx!]["progress"] = progress
@@ -3283,7 +3385,7 @@ extension EditorGroup: UITableViewDelegate, UITableViewDataSource {
                     let rightBarButton = UIBarButtonItem()
                     previewController.navigationItem.rightBarButtonItem = rightBarButton
                     previewController.dataSource = self
-                    previewController.modalPresentationStyle = .fullScreen
+                    previewController.modalPresentationStyle = .custom
                     
                     self.show(previewController, sender: nil)
                 } else {

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 786 - 123
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorPersonal.swift


+ 24 - 6
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/EditorStarMessages.swift

@@ -9,6 +9,7 @@ import UIKit
 import AVKit
 import AVFoundation
 import QuickLook
+import Photos
 
 public class EditorStarMessages: UIViewController, UITableViewDataSource, UITableViewDelegate, UIContextMenuInteractionDelegate, QLPreviewControllerDataSource {
     @IBOutlet var tableChatView: UITableView!
@@ -722,7 +723,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                     let previewImageVC = PreviewAttachmentImageVideo(nibName: "PreviewAttachmentImageVideo", bundle: Bundle.resourceBundle(for: Nexilis.self))
                     previewImageVC.image = image
                     previewImageVC.isHiddenTextField = true
-                    previewImageVC.modalPresentationStyle = .fullScreen
+                    previewImageVC.modalPresentationStyle = .custom
                     previewImageVC.modalTransitionStyle  = .crossDissolve
                     self.present(previewImageVC, animated: true, completion: nil)
                 } else {
@@ -743,6 +744,13 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                             return
                         }
                         
+                        let imageURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.image_id)
+                        let image    = UIImage(contentsOfFile: imageURL.path)
+                        let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+                        if save {
+                            UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil)
+                        }
+                        
                         DispatchQueue.main.async {
                             activityIndicator.stopAnimating()
                             self.tableChatView.reloadData()
@@ -756,6 +764,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                 if FileManager.default.fileExists(atPath: videoURL.path) {
                     let player = AVPlayer(url: videoURL as URL)
                     let playerVC = AVPlayerViewController()
+                    playerVC.modalPresentationStyle = .custom
                     playerVC.player = player
                     self.present(playerVC, animated: true, completion: nil)
                 } else {
@@ -800,6 +809,15 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                                 shapeLoading.strokeEnd = CGFloat(progress / 100)
                                 return
                             }
+                            let save = UserDefaults.standard.bool(forKey: "saveToGallery")
+                            if save {
+                                let videoURL = URL(fileURLWithPath: dirPath).appendingPathComponent(sender.video_id)
+                                PHPhotoLibrary.shared().performChanges({
+                                    PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: videoURL)
+                                }) { saved, error in
+                                    
+                                }
+                            }
                             let idx = self.dataMessages.firstIndex(where: { $0["video_id"] as! String == sender.video_id})
                             if idx != nil {
                                 self.dataMessages[idx!]["progress"] = progress
@@ -818,7 +836,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                     let rightBarButton = UIBarButtonItem()
                     previewController.navigationItem.rightBarButtonItem = rightBarButton
                     previewController.dataSource = self
-                    previewController.modalPresentationStyle = .fullScreen
+                    previewController.modalPresentationStyle = .custom
                     
                     self.show(previewController, sender: nil)
                 } else {
@@ -926,7 +944,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
             }
             else if f_pin == "-999" {
                 data["name"] = "Bot".localized()
-                data["image_id"] = "pb_ball"
+                data["image_id"] = "pb_powered"
             }
             else {
                 data["name"] = "Unknown".localized()
@@ -974,7 +992,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
         })
         let forward = UIAction(title: "Forward".localized(), image: UIImage(systemName: "arrowshape.turn.up.right.fill"), handler: {(_) in
             let navigationController = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "contactChatNav") as! UINavigationController
-            navigationController.modalPresentationStyle = .fullScreen
+            navigationController.modalPresentationStyle = .custom
             if let controller = navigationController.viewControllers.first as? ContactChatViewController {
                 controller.isChooser = { [weak self] scope, pin in
                     if scope == "3" {
@@ -1010,7 +1028,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                     } else {
                         text = text + "\n\n*[\(formatterDate.string(from: date as Date)) \(formatterTime.string(from: date as Date))] \(dataProfile["name"]!):*\n\(dataMessages[indexPath!.row]["message_text"] as! String)"
                     }
-                    text = text + "\n\n\nchat powered by Qmera"
+                    text = text + "\n\n\nchat powered by Nexilis"
                     DispatchQueue.main.async {
                         UIPasteboard.general.string = text
                         self.showToast(message: "Text coppied to clipboard".localized(), font: UIFont.systemFont(ofSize: 12, weight: .medium), controller: self)
@@ -1124,7 +1142,7 @@ public class EditorStarMessages: UIViewController, UITableViewDataSource, UITabl
                     streamingController.data = json
                 }
                 let streamingNav = UINavigationController(rootViewController: streamingController)
-                streamingNav.modalPresentationStyle = .fullScreen
+                streamingNav.modalPresentationStyle = .custom
                 streamingNav.navigationBar.barTintColor = .mainColor
                 streamingNav.navigationBar.tintColor = .white
                 let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]

+ 338 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/FormEditor.swift

@@ -0,0 +1,338 @@
+//
+//  FormEditor.swift
+//  NexilisLite
+//
+//  Created by Qindi on 20/05/22.
+//
+
+import UIKit
+import NotificationBannerSwift
+import nuSDKService
+
+class FormEditor: UIViewController {
+    
+    var jsonData = ""
+    var dataMessage: [String: Any?] = [:]
+    var dataPerson: [String: String?] = [:]
+    var isAorR: Int?
+    var dateApproved = ""
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+
+        let containerView = UIView()
+        self.view.addSubview(containerView)
+        containerView.backgroundColor = .white
+        containerView.anchor(top: self.view.topAnchor, left: self.view.leftAnchor, bottom: self.view.bottomAnchor, right: self.view.rightAnchor, paddingTop: 50, paddingLeft: 40, paddingBottom: 100, paddingRight: 40)
+        
+        let viewTitle = UIView()
+        viewTitle.backgroundColor = .mainColor
+        containerView.addSubview(viewTitle)
+        viewTitle.anchor(top: containerView.topAnchor, left: containerView.leftAnchor, right: containerView.rightAnchor, height: 40)
+        
+        if let json = try! JSONSerialization.jsonObject(with: jsonData.data(using: String.Encoding.utf8)!, options: []) as? [String: Any] {
+            let title = UILabel()
+            title.text = (json["form_title"] as! String).replacingOccurrences(of: "+", with: " ")
+            title.font = .systemFont(ofSize: 15.0, weight: .bold)
+            title.textColor = .white
+            viewTitle.addSubview(title)
+            title.anchor(centerX: viewTitle.centerXAnchor, centerY: viewTitle.centerYAnchor)
+            
+            if checkForm(json: json){
+                buildRorAView(json: json, viewTitle: viewTitle, containerView: containerView)
+            } else {
+                buildWaitingApprovalView(json: json, viewTitle: viewTitle, containerView: containerView)
+            }
+            
+            let buttonClose = UIButton()
+            buttonClose.frame.size = CGSize(width: 50, height: 50)
+            buttonClose.layer.cornerRadius = 25
+            buttonClose.clipsToBounds = true
+            buttonClose.setBackgroundImage(UIImage(systemName: "xmark.circle"), for: .normal)
+            buttonClose.tintColor = .mainColor
+            buttonClose.actionHandle(controlEvents: .touchUpInside,
+             ForAction:{() -> Void in
+                self.dismiss(animated: true, completion: nil)
+             })
+            self.view.addSubview(buttonClose)
+            buttonClose.anchor(top: containerView.bottomAnchor, centerX: self.view.centerXAnchor, width: 50, height: 50)
+        }
+    }
+    
+    private func checkForm(json: [String: Any]) -> Bool {
+        var result = false
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            if let dataForm = Database().getRecords(fmdb: fmdb, query: "select sq_no, created_date from FORM where form_id = '\(dataMessage["reff_id"] as! String)'"), dataForm.next() {
+                result = true
+                isAorR = Int(dataForm.int(forColumnIndex: 0))
+                dateApproved = dataForm.string(forColumnIndex: 1)!
+            }
+        })
+        return result
+    }
+    
+    private func buildRorAView(json: [String: Any], viewTitle: UIView, containerView: UIView) {
+        if isAorR != nil {
+            let viewStatus = UIView()
+            containerView.addSubview(viewStatus)
+            viewStatus.anchor(top: viewTitle.bottomAnchor, left: containerView.leftAnchor, right: containerView.rightAnchor, height: 20)
+            
+            let textStatus = UILabel()
+            viewStatus.addSubview(textStatus)
+            textStatus.anchor(centerX: viewStatus.centerXAnchor, centerY: viewStatus.centerYAnchor)
+            textStatus.font = .systemFont(ofSize: 14, weight: .bold)
+            textStatus.textColor = .white
+            
+            let imageProvince = UIImageView()
+            imageProvince.image = UIImage(systemName: "doc.text.image")
+            imageProvince.tintColor = .mainColor
+            imageProvince.contentMode = .scaleToFill
+            containerView.addSubview(imageProvince)
+            imageProvince.anchor(top: viewStatus.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+            
+            let province = UILabel()
+            province.text = "Province".localized()
+            province.font = .systemFont(ofSize: 14.0, weight: .bold)
+            province.textColor = .gray
+            containerView.addSubview(province)
+            province.anchor(top: viewStatus.bottomAnchor, left: imageProvince.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+            
+            let provinceFill = UILabel()
+            provinceFill.text = json["province"] as? String
+            provinceFill.font = .systemFont(ofSize: 14.0)
+            provinceFill.textColor = .black
+            containerView.addSubview(provinceFill)
+            provinceFill.anchor(top: province.bottomAnchor, left:  imageProvince.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+            
+            let imageClub = UIImageView()
+            imageClub.image = UIImage(systemName: "doc.text.image")
+            imageClub.tintColor = .mainColor
+            imageClub.contentMode = .scaleToFill
+            containerView.addSubview(imageClub)
+            imageClub.anchor(top: provinceFill.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+            
+            let club = UILabel()
+            club.text = "Club".localized()
+            club.font = .systemFont(ofSize: 14.0, weight: .bold)
+            club.textColor = .gray
+            containerView.addSubview(club)
+            club.anchor(top: provinceFill.bottomAnchor, left: imageClub.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+            
+            let clubFill = UILabel()
+            clubFill.text = json["club"] as? String
+            clubFill.font = .systemFont(ofSize: 14.0)
+            clubFill.textColor = .black
+            containerView.addSubview(clubFill)
+            clubFill.anchor(top: club.bottomAnchor, left: imageClub.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+            
+            let imageAorR = UIImageView()
+            imageAorR.image = UIImage(systemName: "person.fill")
+            imageAorR.tintColor = .mainColor
+            imageAorR.contentMode = .scaleToFill
+            containerView.addSubview(imageAorR)
+            imageAorR.anchor(top: clubFill.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+            
+            let descAorR = UILabel()
+            descAorR.font = .systemFont(ofSize: 14.0, weight: .bold)
+            descAorR.textColor = .gray
+            containerView.addSubview(descAorR)
+            descAorR.anchor(top: clubFill.bottomAnchor, left: imageAorR.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+            
+            let name = UILabel()
+            name.text = User.getData(pin: UserDefaults.standard.string(forKey: "me"))?.fullName
+            name.font = .systemFont(ofSize: 14.0)
+            name.textColor = .black
+            containerView.addSubview(name)
+            name.anchor(top: descAorR.bottomAnchor, left: imageAorR.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+            
+            let imageDateAorR = UIImageView()
+            imageDateAorR.image = UIImage(systemName: "person.fill")
+            imageDateAorR.tintColor = .mainColor
+            imageDateAorR.contentMode = .scaleToFill
+            containerView.addSubview(imageDateAorR)
+            imageDateAorR.anchor(top: name.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+            
+            let dateAorR = UILabel()
+            dateAorR.text = "Time".localized()
+            dateAorR.font = .systemFont(ofSize: 14.0, weight: .bold)
+            dateAorR.textColor = .gray
+            containerView.addSubview(dateAorR)
+            dateAorR.anchor(top: name.bottomAnchor, left: imageAorR.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+            
+            let dateLabel = UILabel()
+            let stringDate = dateApproved.isEmpty ? "\(Date().currentTimeMillis())" : dateApproved
+            let date = Date(milliseconds: Int64(stringDate)!)
+            let formatter = DateFormatter()
+            formatter.dateFormat = "dd MMM yyyy, HH:mm"
+            formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+            dateLabel.text = formatter.string(from: date as Date)
+            dateLabel.font = .systemFont(ofSize: 14.0)
+            dateLabel.textColor = .black
+            containerView.addSubview(dateLabel)
+            dateLabel.anchor(top: dateAorR.bottomAnchor, left: imageAorR.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+            
+            if isAorR == 0 {
+                viewStatus.backgroundColor = .systemRed
+                textStatus.text = "Rejected".localized()
+                descAorR.text = "Rejected by".localized()
+            } else {
+                viewStatus.backgroundColor = .systemGreen
+                textStatus.text = "Approved".localized()
+                descAorR.text = "Approved by".localized()
+            }
+        }
+    }
+    
+    private func buildWaitingApprovalView(json: [String: Any], viewTitle: UIView, containerView: UIView) {
+        let imageRequested = UIImageView()
+        imageRequested.image = UIImage(systemName: "person.fill")
+        imageRequested.tintColor = .mainColor
+        imageRequested.contentMode = .scaleToFill
+        containerView.addSubview(imageRequested)
+        imageRequested.anchor(top: viewTitle.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+        
+        let requestedBy = UILabel()
+        requestedBy.text = "Requested by".localized()
+        requestedBy.font = .systemFont(ofSize: 14.0, weight: .bold)
+        requestedBy.textColor = .gray
+        containerView.addSubview(requestedBy)
+        requestedBy.anchor(top: viewTitle.bottomAnchor, left: imageRequested.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+        
+        let name = UILabel()
+        name.text = dataPerson["name"] as? String
+        name.font = .systemFont(ofSize: 14.0)
+        name.textColor = .black
+        containerView.addSubview(name)
+        name.anchor(top: requestedBy.bottomAnchor, left: imageRequested.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+        
+        let imageClubType = UIImageView()
+        imageClubType.image = UIImage(systemName: "doc.text.image")
+        imageClubType.tintColor = .mainColor
+        imageClubType.contentMode = .scaleToFill
+        containerView.addSubview(imageClubType)
+        imageClubType.anchor(top: name.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+        
+        let clubType = UILabel()
+        clubType.text = "Club Type".localized()
+        clubType.font = .systemFont(ofSize: 14.0, weight: .bold)
+        clubType.textColor = .gray
+        containerView.addSubview(clubType)
+        clubType.anchor(top: name.bottomAnchor, left: imageClubType.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+        
+        let clubTypeFill = UILabel()
+        clubTypeFill.text = json["club_type"] as? String
+        clubTypeFill.font = .systemFont(ofSize: 14.0)
+        clubTypeFill.textColor = .black
+        containerView.addSubview(clubTypeFill)
+        clubTypeFill.anchor(top: clubType.bottomAnchor, left: imageClubType.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+        
+        let imageProvince = UIImageView()
+        imageProvince.image = UIImage(systemName: "doc.text.image")
+        imageProvince.tintColor = .mainColor
+        imageProvince.contentMode = .scaleToFill
+        containerView.addSubview(imageProvince)
+        imageProvince.anchor(top: clubTypeFill.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+        
+        let province = UILabel()
+        province.text = "Province".localized()
+        province.font = .systemFont(ofSize: 14.0, weight: .bold)
+        province.textColor = .gray
+        containerView.addSubview(province)
+        province.anchor(top: clubTypeFill.bottomAnchor, left: imageProvince.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+        
+        let provinceFill = UILabel()
+        provinceFill.text = json["province"] as? String
+        provinceFill.font = .systemFont(ofSize: 14.0)
+        provinceFill.textColor = .black
+        containerView.addSubview(provinceFill)
+        provinceFill.anchor(top: province.bottomAnchor, left:  imageProvince.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+        
+        let imageClub = UIImageView()
+        imageClub.image = UIImage(systemName: "doc.text.image")
+        imageClub.tintColor = .mainColor
+        imageClub.contentMode = .scaleToFill
+        containerView.addSubview(imageClub)
+        imageClub.anchor(top: provinceFill.bottomAnchor, left: containerView.leftAnchor, paddingTop: 15, paddingLeft: 10, width: 20, height: 20)
+        
+        let club = UILabel()
+        club.text = "Club".localized()
+        club.font = .systemFont(ofSize: 14.0, weight: .bold)
+        club.textColor = .gray
+        containerView.addSubview(club)
+        club.anchor(top: provinceFill.bottomAnchor, left: imageClub.rightAnchor, right: containerView.rightAnchor, paddingTop: 10, paddingLeft: 10)
+        
+        let clubFill = UILabel()
+        clubFill.text = json["club"] as? String
+        clubFill.font = .systemFont(ofSize: 14.0)
+        clubFill.textColor = .black
+        containerView.addSubview(clubFill)
+        clubFill.anchor(top: club.bottomAnchor, left: imageClub.rightAnchor, right: containerView.rightAnchor, paddingLeft: 10)
+        
+        let buttonReject = UIButton()
+        buttonReject.backgroundColor = .systemRed
+        buttonReject.setImage(UIImage(systemName: "xmark"), for: .normal)
+        buttonReject.tintColor = .white
+        buttonReject.setTitle("Reject".localized(), for: .normal)
+        buttonReject.titleLabel?.font = .boldSystemFont(ofSize: 15)
+        buttonReject.layer.cornerRadius = 8
+        containerView.addSubview(buttonReject)
+        buttonReject.anchor(left: containerView.leftAnchor, bottom: containerView.bottomAnchor, paddingLeft: 10, paddingBottom: 10, width: (self.view.bounds.width / 2) - 55, height: 50)
+        buttonReject.tag = 0
+        buttonReject.addTarget(self, action: #selector(rejectApproveForm(_:)), for: .touchUpInside)
+        
+        let buttonAccept = UIButton()
+        buttonAccept.backgroundColor = .systemGreen
+        buttonAccept.setImage(UIImage(systemName: "checkmark"), for: .normal)
+        buttonAccept.tintColor = .white
+        buttonAccept.setTitle("Approve".localized(), for: .normal)
+        buttonAccept.titleLabel?.font = .boldSystemFont(ofSize: 15)
+        buttonAccept.layer.cornerRadius = 8
+        containerView.addSubview(buttonAccept)
+        buttonAccept.anchor(bottom: containerView.bottomAnchor, right: containerView.rightAnchor, paddingBottom: 10, paddingRight: 10, width: (self.view.bounds.width / 2) - 55, height: 50)
+        buttonAccept.tag = 1
+        buttonAccept.addTarget(self, action: #selector(rejectApproveForm(_:)), for: .touchUpInside)
+    }
+    
+    @objc func rejectApproveForm(_ sender: UIButton) {
+        let refId = dataMessage["reff_id"] as? String
+        let isApprove = sender.tag
+        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            return
+        }
+        DispatchQueue.global().async { [self] in
+            let idMe = UserDefaults.standard.string(forKey: "me")!
+            let resp = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getFormApproval(p_f_pin: idMe, p_ref_id: refId ?? "", p_approve: "\(isApprove)", p_note: "", p_sign: ""))
+            if resp != nil {
+                Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                    do {
+                        _ = try Database.shared.insertRecord(fmdb: fmdb, table: "FORM", cvalues: [
+                            "form_id" : refId ?? "",
+                            "name" : "",
+                            "created_date" : "\(Date().currentTimeMillis())",
+                            "created_by" : dataPerson["f_pin"]!!,
+                            "sq_no" : isApprove,
+                        ], replace: true)
+                    } catch {
+                        rollback.pointee = true
+                        print(error)
+                    }
+                })
+                DispatchQueue.main.async {
+                    self.view.subviews.forEach({ $0.removeFromSuperview() })
+                    self.viewDidLoad()
+                }
+            } else {
+                DispatchQueue.main.async {
+                    let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                    imageView.tintColor = .white
+                    let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                    banner.show()
+                }
+            }
+        }
+    }
+}

+ 1 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.swift

@@ -137,6 +137,7 @@ class PreviewAttachmentImageVideo: UIViewController, UIScrollViewDelegate, UITex
         let player = AVPlayer(url: sender.videoURL! as URL)
         let playerVC = AVPlayerViewController()
         playerVC.player = player
+        playerVC.modalPresentationStyle = .custom
         self.present(playerVC, animated: true, completion: nil)
     }
     

+ 1 - 1
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Chat/PreviewAttachmentImageVideo.xib

@@ -65,7 +65,7 @@
                         <constraint firstAttribute="width" constant="40" id="axp-R5-jBp"/>
                         <constraint firstAttribute="height" constant="40" id="xsU-84-Xow"/>
                     </constraints>
-                    <color key="tintColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
+                    <color key="tintColor" red="0.41176470590000003" green="0.27058823529999998" blue="0.64705882349999999" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
                     <state key="normal">
                         <imageReference key="image" image="xmark" catalog="system" symbolScale="large"/>
                     </state>

+ 48 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Contact/ContactCallViewController.swift

@@ -34,11 +34,22 @@ class ContactCallViewController: UIViewController {
         }
     }
     
+    override func viewDidAppear(_ animated: Bool) {
+        print(#function, ">>>> DIDAPPEAR CONTACT CALL")
+        NotificationCenter.default.removeObserver(self)
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshView"), object: nil, userInfo: nil)
+    }
+    
     override func viewDidLoad() {
         super.viewDidLoad()
         title = "Start Call"
+        let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
+        self.navigationController?.navigationBar.titleTextAttributes = attributes
         if (isAddParticipantVideo) {
-            title = "Add Participant"
+            title = "Friends".localized()
+            
+            let buttonAddFriend = UIBarButtonItem(image: UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default))?.withTintColor(.white), style: .plain, target: self, action: #selector(addFriend(sender:)))
+            navigationItem.rightBarButtonItem = buttonAddFriend
         } else {
             let menu = UIMenu(title: "", children: [
                 UIAction(title: "Create Group", image: UIImage(systemName: "person.and.person"), handler: {(_) in
@@ -52,7 +63,12 @@ class ContactCallViewController: UIViewController {
                     self.navigationController?.present(controller, animated: true, completion: nil)
                 }),
                 UIAction(title: "Add Friends", image: UIImage(systemName: "person.badge.plus"), handler: {(_) in
-                    let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav")
+                    let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
+                    if let vc = controller.viewControllers.first as? AddFriendTableViewController {
+                        vc.isDismiss = {
+                            self.getData()
+                        }
+                    }
                     self.navigationController?.present(controller, animated: true, completion: nil)
                 }),
 //                UIAction(title: "Configure Email", image: UIImage(systemName: "mail"), handler: {(_) in }),
@@ -88,6 +104,29 @@ class ContactCallViewController: UIViewController {
         
         tableView.delegate = self
         tableView.dataSource = self
+        
+        let center: NotificationCenter = NotificationCenter.default
+        center.addObserver(self, selector: #selector(refresh(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
+    }
+    
+    @objc func addFriend(sender: UIBarButtonItem) {
+        let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
+        if let vc = controller.viewControllers.first as? AddFriendTableViewController {
+            vc.isDismiss = {
+                self.getData()
+                self.dataPersonNotChange = self.dataPerson
+                self.tableView.reloadData()
+            }
+        }
+        self.navigationController?.present(controller, animated: true, completion: nil)
+    }
+    
+    @objc func refresh(notification: NSNotification) {
+        DispatchQueue.main.async {
+            self.getData()
+            self.dataPersonNotChange = self.dataPerson
+            self.tableView.reloadData()
+        }
     }
 
     @objc func didTapCancel(sender: AnyObject) {
@@ -99,9 +138,10 @@ class ContactCallViewController: UIViewController {
     }
     
     func getData() {
+        dataPerson.removeAll()
         let idMe = UserDefaults.standard.string(forKey: "me") as String?
         Database.shared.database?.inTransaction({ (fmdb, rollback) in
-            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, official_account, image_id, device_id, offline_mode, user_type FROM BUDDY where official_account<>'1' and f_pin <> '\(idMe!)' order by 2 collate nocase asc") {
+            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, official_account, image_id, device_id, offline_mode, user_type, ex_block FROM BUDDY where official_account<>'1' and f_pin <> '\(idMe!)' order by 2 collate nocase asc") {
                 while cursorData.next() {
                     var row: [String: String?] = [:]
                     row["f_pin"] = cursorData.string(forColumnIndex: 0)
@@ -118,6 +158,10 @@ class ContactCallViewController: UIViewController {
                     if name.trimmingCharacters(in: .whitespaces) == "USR\(row["f_pin"]!!)" {
                         continue
                     }
+                    row["block"] = cursorData.string(forColumnIndex: 8)
+                    if row["block"] == "1" || row["block"] == "-1" {
+                        continue
+                    }
                     row["name"] = name
                     row["picture"] = cursorData.string(forColumnIndex: 4)
                     row["isOfficial"] = cursorData.string(forColumnIndex: 3)
@@ -132,7 +176,7 @@ class ContactCallViewController: UIViewController {
     }
     
     func filterContentForSearchText(_ searchText: String) {
-        fillteredData = dataPerson.filter { $0["name"]!!.lowercased().contains(searchText.lowercased()) }
+        fillteredData = self.dataPerson.filter { $0["name"]!!.lowercased().contains(searchText.lowercased()) }
         tableView.reloadData()
     }
 }
@@ -203,9 +247,6 @@ extension ContactCallViewController: UITableViewDataSource {
     
     @objc func call(sender: Any) {
         let index = sender as! UIButton
-        if isFilltering {
-            dataPerson = fillteredData
-        }
         if let pin = dataPerson[index.tag]["f_pin"] {
             let controller = QmeraAudioViewController()
             controller.user = User.getData(pin: pin)

+ 11 - 14
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/AddFriendTableViewController.swift

@@ -7,7 +7,7 @@
 
 import UIKit
 
-public class AddFriendTableViewController: UITableViewController {
+class AddFriendTableViewController: UITableViewController {
     
     var searchController: UISearchController!
     
@@ -23,7 +23,7 @@ public class AddFriendTableViewController: UITableViewController {
         return searchController.isActive && !isSearchBarEmpty
     }
     
-    public var isDismiss: (() -> ())?
+    var isDismiss: (() -> ())?
     
     func filterContentForSearchText(_ searchText: String) {
         fillteredData = data.filter{ $0.fullName.lowercased().contains(searchText.lowercased()) }
@@ -37,14 +37,14 @@ public class AddFriendTableViewController: UITableViewController {
         tableView.reloadData()
     }
     
-    public override func viewDidDisappear(_ animated: Bool) {
+    override func viewDidDisappear(_ animated: Bool) {
         isDismiss?()
     }
     
-    public override func viewDidLoad() {
+    override func viewDidLoad() {
         super.viewDidLoad()
         
-        title = "Friends".localized()
+        title = "Add Friends".localized()
         
         navigationController?.navigationBar.prefersLargeTitles = true
         
@@ -124,11 +124,11 @@ public class AddFriendTableViewController: UITableViewController {
     
     // MARK: - Table view data source
     
-    public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
         return "Suggestions"
     }
     
-    public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         let user: User
         if isFilltering {
             user = fillteredData[indexPath.row]
@@ -151,14 +151,14 @@ public class AddFriendTableViewController: UITableViewController {
         navigationController?.show(controller, sender: nil)
     }
     
-    public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         if isFilltering {
             return fillteredData.count
         }
         return data.count
     }
     
-    public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
         var content = cell.defaultContentConfiguration()
         let user: User
@@ -168,11 +168,8 @@ public class AddFriendTableViewController: UITableViewController {
             user = data[indexPath.row]
         }
         content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
             content.image = image
-            if isDownloaded {
-                tableView.reloadRows(at: [indexPath], with: .none)
-            }
         })
         content.text = user.fullName
         content.textProperties.font = UIFont.systemFont(ofSize: 14)
@@ -186,7 +183,7 @@ public class AddFriendTableViewController: UITableViewController {
 
 extension AddFriendTableViewController: UISearchControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating {
     
-    public func updateSearchResults(for searchController: UISearchController) {
+    func updateSearchResults(for searchController: UISearchController) {
         filterContentForSearchText(searchController.searchBar.text!)
     }
     

+ 4 - 3
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/AudienceViewController.swift

@@ -47,16 +47,17 @@ class AudienceViewController: UITableViewController {
             destination.targetAudienceLabel.text = cell.textLabel?.text
             destination.targetAudienceLabel.tag = cell.tag
             destination.dest = "\(cell.tag + 1)"
-            if (destination.dest == BroadcastViewController.DESTINATION_GROUP || destination.dest == BroadcastViewController.DESTINATION_SPESIFIC) {
+            if(destination.dest == BroadcastViewController.DESTINATION_GROUP || destination.dest == BroadcastViewController.DESTINATION_SPESIFIC) {
                 destination.memberSection.isHidden = false
             }
             else {
+                destination.memberSection.frame.size.height = 0.0
                 destination.memberSection.isHidden = true
             }
-            if (destination.dest == BroadcastViewController.DESTINATION_GROUP) {
+            if(destination.dest == BroadcastViewController.DESTINATION_GROUP){
                 destination.memberListLabel.text = "Groups"
             }
-            else {
+            else{
                 destination.memberListLabel.text = "Members"
             }
             destination.memberTable.reloadData()

+ 25 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/BroadcastMembersTableViewCell.swift

@@ -70,6 +70,13 @@ class BroadcastMembersTableViewController: UITableViewController, UISearchContro
         
         navigationItem.searchController = searchController
         navigationItem.hidesSearchBarWhenScrolling = false
+        
+        let buttonAddFriend = UIBarButtonItem(image: UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 18, weight: .regular, scale: .default)), style: .plain, target: self, action: #selector(addFriend(sender:)))
+        navigationItem.rightBarButtonItem = buttonAddFriend
+        
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
+            self.getData()
+        })
 
         // Uncomment the following line to preserve selection between presentations
         // self.clearsSelectionOnViewWillAppear = false
@@ -78,8 +85,23 @@ class BroadcastMembersTableViewController: UITableViewController, UISearchContro
         // self.navigationItem.rightBarButtonItem = self.editButtonItem
     }
     
+    @objc func addFriend(sender: UIBarButtonItem) {
+        let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
+        if let vc = controller.viewControllers.first as? AddFriendTableViewController {
+            vc.isDismiss = {
+                self.contacts.removeAll()
+                self.getContacts {
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                    }
+                }
+            }
+        }
+        self.navigationController?.present(controller, animated: true, completion: nil)
+    }
+    
     override func viewWillAppear(_ animated: Bool) {
-        getData()
+        pullBuddy()
     }
     
     @objc func onReload(notification: NSNotification) {
@@ -255,9 +277,8 @@ class BroadcastMembersTableViewController: UITableViewController, UISearchContro
             }
             content.text = group.name
             content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-            getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+            getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                 content.image = image
-                tableView.reloadRows(at: [indexPath], with: .none)
             }
             cell.contentConfiguration = content
             cell.tag = indexPath.row
@@ -272,11 +293,8 @@ class BroadcastMembersTableViewController: UITableViewController, UISearchContro
                 data = contacts[indexPath.row]
             }
             content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-            getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+            getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                 content.image = image
-                if isDownloaded {
-                    tableView.reloadRows(at: [indexPath], with: .none)
-                }
             })
             if (data.official == "1") {
                 content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)

+ 26 - 17
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/BroadcastModeViewController.swift

@@ -80,10 +80,15 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
     
     var forms: [Form] = []
     
+    var loadingView = UIViewController()
+    
     override func viewDidLoad() {
         super.viewDidLoad()
-        let me = UserDefaults.standard.string(forKey: "me")!
-        Nexilis.write(message: CoreMessage_TMessageBank.getFormList(p_pin: me, p_last_id: "0"))
+        
+        DispatchQueue.global().async {
+            let me = UserDefaults.standard.string(forKey: "me")!
+            _ = Nexilis.write(message: CoreMessage_TMessageBank.getFormList(p_pin: me, p_last_id: "0"))
+        }
         
         titleTextField.delegate = self
         messageTextView.delegate = self
@@ -155,6 +160,7 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
             let destination = segue.destination as! AudienceViewController
             destination.isBroadcast = true
             destination.selected = targetAudienceLabel.text
+            
         } else if segue.identifier == "br_type" {
             let destination = segue.destination as! TypeViewController
             destination.selected = broadcastTypeLabel.text
@@ -263,6 +269,17 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
         previewItem = nil
     }
     
+    func showActivityIndicatory() {
+        loadingView.view.backgroundColor = .black.withAlphaComponent(0.3)
+        let activityView = UIActivityIndicatorView(style: UIActivityIndicatorView.Style.large)
+        activityView.center = self.view.center
+        loadingView.view.addSubview(activityView)
+        activityView.startAnimating()
+        loadingView.modalPresentationStyle = .custom
+        loadingView.modalTransitionStyle = .crossDissolve
+        self.present(loadingView, animated: true)
+    }
+    
     @IBAction func submitBroadcast(_ sender: Any) {
         let startTime = startTimePicker.date.millisecondsSince1970
         var endTime = startTime
@@ -319,11 +336,7 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
         print("thumb_id: \(thumbId)")
         print("file_id: \(fileId)")
         print("members: \(membersInvited)")
-        let bp = false
-        if(bp){
-            navigationController?.dismiss(animated: true, completion: nil)
-            return
-        }
+        showActivityIndicatory()
         if(form == BroadcastViewController.FORM_NOT_FORM){
             if(fileType == BroadcastViewController.FILE_TYPE_IMAGE){
                 saveImage()
@@ -506,7 +519,7 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
     }
     
     func sendMsg(startTime: Int64, endTime: Int64){
-        var message = CoreMessage_TMessageBank.broadcastMessage(title: messageTitle, broadcast_flag: mode, message: message, starting_date: startTime, ending_date: startTime, destination: dest, data: membersInvited, category_flag: fileType, notification_type: type, link: link, thumb_id: thumbId, file_id: fileId)
+        let message = CoreMessage_TMessageBank.broadcastMessage(title: messageTitle, broadcast_flag: mode, message: message, starting_date: startTime, ending_date: startTime, destination: dest, data: membersInvited, category_flag: fileType, notification_type: type, link: link, thumb_id: thumbId, file_id: fileId)
         if(form != BroadcastViewController.FORM_NOT_FORM){
             message.mBodies[CoreMessage_TMessageKey.FILE_ID] = form
             message.mBodies[CoreMessage_TMessageKey.MESSAGE_SCOPE_ID] = "18"
@@ -514,11 +527,10 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
         if let response = Nexilis.writeSync(message: message) {
             if (response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "00") {
                 DispatchQueue.main.async {
-                    self.navigationController?.dismiss(animated: true, completion: nil)
+                    self.navigationController?.presentingViewController?.dismiss(animated: true)
                 }
-            }
-            else {
-                
+            } else {
+                self.loadingView.dismiss(animated: true)
             }
         }
     }
@@ -545,11 +557,8 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
                 let data: User
                 data = contacts[indexPath.row]
                 content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-                getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+                getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                     content.image = image
-                    if isDownloaded {
-                        tableView.reloadRows(at: [indexPath], with: .none)
-                    }
                 })
                 if (data.official == "1") {
                     content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)
@@ -571,7 +580,7 @@ class BroadcastViewController: UITableViewController, UITextFieldDelegate, UITex
                 let group: Group
                 group = groups[indexPath.row]
                 content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-                getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+                getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                     content.image = image
 //                    tableView.reloadRows(at: [indexPath], with: .none)
                 }

+ 17 - 10
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeDeviceViewController.swift

@@ -18,12 +18,19 @@ public class ChangeDeviceViewController: UIViewController {
     
     public override func viewDidLoad() {
         super.viewDidLoad()
+        
+        let randomInt = Int.random(in: 5..<11)
+        let image = UIImage(named: "lbackground_\(randomInt)")
+        if image != nil {
+            self.view.backgroundColor = UIColor.init(patternImage: image!)
+        }
 
         self.title = "Change Device".localized()
         navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Submit".localized(), style: .plain, target: self, action: #selector(didTapSubmit(sender:)))
         
         passwordField.addPadding(.right(40))
-        passwordField.isSecureTextEntry = false
+        passwordField.isSecureTextEntry = true
+        showPasswordButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
         
         showPasswordButton.addTarget(self, action: #selector(showPassword), for: .touchUpInside)
         
@@ -51,35 +58,35 @@ public class ChangeDeviceViewController: UIViewController {
         guard let name = usernameField.text, !name.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Username can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Username can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if !name.matches("^[a-zA-Z ]*$") {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         guard let password = passwordField.text, !password.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if password.count < 6 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if !CheckConnection.isConnectedToNetwork() || API.nGetCLXConnState() == 0 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -90,14 +97,14 @@ public class ChangeDeviceViewController: UIViewController {
                     DispatchQueue.main.async {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Invalid user / Username and password does not match".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Invalid user / Username and password does not match".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                     }
                 } else if !response.isOk() {
                     DispatchQueue.main.async {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                     }
                 } else {
@@ -115,7 +122,7 @@ public class ChangeDeviceViewController: UIViewController {
                         DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
                             let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
                             imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Successfully changed device".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+                            let banner = FloatingNotificationBanner(title: "Successfully changed device".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                             banner.show()
                             self.navigationController?.popViewController(animated: true)
                             self.isDismiss?(thumb)
@@ -126,7 +133,7 @@ public class ChangeDeviceViewController: UIViewController {
                 DispatchQueue.main.async {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                 }
             }

+ 28 - 15
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeNamePassswordViewController.swift

@@ -17,17 +17,30 @@ public class ChangeNamePassswordViewController: UIViewController {
     public var fromSetting = false
     public var isSuccess: (() -> ())?
     
+    public override func viewWillDisappear(_ animated: Bool) {
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshView"), object: nil, userInfo: nil)
+    }
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
+        
+        let randomInt = Int.random(in: 5..<11)
+        let image = UIImage(named: "lbackground_\(randomInt)")
+        if image != nil {
+            self.view.backgroundColor = UIColor.init(patternImage: image!)
+        }
 
         self.title = "Change Profile".localized()
         if !fromSetting {
+            let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
+            self.navigationController?.navigationBar.titleTextAttributes = attributes
             navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapExit(sender:)))
         }
         navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .save, target: self, action: #selector(didTapSave(sender:)))
         
         passwordField.addPadding(.right(40))
-        passwordField.isSecureTextEntry = false
+        passwordField.isSecureTextEntry = true
+        showPasswordButton.setImage(UIImage(systemName: "eye.slash.fill"), for: .normal)
         
         showPasswordButton.addTarget(self, action: #selector(showPassword), for: .touchUpInside)
         
@@ -54,28 +67,28 @@ public class ChangeNamePassswordViewController: UIViewController {
         guard let name = usernameField.text, !name.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Username can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Username can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if !name.matches("^[a-zA-Z ]*$") {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Contains prohibited characters. Only alphabetic characters are allowed.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         guard let password = passwordField.text, !password.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if password.count < 6 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -86,7 +99,7 @@ public class ChangeNamePassswordViewController: UIViewController {
         if first.count > 24 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "First name is too long".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "First name is too long".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -94,7 +107,7 @@ public class ChangeNamePassswordViewController: UIViewController {
         if last.count > 24 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Last name is too long".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Last name is too long".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -104,7 +117,7 @@ public class ChangeNamePassswordViewController: UIViewController {
         if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -114,7 +127,7 @@ public class ChangeNamePassswordViewController: UIViewController {
                     DispatchQueue.main.async {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Username has already been registered. Please use another username".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Username has already been registered. Please use another username".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                     }
                 } else if resp.isOk() {
@@ -136,7 +149,7 @@ public class ChangeNamePassswordViewController: UIViewController {
                             DispatchQueue.main.async {
                                 let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
                                 imageView.tintColor = .white
-                                let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+                                let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                                 banner.show()
                                 self.didTapExit(sender: "exit")
                             }
@@ -145,7 +158,7 @@ public class ChangeNamePassswordViewController: UIViewController {
                         DispatchQueue.main.async {
                             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                             imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                            let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                             banner.show()
                         }
                     }
@@ -154,7 +167,7 @@ public class ChangeNamePassswordViewController: UIViewController {
                 DispatchQueue.main.async {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                 }
             }
@@ -173,9 +186,9 @@ public class ChangeNamePassswordViewController: UIViewController {
 
 }
 
-class PasswordTextField: UITextField {
+public class PasswordTextField: UITextField {
 
-    override var isSecureTextEntry: Bool {
+    public override var isSecureTextEntry: Bool {
         didSet {
             if isFirstResponder {
                 _ = becomeFirstResponder()
@@ -184,7 +197,7 @@ class PasswordTextField: UITextField {
         }
     }
 
-    override func becomeFirstResponder() -> Bool {
+    public override func becomeFirstResponder() -> Bool {
 
         let success = super.becomeFirstResponder()
         if isSecureTextEntry, let text = self.text {

+ 8 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangeNameTableViewController.swift

@@ -6,6 +6,7 @@
 //
 
 import UIKit
+import NotificationBannerSwift
 
 class ChangeNameTableViewController: UITableViewController {
     
@@ -45,6 +46,13 @@ class ChangeNameTableViewController: UITableViewController {
                         self.navigationController?.popViewController(animated: true)
                         self.isDismiss?()
                     }
+                } else if resp.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "1a" {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Username has been registered, please use another name".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
                 }
             }
         }

+ 7 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ChangePasswordViewController.swift

@@ -43,35 +43,35 @@ class ChangePasswordViewController: UIViewController {
         guard let odlPassword = oldPassField.text, !odlPassword.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Old password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Old password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if odlPassword.count < 6 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Old password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Old password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if odlPassword != UserDefaults.standard.string(forKey: "pwd"){
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Incorrect old password".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Incorrect old password".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         guard let newPassword = newPassField.text, !newPassword.isEmpty else {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "New password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "New password can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
         if newPassword.count < 6 {
             let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "New password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "New password min 6 character".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
             banner.show()
             return
         }
@@ -88,7 +88,7 @@ class ChangePasswordViewController: UIViewController {
                     DispatchQueue.main.async {
                         let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Successfully changed password".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Successfully changed password".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                         banner.show()
                         self.navigationController?.popViewController(animated: true)
                     }
@@ -97,7 +97,7 @@ class ChangePasswordViewController: UIViewController {
                 DispatchQueue.main.async {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Unable to access servers. Try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                 }
             }

+ 870 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/CheckConnection.swift

@@ -0,0 +1,870 @@
+//
+//  ContactChatViewController.swift
+//  Qmera
+//
+//  Created by Yayan Dwi on 22/09/21.
+//
+
+import UIKit
+import FMDB
+import NotificationBannerSwift
+
+class ContactChatViewController: UITableViewController {
+    
+    deinit {
+        print(#function, ">>>> TADAA")
+        NotificationCenter.default.removeObserver(self)
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshView"), object: nil, userInfo: nil)
+    }
+    
+    var isChooser: ((String, String) -> ())?
+    
+    var isAdmin: Bool = false
+    
+    var chats: [Chat] = []
+    
+    var contacts: [User] = []
+    
+    var groups: [Group] = []
+    
+    var groupMap: [String:Int] = [:]
+    
+    var searchController: UISearchController!
+    
+    var segment: UISegmentedControl!
+    
+    var fillteredData: [Any] = []
+    
+    var isSearchBarEmpty: Bool {
+        return searchController.searchBar.text?.isEmpty ?? true
+    }
+    
+    var isFilltering: Bool {
+        return searchController.isActive && !isSearchBarEmpty
+    }
+    
+    func filterContentForSearchText(_ searchText: String) {
+        switch segment.selectedSegmentIndex {
+        case 1:
+            fillteredData = self.contacts.filter { $0.fullName.lowercased().contains(searchText.lowercased()) }
+        case 2:
+            fillteredData = self.groups.filter { $0.name.lowercased().contains(searchText.lowercased()) }
+        default:
+            fillteredData = self.chats.filter { $0.name.lowercased().contains(searchText.lowercased()) || $0.messageText.lowercased().contains(searchText.lowercased()) }
+        }
+        tableView.reloadData()
+    }
+    
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        let me = UserDefaults.standard.string(forKey: "me")!
+        Database.shared.database?.inTransaction({ fmdb, rollback in
+            if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select FIRST_NAME, LAST_NAME, IMAGE_ID, USER_TYPE from BUDDY where F_PIN = '\(me)'"), cursor.next() {
+                isAdmin = cursor.string(forColumnIndex: 3) == "23" || cursor.string(forColumnIndex: 3) == "24"
+                cursor.close()
+            }
+        })
+        
+        title = "Start Conversation"
+        
+        //        navigationController?.navigationBar.prefersLargeTitles = true
+        
+        navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(sender:)))
+        
+        var childrenMenu : [UIAction] = [
+            UIAction(title: "Create Group", image: UIImage(systemName: "person.and.person"), handler: {[weak self](_) in
+                let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "createGroupNav") as! UINavigationController
+                let vc = controller.topViewController as! GroupCreateViewController
+                vc.isDismiss = { id in
+                    let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "groupDetailView") as! GroupDetailViewController
+                    controller.data = id
+                    self?.navigationController?.show(controller, sender: nil)
+                }
+                self?.navigationController?.present(controller, animated: true, completion: nil)
+            }),
+            UIAction(title: "Add Friends", image: UIImage(systemName: "person.badge.plus"), handler: {[weak self](_) in
+                let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
+                if let vc = controller.viewControllers.first as? AddFriendTableViewController {
+                    vc.isDismiss = {
+                        self?.getContacts {
+                            DispatchQueue.main.async {
+                                self?.tableView.reloadData()
+                            }
+                        }
+                    }
+                }
+                self?.navigationController?.present(controller, animated: true, completion: nil)
+            }),
+//            UIAction(title: "Configure Email", image: UIImage(systemName: "mail"), handler: {[weak self](_) in
+//
+//            }),
+            UIAction(title: "Favorite Messages", image: UIImage(systemName: "star"), handler: {[weak self](_) in
+                let editorStaredVC = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "staredVC") as! EditorStarMessages
+                self?.navigationController?.show(editorStaredVC, sender: nil)
+            }),
+        ]
+        //debug only
+//        isAdmin = true
+        if(isAdmin){
+            childrenMenu.append(UIAction(title: "Broadcast Message", image: UIImage(systemName: "envelope.open"), handler: {[weak self](_) in
+                let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "broadcastNav")
+                self?.navigationController?.present(controller, animated: true, completion: nil)
+            }))
+        }
+        
+        let menu = UIMenu(title: "", children: childrenMenu)
+        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .add, primaryAction: .none, menu: menu)
+        
+        searchController = UISearchController(searchResultsController: nil)
+        searchController.delegate = self
+        searchController.searchResultsUpdater = self
+        searchController.searchBar.autocapitalizationType = .none
+        searchController.searchBar.delegate = self
+        searchController.searchBar.barTintColor = .secondaryColor
+        searchController.searchBar.searchTextField.backgroundColor = .secondaryColor
+        searchController.obscuresBackgroundDuringPresentation = false
+        
+        navigationItem.searchController = searchController
+        navigationItem.hidesSearchBarWhenScrolling = true
+        
+        definesPresentationContext = true
+        
+        segment = UISegmentedControl(items: ["Chat", "Contact", "Group"])
+        segment.sizeToFit()
+        segment.selectedSegmentIndex = 0
+        segment.addTarget(self, action: #selector(segmentChanged(sender:)), for: .valueChanged)
+        
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onMember"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
+        
+        tableView.tableHeaderView = segment
+        tableView.tableFooterView = UIView()
+        
+        pullBuddy()
+        
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        getData()
+    }
+    
+    @objc func onReload(notification: NSNotification) {
+        let data:[AnyHashable : Any] = notification.userInfo!
+        if data["member"] as? String == UserDefaults.standard.string(forKey: "me") {
+            DispatchQueue.main.async {
+                self.getData()
+            }
+        } else if data["state"] as? Int == 99 {
+            DispatchQueue.main.async {
+                self.getData()
+            }
+        }
+    }
+    
+    @objc func onReceiveMessage(notification: NSNotification) {
+        let data:[AnyHashable : Any] = notification.userInfo!
+        guard let dataMessage = data["message"] as? TMessage else {
+            return
+        }
+        let isUser = User.getData(pin: dataMessage.getBody(key: CoreMessage_TMessageKey.L_PIN)) != nil
+        let chatId = dataMessage.getBody(key: CoreMessage_TMessageKey.CHAT_ID, default_value: "").isEmpty ? dataMessage.getBody(key: CoreMessage_TMessageKey.L_PIN) : dataMessage.getBody(key: CoreMessage_TMessageKey.CHAT_ID, default_value: "")
+        let pin = isUser ? dataMessage.getBody(key: CoreMessage_TMessageKey.F_PIN) : chatId
+        let messageId = dataMessage.getBody(key: CoreMessage_TMessageKey.MESSAGE_ID)
+        if let index = chats.firstIndex(of: Chat(pin: pin)) {
+            guard let chat = Chat.getData(messageId: messageId).first else {
+                return
+            }
+            DispatchQueue.main.async {
+                if self.segment.selectedSegmentIndex == 0 {
+                    self.tableView.beginUpdates()
+                    self.chats.remove(at: index)
+                    self.tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .none)
+                }
+                self.chats.insert(chat, at: 0)
+                if self.segment.selectedSegmentIndex == 0 {
+                    self.tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .none)
+                    self.tableView.endUpdates()
+                }
+            }
+        } else {
+            guard let chat = Chat.getData(messageId: messageId).first else {
+                return
+            }
+            DispatchQueue.main.async {
+                if self.segment.selectedSegmentIndex == 0 {
+                    self.tableView.beginUpdates()
+                }
+                self.chats.insert(chat, at: 0)
+                if self.segment.selectedSegmentIndex == 0 {
+                    self.tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .none)
+                    self.tableView.endUpdates()
+                }
+            }
+        }
+    }
+    
+    @objc func add(sender: Any) {
+        
+    }
+    
+    @objc func cancel(sender: Any) {
+        navigationController?.dismiss(animated: true, completion: nil)
+    }
+    
+    @objc func segmentChanged(sender: Any) {
+        filterContentForSearchText(searchController.searchBar.text!)
+        if segment.selectedSegmentIndex == 2 {
+            self.getGroups { g1 in
+                self.getOpenGroups(listGroups: g1, completion: { g in
+                    self.groups.removeAll()
+                    self.groups.append(contentsOf: g1)
+                    for og in g {
+                        if self.groups.first(where: { $0.id == og.id }) == nil {
+                            self.groups.append(og)
+                        }
+                    }
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                    }
+                })
+            }
+        }
+    }
+    
+    // MARK: - Data source
+    
+    func getData() {
+        getChats {
+            self.getContacts {
+                self.getGroups { g1 in
+                    self.getOpenGroups(listGroups: g1, completion: { g in
+                        self.groups.removeAll()
+                        self.groups.append(contentsOf: g1)
+                        for og in g {
+                            if self.groups.first(where: { $0.id == og.id }) == nil {
+                                self.groups.append(og)
+                            }
+                        }
+                        DispatchQueue.main.async {
+                            self.tableView.reloadData()
+                        }
+                    })
+                }
+            }
+        }
+    }
+    
+    func getChats(completion: @escaping ()->()) {
+        self.chats.removeAll()
+        DispatchQueue.global().async {
+            self.chats.append(contentsOf: Chat.getData())
+            completion()
+        }
+    }
+    
+    private func getContacts(completion: @escaping ()->()) {
+        self.contacts.removeAll()
+        DispatchQueue.global().async {
+            Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, image_id, official_account, user_type FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' order by 5 desc, 2 collate nocase asc") {
+                    while cursorData.next() {
+                        let user = User(pin: cursorData.string(forColumnIndex: 0) ?? "",
+                                        firstName: cursorData.string(forColumnIndex: 1) ?? "",
+                                        lastName: cursorData.string(forColumnIndex: 2) ?? "",
+                                        thumb: cursorData.string(forColumnIndex: 3) ?? "",
+                                        userType: cursorData.string(forColumnIndex: 5) ?? "")
+                        if (user.firstName + " " + user.lastName).trimmingCharacters(in: .whitespaces) == "USR\(user.pin)" {
+                            continue
+                        }
+                        user.official = cursorData.string(forColumnIndex: 4) ?? ""
+                        self.contacts.append(user)
+                    }
+                    cursorData.close()
+                }
+                completion()
+            })
+        }
+    }
+    
+    private func getGroupRecursive(fmdb: FMDatabase, id: String = "", parent: String = "") -> [Group] {
+        var data: [Group] = []
+        var query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.is_education from GROUPZ g where "
+        if id.isEmpty {
+            query += "g.parent = '\(parent)'"
+        } else {
+            query += "g.group_id = '\(id)'"
+        }
+        if let cursor = Database.shared.getRecords(fmdb: fmdb, query: query) {
+            while cursor.next() {
+                let group = Group(
+                    id: cursor.string(forColumnIndex: 0) ?? "",
+                    name: cursor.string(forColumnIndex: 1) ?? "",
+                    profile: cursor.string(forColumnIndex: 2) ?? "",
+                    quote: cursor.string(forColumnIndex: 3) ?? "",
+                    by: cursor.string(forColumnIndex: 4) ?? "",
+                    date: cursor.string(forColumnIndex: 5) ?? "",
+                    parent: cursor.string(forColumnIndex: 6) ?? "",
+                    chatId: "",
+                    groupType: cursor.string(forColumnIndex: 7) ?? "",
+                    isOpen: cursor.string(forColumnIndex: 8) ?? "",
+                    official: cursor.string(forColumnIndex: 9) ?? "",
+                    isEducation: cursor.string(forColumnIndex: 10) ?? "")
+                
+                if group.chatId.isEmpty {
+                    let lounge = Group(id: group.id, name: "Lounge".localized(), profile: "", quote: group.quote, by: group.by, date: group.date, parent: group.id, chatId: group.chatId, groupType: group.groupType, isOpen: group.isOpen, official: group.official, isEducation: group.isEducation, isLounge: true)
+                    group.childs.append(lounge)
+                }
+                
+                if let topicCursor = Database.shared.getRecords(fmdb: fmdb, query: "select chat_id, title, thumb from DISCUSSION_FORUM where group_id = '\(group.id)'") {
+                    while topicCursor.next() {
+                        let topic = Group(id: group.id,
+                                          name: topicCursor.string(forColumnIndex: 1) ?? "",
+                                          profile: topicCursor.string(forColumnIndex: 2) ?? "",
+                                          quote: group.quote,
+                                          by: group.by,
+                                          date: group.date,
+                                          parent: group.id,
+                                          chatId: topicCursor.string(forColumnIndex: 0) ?? "",
+                                          groupType: group.groupType,
+                                          isOpen: group.isOpen,
+                                          official: group.official,
+                                          isEducation: group.isEducation)
+                        group.childs.append(topic)
+                    }
+                    topicCursor.close()
+                }
+                
+                if !group.id.isEmpty {
+                    if group.official == "1" {
+                        let idMe = UserDefaults.standard.string(forKey: "me") as String?
+                        if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
+//                            if cursorUser.string(forColumnIndex: 0) == "23" || cursorUser.string(forColumnIndex: 0) == "24" {
+//                                group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                            }
+                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+                            cursorUser.close()
+                        }
+                    } else if group.official != "1"{
+                        group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+                    }
+                    group.childs = group.childs.sorted(by: { $0.name < $1.name })
+                    let dataLounge = group.childs.filter({$0.name == "Lounge".localized()})
+                    group.childs = group.childs.filter({ $0.name != "Lounge".localized() })
+                    group.childs.insert(contentsOf: dataLounge, at: 0)
+                }
+                data.append(group)
+            }
+            cursor.close()
+        }
+        return data
+    }
+    
+    private func getOpenGroups(listGroups: [Group], completion: @escaping ([Group]) -> ()) {
+        if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getOpenGroups(p_account: "1,2,3,5,6,7", offset: "0", search: "")) {
+            var dataGroups: [Group] = []
+            if (response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "00") {
+                let data = response.getBody(key: CoreMessage_TMessageKey.DATA)
+                if let json = try! JSONSerialization.jsonObject(with: data.data(using: String.Encoding.utf8)!, options: []) as? [[String: Any?]] {
+                    for dataJson in json {
+                        let group = Group(
+                            id: dataJson[CoreMessage_TMessageKey.GROUP_ID] as? String ?? "",
+                            name: dataJson[CoreMessage_TMessageKey.GROUP_NAME] as? String ?? "",
+                            profile: dataJson[CoreMessage_TMessageKey.THUMB_ID] as? String ?? "",
+                            quote: dataJson[CoreMessage_TMessageKey.QUOTE] as? String ?? "",
+                            by: dataJson[CoreMessage_TMessageKey.BLOCK] as? String ?? "",
+                            date: "",
+                            parent: "",
+                            chatId: "",
+                            groupType: "NOTJOINED",
+                            isOpen: dataJson[CoreMessage_TMessageKey.IS_OPEN] as? String ?? "",
+                            official: "0",
+                            isEducation: "")
+                        dataGroups.append(group)
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    self.groups.removeAll()
+                    self.groups.append(contentsOf: listGroups)
+                    self.tableView.reloadData()
+                }
+            }
+            completion(dataGroups)
+        }
+    }
+    
+    private func getGroups(id: String = "", parent: String = "", completion: @escaping ([Group]) -> ()) {
+        DispatchQueue.global().async {
+            Database.shared.database?.inTransaction({ fmdb, rollback in
+                completion(self.getGroupRecursive(fmdb: fmdb, id: id, parent: parent))
+            })
+        }
+    }
+    
+    private func pullBuddy() {
+        if let me = UserDefaults.standard.string(forKey: "me") {
+            DispatchQueue.global().async {
+                let _ = Nexilis.write(message: CoreMessage_TMessageBank.getBatchBuddiesInfos(p_f_pin: me, last_update: 0))
+            }
+        }
+    }
+    
+    private func joinOpenGroup(groupId: String, flagMember: String = "0", completion: @escaping (Bool) -> ()) {
+        DispatchQueue.global().async {
+            var result: Bool = false
+            let idMe = UserDefaults.standard.string(forKey: "me") as String?
+            if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getAddGroupMember(p_group_id: groupId, p_member_pin: idMe!, p_position: "0")), response.isOk() {
+                result = true
+            }
+            completion(result)
+        }
+    }
+    
+}
+
+// MARK: - Table view data source
+
+extension ContactChatViewController {
+    
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+        switch segment.selectedSegmentIndex {
+        case 0:
+            let data: Chat
+            if isFilltering {
+                data = fillteredData[indexPath.row] as! Chat
+            } else {
+                data = chats[indexPath.row]
+            }
+            if let chooser = isChooser {
+                if data.pin == "-999"{
+                    return
+                }
+                chooser(data.messageScope, data.pin)
+                dismiss(animated: true, completion: nil)
+                return
+            }
+            let user = User.getData(pin: data.pin)
+            if user != nil || data.pin == "-999" {
+                let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+                editorPersonalVC.hidesBottomBarWhenPushed = true
+                editorPersonalVC.unique_l_pin = data.pin
+                navigationController?.show(editorPersonalVC, sender: nil)
+            } else {
+                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                editorGroupVC.hidesBottomBarWhenPushed = true
+                editorGroupVC.unique_l_pin = data.pin
+                navigationController?.show(editorGroupVC, sender: nil)
+            }
+        case 1:
+            let data: User
+            if isFilltering {
+                data = fillteredData[indexPath.row] as! User
+            } else {
+                data = contacts[indexPath.row]
+            }
+            if let chooser = isChooser {
+                chooser("3", data.pin)
+                dismiss(animated: true, completion: nil)
+                return
+            }
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        case 2:
+            let group: Group
+            if isFilltering {
+                if indexPath.row == 0 {
+                    group = fillteredData[indexPath.section] as! Group
+                } else {
+                    group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
+                }
+            } else {
+                if indexPath.row == 0 {
+                    group = groups[indexPath.section]
+                } else {
+                    group = groups[indexPath.section].childs[indexPath.row - 1]
+                }
+            }
+            group.isSelected = !group.isSelected
+            if !group.isSelected{
+                var sects = 0
+                var sect = indexPath.section
+                var id = group.id
+                if let e = groupMap[id] {
+                    var loooop = true
+                    repeat {
+                        let c = sect + 1
+                        if isFilltering {
+                            if let o = self.fillteredData[c] as? Group {
+                                if o.parent == id {
+                                    sects = sects + 1
+                                    sect = c
+                                    id = o.id
+                                }
+                                else {
+                                    loooop = false
+                                }
+                            }
+                        }
+                        else {
+                            if self.groups[c].parent == id {
+                                sects = sects + 1
+                                sect = c
+                                id = self.groups[c].id
+                            }
+                            else {
+                                loooop = false
+                            }
+                        }
+                    } while(loooop)
+                }
+                for i in stride(from: sects, to: 0, by: -1){
+                    if isFilltering {
+                        self.fillteredData.remove(at: indexPath.section + i)
+                    }
+                    else {
+                        self.groups.remove(at: indexPath.section + i)
+                    }
+                }
+                groupMap.removeValue(forKey: group.id)
+            }
+            if group.groupType == "NOTJOINED" {
+                let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
+                alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+                alert.addAction(UIAlertAction(title: "Join".localized(), style: .default, handler: {(_) in
+                    self.joinOpenGroup(groupId: group.id, completion: { result in
+                        if result {
+                            DispatchQueue.main.async {
+                                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                                editorGroupVC.hidesBottomBarWhenPushed = true
+                                editorGroupVC.unique_l_pin = group.id
+                                self.navigationController?.show(editorGroupVC, sender: nil)
+                            }
+                        }
+                    })
+                }))
+                self.present(alert, animated: true, completion: nil)
+                return
+            }
+            if group.childs.count == 0 {
+                let groupId = group.chatId.isEmpty ? group.id : group.chatId
+                if let chooser = isChooser {
+                    chooser("4", groupId)
+                    dismiss(animated: true, completion: nil)
+                    return
+                }
+                let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
+                editorGroupVC.hidesBottomBarWhenPushed = true
+                editorGroupVC.unique_l_pin = groupId
+                navigationController?.show(editorGroupVC, sender: nil)
+            } else {
+                if indexPath.row == 0 {
+                    tableView.reloadData()
+                } else {
+                    getGroups(id: group.id) { g in
+                        DispatchQueue.main.async {
+                            print("index path section: \(indexPath.section)")
+                            print("index path row: \(indexPath.row)")
+//                            print("index path item: \(indexPath.item)")
+                            if self.isFilltering {
+//                                self.fillteredData.remove(at: indexPath.section)
+                                if self.fillteredData[indexPath.section] is Group {
+                                    self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
+                                    self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
+                                }
+                            } else {
+//                                self.groups.remove(at: indexPath.section)
+                                self.groupMap[self.groups[indexPath.section].id] = 1
+                                self.groups.insert(contentsOf: g, at: indexPath.section + 1)
+                            }
+                            print("groupMap: \(self.groupMap)")
+                            tableView.reloadData()
+                        }
+                    }
+                }
+            }
+        default:
+            let data = contacts[indexPath.row]
+            let editorPersonalVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorPersonalVC") as! EditorPersonal
+            editorPersonalVC.hidesBottomBarWhenPushed = true
+            editorPersonalVC.unique_l_pin = data.pin
+            navigationController?.show(editorPersonalVC, sender: nil)
+        }
+    }
+    
+}
+
+extension ContactChatViewController {
+    
+    override func numberOfSections(in tableView: UITableView) -> Int {
+        if isFilltering {
+            if segment.selectedSegmentIndex == 2 {
+                return fillteredData.count
+            }
+            return 1
+        } else {
+            if segment.selectedSegmentIndex == 2 {
+                return groups.count
+            }
+            return 1
+        }
+    }
+    
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        var value = 0
+        if isFilltering {
+            if segment.selectedSegmentIndex == 2, let groups = fillteredData as? [Group] {
+                let group = groups[section]
+                if group.isSelected {
+                    if let g = groupMap[group.id] {
+                        value = 1
+                    }
+                    else {
+                        value = group.childs.count + 1
+                    }
+                } else {
+                    value = 1
+                }
+            }
+            return fillteredData.count
+        }
+        switch segment.selectedSegmentIndex {
+        case 0:
+            value = chats.count
+        case 1:
+            value = contacts.count
+        case 2:
+            let group = groups[section]
+            if group.isSelected {
+                if let g = groupMap[group.id] {
+                    value = 1
+                }
+                else {
+                    value = group.childs.count + 1
+                }
+            } else {
+                value = 1
+            }
+        default:
+            value = chats.count
+        }
+        return value
+    }
+    
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        var cell: UITableViewCell!
+        switch segment.selectedSegmentIndex {
+        case 0:
+            cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierChat", for: indexPath)
+            cell.separatorInset.left = 60.0
+            let content = cell.contentView
+            if content.subviews.count > 0 {
+                content.subviews.forEach { $0.removeFromSuperview() }
+            }
+            let data: Chat
+            if isFilltering {
+                data = fillteredData[indexPath.row] as! Chat
+            } else {
+                data = chats[indexPath.row]
+            }
+            let imageView = UIImageView()
+            content.addSubview(imageView)
+            imageView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 10.0),
+                imageView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0),
+                imageView.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -20.0),
+                imageView.widthAnchor.constraint(equalToConstant: 40.0),
+                imageView.heightAnchor.constraint(equalToConstant: 40.0)
+            ])
+            if data.profile.isEmpty && data.pin != "-999" {
+                let user = User.getData(pin: data.pin)
+                if user != nil {
+                    imageView.image = UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+                } else {
+                    imageView.image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
+                }
+            } else {
+                getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "gaspol_icon" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+                    imageView.image = image
+                })
+            }
+            let titleView = UILabel()
+            content.addSubview(titleView)
+            titleView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                titleView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10.0),
+                titleView.topAnchor.constraint(equalTo: content.topAnchor, constant: 10.0),
+                titleView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+            ])
+            titleView.text = data.name
+            titleView.font = UIFont.systemFont(ofSize: 14, weight: .medium)
+            
+            let messageView = UILabel()
+            content.addSubview(messageView)
+            messageView.translatesAutoresizingMaskIntoConstraints = false
+            NSLayoutConstraint.activate([
+                messageView.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: 10.0),
+                messageView.topAnchor.constraint(equalTo: titleView.bottomAnchor, constant: 5.0),
+                messageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -40.0),
+            ])
+            messageView.textColor = .gray
+            let text = Utils.previewMessageText(chat: data)
+            if let attributeText = text as? NSAttributedString {
+                messageView.attributedText = attributeText
+            } else if let stringText = text as? String {
+                messageView.text = stringText
+            }
+            messageView.font = UIFont.systemFont(ofSize: 12)
+            messageView.numberOfLines = 2
+            
+            if data.counter != "0" {
+                let viewCounter = UIView()
+                content.addSubview(viewCounter)
+                viewCounter.translatesAutoresizingMaskIntoConstraints = false
+                NSLayoutConstraint.activate([
+                    viewCounter.centerYAnchor.constraint(equalTo: content.centerYAnchor),
+                    viewCounter.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
+                    viewCounter.widthAnchor.constraint(greaterThanOrEqualToConstant: 20),
+                    viewCounter.heightAnchor.constraint(equalToConstant: 20)
+                ])
+                viewCounter.backgroundColor = .systemRed
+                viewCounter.layer.cornerRadius = 10
+                viewCounter.clipsToBounds = true
+                viewCounter.layer.borderWidth = 0.5
+                viewCounter.layer.borderColor = UIColor.secondaryColor.cgColor
+
+                let labelCounter = UILabel()
+                viewCounter.addSubview(labelCounter)
+                labelCounter.translatesAutoresizingMaskIntoConstraints = false
+                NSLayoutConstraint.activate([
+                    labelCounter.centerYAnchor.constraint(equalTo: viewCounter.centerYAnchor),
+                    labelCounter.leadingAnchor.constraint(equalTo: viewCounter.leadingAnchor, constant: 2),
+                    labelCounter.trailingAnchor.constraint(equalTo: viewCounter.trailingAnchor, constant: -2),
+                ])
+                labelCounter.font = UIFont.systemFont(ofSize: 11)
+                if Int(data.counter)! > 99 {
+                    labelCounter.text = "99+"
+                } else {
+                    labelCounter.text = data.counter
+                }
+                labelCounter.textColor = .secondaryColor
+                labelCounter.textAlignment = .center
+            }
+        case 1:
+            cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierContact", for: indexPath)
+            var content = cell.defaultContentConfiguration()
+            let data: User
+            if isFilltering {
+                data = fillteredData[indexPath.row] as! User
+            } else {
+                data = contacts[indexPath.row]
+            }
+            content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
+            getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
+                content.image = image
+            })
+            if (data.official == "1") {
+                content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)
+            }
+            else if data.userType == "23" {
+                content.attributedText = self.set(image: UIImage(named: "ic_internal", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)
+            } else if data.userType == "24" {
+                content.attributedText = self.set(image: UIImage(named: "pb_call_center", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)
+            }
+            else {
+                content.text = data.fullName
+            }
+            content.textProperties.font = UIFont.systemFont(ofSize: 14)
+            cell.contentConfiguration = content
+        case 2:
+            cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierGroup", for: indexPath)
+            var content = cell.defaultContentConfiguration()
+            content.textProperties.font = UIFont.systemFont(ofSize: 14)
+            let group: Group
+            if isFilltering {
+                if indexPath.row == 0 {
+                    group = fillteredData[indexPath.section] as! Group
+                } else {
+                    group = (fillteredData[indexPath.section] as! Group).childs[indexPath.row - 1]
+                }
+            } else {
+                if indexPath.row == 0 {
+                    group = groups[indexPath.section]
+                } else {
+                    group = groups[indexPath.section].childs[indexPath.row - 1]
+                }
+            }
+            if group.official == "1" && group.parent == "" {
+                content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(group.name)", size: 15, y: -4)
+            }
+            else if group.isOpen == "1" && group.parent == "" {
+                if self.traitCollection.userInterfaceStyle == .dark {
+                    content.attributedText = self.set(image: UIImage(systemName: "globe")!.withTintColor(.white), with: "  \(group.name)", size: 15, y: -4)
+                } else {
+                    content.attributedText = self.set(image: UIImage(systemName: "globe")!, with: "  \(group.name)", size: 15, y: -4)
+                }
+            } else if group.parent == "" {
+                if self.traitCollection.userInterfaceStyle == .dark {
+                    content.attributedText = self.set(image: UIImage(systemName: "lock.fill")!.withTintColor(.white), with: "  \(group.name)", size: 15, y: -4)
+                } else {
+                    content.attributedText = self.set(image: UIImage(systemName: "lock.fill")!, with: "  \(group.name)", size: 15, y: -4)
+                }
+            } else {
+                content.text = group.name
+            }
+            if group.childs.count > 0 {
+                let iconName = (group.isSelected) ? "chevron.up.circle" : "chevron.down.circle"
+                let imageView = UIImageView(image: UIImage(systemName: iconName))
+                imageView.tintColor = .black
+                cell.accessoryView = imageView
+            }
+            else {
+                cell.accessoryView = nil
+                cell.accessoryType = .none
+            }
+            content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
+            getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
+                content.image = image
+            }
+            cell.contentConfiguration = content
+        default:
+            cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifierContact", for: indexPath)
+            var content = cell.defaultContentConfiguration()
+            content.text = ""
+            cell.contentConfiguration = content
+        }
+        return cell
+    }
+    
+    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+        return 70
+    }
+    
+}
+
+
+extension ContactChatViewController: UISearchControllerDelegate, UISearchBarDelegate, UISearchResultsUpdating {
+    
+    func updateSearchResults(for searchController: UISearchController) {
+        filterContentForSearchText(searchController.searchBar.text!)
+    }
+    
+    func set(image: UIImage, with text: String, size: CGFloat, y: CGFloat) -> NSAttributedString {
+        let attachment = NSTextAttachment()
+        attachment.image = image
+        attachment.bounds = CGRect(x: 0, y: y, width: size, height: size)
+        let attachmentStr = NSAttributedString(attachment: attachment)
+        
+        let mutableAttributedString = NSMutableAttributedString()
+        mutableAttributedString.append(attachmentStr)
+        
+        let textString = NSAttributedString(string: text)
+        mutableAttributedString.append(textString)
+        
+        return mutableAttributedString
+    }
+    
+}

+ 149 - 48
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ContactChatViewController.swift

@@ -26,6 +26,8 @@ class ContactChatViewController: UITableViewController {
     
     var groups: [Group] = []
     
+    var groupMap: [String:Int] = [:]
+    
     var searchController: UISearchController!
     
     var segment: UISegmentedControl!
@@ -63,23 +65,26 @@ class ContactChatViewController: UITableViewController {
         })
         
         title = "Start Conversation".localized()
+        let attributes = [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
+        self.navigationController?.navigationBar.titleTextAttributes = attributes
         
         //        navigationController?.navigationBar.prefersLargeTitles = true
         
         navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(cancel(sender:)))
         
         var childrenMenu : [UIAction] = [
-            UIAction(title: "Create Group".localized(), image: UIImage(systemName: "person.and.person"), handler: {[weak self](_) in
+            UIAction(title: "Create Group", image: UIImage(systemName: "person.and.person"), handler: {[weak self](_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "createGroupNav") as! UINavigationController
                 let vc = controller.topViewController as! GroupCreateViewController
                 vc.isDismiss = { id in
+                    self?.groupMap.removeAll()
                     let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "groupDetailView") as! GroupDetailViewController
                     controller.data = id
                     self?.navigationController?.show(controller, sender: nil)
                 }
                 self?.navigationController?.present(controller, animated: true, completion: nil)
             }),
-            UIAction(title: "Add Friends".localized(), image: UIImage(systemName: "person.badge.plus"), handler: {[weak self](_) in
+            UIAction(title: "Add Friends", image: UIImage(systemName: "person.badge.plus"), handler: {[weak self](_) in
                 let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "addFriendNav") as! UINavigationController
                 if let vc = controller.viewControllers.first as? AddFriendTableViewController {
                     vc.isDismiss = {
@@ -95,7 +100,7 @@ class ContactChatViewController: UITableViewController {
 //            UIAction(title: "Configure Email", image: UIImage(systemName: "mail"), handler: {[weak self](_) in
 //
 //            }),
-            UIAction(title: "Favorite Messages".localized(), image: UIImage(systemName: "star"), handler: {[weak self](_) in
+            UIAction(title: "Favorite Messages", image: UIImage(systemName: "star"), handler: {[weak self](_) in
                 let editorStaredVC = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "staredVC") as! EditorStarMessages
                 self?.navigationController?.show(editorStaredVC, sender: nil)
             }),
@@ -126,13 +131,15 @@ class ContactChatViewController: UITableViewController {
         
         definesPresentationContext = true
         
-        segment = UISegmentedControl(items: ["Chat".localized(), "Contact".localized(), "Group".localized()])
+        segment = UISegmentedControl(items: ["Chats".localized(), "Contacts".localized(), "Groups".localized()])
         segment.sizeToFit()
         segment.selectedSegmentIndex = 0
         segment.addTarget(self, action: #selector(segmentChanged(sender:)), for: .valueChanged)
+        segment.setTitleTextAttributes([NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 12.0)], for: .normal)
         
         NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
         NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onMember"), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onReload(notification:)), name: NSNotification.Name(rawValue: "onUpdatePersonInfo"), object: nil)
         
         tableView.tableHeaderView = segment
         tableView.tableFooterView = UIView()
@@ -142,12 +149,17 @@ class ContactChatViewController: UITableViewController {
     }
     
     override func viewWillAppear(_ animated: Bool) {
+        groupMap.removeAll()
         getData()
     }
     
     @objc func onReload(notification: NSNotification) {
         let data:[AnyHashable : Any] = notification.userInfo!
-        if data["member"] as! String == UserDefaults.standard.string(forKey: "me")! {
+        if data["member"] as? String == UserDefaults.standard.string(forKey: "me") {
+            DispatchQueue.main.async {
+                self.getData()
+            }
+        } else if data["state"] as? Int == 99 {
             DispatchQueue.main.async {
                 self.getData()
             }
@@ -206,11 +218,29 @@ class ContactChatViewController: UITableViewController {
     
     @objc func segmentChanged(sender: Any) {
         filterContentForSearchText(searchController.searchBar.text!)
+        if segment.selectedSegmentIndex == 2 {
+            self.getGroups { g1 in
+                self.getOpenGroups(listGroups: g1, completion: { g in
+                    self.groups.removeAll()
+                    self.groups.append(contentsOf: g1)
+                    for og in g {
+                        if self.groups.first(where: { $0.id == og.id }) == nil {
+                            self.groups.append(og)
+                        }
+                    }
+                    DispatchQueue.main.async {
+                        self.tableView.reloadData()
+                    }
+                })
+            }
+        }
     }
     
     // MARK: - Data source
     
     func getData() {
+        self.chats.removeAll()
+        self.contacts.removeAll()
         getChats {
             self.getContacts {
                 self.getGroups { g1 in
@@ -232,36 +262,36 @@ class ContactChatViewController: UITableViewController {
     }
     
     func getChats(completion: @escaping ()->()) {
+        self.chats.removeAll()
         DispatchQueue.global().async {
-            self.chats.removeAll()
-            self.chats.append(contentsOf: Chat.getData())
+            let chatData = Chat.getData()
+            if !self.chats.contains(where: {$0.messageId == chatData.first?.messageId ?? ""}) {
+                self.chats.append(contentsOf: chatData)
+            }
             completion()
         }
     }
     
     private func getContacts(completion: @escaping ()->()) {
+        self.contacts.removeAll()
         DispatchQueue.global().async {
-            self.contacts.removeAll()
             Database.shared.database?.inTransaction({ (fmdb, rollback) in
-                do {
-                    if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, image_id, official_account, user_type FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' order by 5 desc, 2 collate nocase asc") {
-                        while cursorData.next() {
-                            let user = User(pin: cursorData.string(forColumnIndex: 0) ?? "",
-                                            firstName: cursorData.string(forColumnIndex: 1) ?? "",
-                                            lastName: cursorData.string(forColumnIndex: 2) ?? "",
-                                            thumb: cursorData.string(forColumnIndex: 3) ?? "",
-                                            userType: cursorData.string(forColumnIndex: 5) ?? "")
-                            if (user.firstName + " " + user.lastName).trimmingCharacters(in: .whitespaces) == "USR\(user.pin)" {
-                                continue
-                            }
-                            user.official = cursorData.string(forColumnIndex: 4) ?? ""
+                if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: "SELECT f_pin, first_name, last_name, image_id, official_account, user_type FROM BUDDY where f_pin <> '\(UserDefaults.standard.string(forKey: "me")!)' order by 5 desc, 2 collate nocase asc") {
+                    while cursorData.next() {
+                        let user = User(pin: cursorData.string(forColumnIndex: 0) ?? "",
+                                        firstName: cursorData.string(forColumnIndex: 1) ?? "",
+                                        lastName: cursorData.string(forColumnIndex: 2) ?? "",
+                                        thumb: cursorData.string(forColumnIndex: 3) ?? "",
+                                        userType: cursorData.string(forColumnIndex: 5) ?? "")
+                        if (user.firstName + " " + user.lastName).trimmingCharacters(in: .whitespaces) == "USR\(user.pin)" {
+                            continue
+                        }
+                        user.official = cursorData.string(forColumnIndex: 4) ?? ""
+                        if !self.contacts.contains(where: {$0.pin == user.pin}) {
                             self.contacts.append(user)
                         }
-                        cursorData.close()
                     }
-                } catch {
-                    rollback.pointee = true
-                    print(error)
+                    cursorData.close()
                 }
                 completion()
             })
@@ -320,14 +350,19 @@ class ContactChatViewController: UITableViewController {
                     if group.official == "1" {
                         let idMe = UserDefaults.standard.string(forKey: "me") as String?
                         if let cursorUser = Database.shared.getRecords(fmdb: fmdb, query: "SELECT user_type FROM BUDDY where f_pin='\(idMe!)'"), cursorUser.next() {
-                            if cursorUser.string(forColumnIndex: 0) == "23" || cursorUser.string(forColumnIndex: 0) == "24" {
-                                group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
-                            }
+//                            if cursorUser.string(forColumnIndex: 0) == "23" || cursorUser.string(forColumnIndex: 0) == "24" {
+//                                group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
+//                            }
+                            group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
                             cursorUser.close()
                         }
                     } else if group.official != "1"{
                         group.childs.append(contentsOf: getGroupRecursive(fmdb: fmdb, parent: group.id))
                     }
+                    group.childs = group.childs.sorted(by: { $0.name < $1.name })
+                    let dataLounge = group.childs.filter({$0.name == "Lounge".localized()})
+                    group.childs = group.childs.filter({ $0.name != "Lounge".localized() })
+                    group.childs.insert(contentsOf: dataLounge, at: 0)
                 }
                 data.append(group)
             }
@@ -427,6 +462,7 @@ extension ContactChatViewController {
                 editorPersonalVC.unique_l_pin = data.pin
                 navigationController?.show(editorPersonalVC, sender: nil)
             } else {
+                groupMap.removeAll()
                 let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
                 editorGroupVC.hidesBottomBarWhenPushed = true
                 editorGroupVC.unique_l_pin = data.pin
@@ -464,6 +500,52 @@ extension ContactChatViewController {
                 }
             }
             group.isSelected = !group.isSelected
+            if !group.isSelected{
+                var sects = 0
+                var sect = indexPath.section
+                var id = group.id
+                if let e = groupMap[id] {
+                    var loooop = true
+                    repeat {
+                        let c = sect + 1
+                        if isFilltering {
+                            if let o = self.fillteredData[c] as? Group {
+                                if o.parent == id {
+                                    sects = sects + 1
+                                    sect = c
+                                    id = o.id
+                                    (self.fillteredData[c] as! Group).isSelected = false
+                                    self.groupMap.removeValue(forKey: (self.fillteredData[c] as! Group).id)
+                                }
+                                else {
+                                    loooop = false
+                                }
+                            }
+                        }
+                        else {
+                            if self.groups[c].parent == id {
+                                sects = sects + 1
+                                sect = c
+                                id = self.groups[c].id
+                                self.groups[c].isSelected = false
+                                self.groupMap.removeValue(forKey: self.groups[c].id)
+                            }
+                            else {
+                                loooop = false
+                            }
+                        }
+                    } while(loooop)
+                }
+                for i in stride(from: sects, to: 0, by: -1){
+                    if isFilltering {
+                        self.fillteredData.remove(at: indexPath.section + i)
+                    }
+                    else {
+                        self.groups.remove(at: indexPath.section + i)
+                    }
+                }
+                groupMap.removeValue(forKey: group.id)
+            }
             if group.groupType == "NOTJOINED" {
                 let alert = UIAlertController(title: "Do you want to join this group?".localized(), message: "Groups : \(group.name)\nMembers: \(group.by)".localized(), preferredStyle: .alert)
                 alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
@@ -471,6 +553,7 @@ extension ContactChatViewController {
                     self.joinOpenGroup(groupId: group.id, completion: { result in
                         if result {
                             DispatchQueue.main.async {
+                                self.groupMap.removeAll()
                                 let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
                                 editorGroupVC.hidesBottomBarWhenPushed = true
                                 editorGroupVC.unique_l_pin = group.id
@@ -489,6 +572,7 @@ extension ContactChatViewController {
                     dismiss(animated: true, completion: nil)
                     return
                 }
+                self.groupMap.removeAll()
                 let editorGroupVC = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "editorGroupVC") as! EditorGroup
                 editorGroupVC.hidesBottomBarWhenPushed = true
                 editorGroupVC.unique_l_pin = groupId
@@ -499,13 +583,21 @@ extension ContactChatViewController {
                 } else {
                     getGroups(id: group.id) { g in
                         DispatchQueue.main.async {
+                            print("index path section: \(indexPath.section)")
+                            print("index path row: \(indexPath.row)")
+//                            print("index path item: \(indexPath.item)")
                             if self.isFilltering {
-                                self.fillteredData.remove(at: indexPath.section)
-                                self.fillteredData.insert(contentsOf: g, at: indexPath.section)
+//                                self.fillteredData.remove(at: indexPath.section)
+                                if self.fillteredData[indexPath.section] is Group {
+                                    self.groupMap[(self.fillteredData[indexPath.section] as! Group).id] = 1
+                                    self.fillteredData.insert(contentsOf: g, at: indexPath.section + 1)
+                                }
                             } else {
-                                self.groups.remove(at: indexPath.section)
-                                self.groups.insert(contentsOf: g, at: indexPath.section)
+//                                self.groups.remove(at: indexPath.section)
+                                self.groupMap[self.groups[indexPath.section].id] = 1
+                                self.groups.insert(contentsOf: g, at: indexPath.section + 1)
                             }
+                            print("groupMap: \(self.groupMap)")
                             tableView.reloadData()
                         }
                     }
@@ -544,11 +636,15 @@ extension ContactChatViewController {
             if segment.selectedSegmentIndex == 2, let groups = fillteredData as? [Group] {
                 let group = groups[section]
                 if group.isSelected {
-                    value = group.childs.count + 1
+                    if let g = groupMap[group.id] {
+                        value = 1
+                    }
+                    else {
+                        value = group.childs.count + 1
+                    }
                 } else {
                     value = 1
                 }
-                return value
             }
             return fillteredData.count
         }
@@ -560,7 +656,12 @@ extension ContactChatViewController {
         case 2:
             let group = groups[section]
             if group.isSelected {
-                value = group.childs.count + 1
+                if let g = groupMap[group.id] {
+                    value = 1
+                }
+                else {
+                    value = group.childs.count + 1
+                }
             } else {
                 value = 1
             }
@@ -604,14 +705,8 @@ extension ContactChatViewController {
                     imageView.image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                 }
             } else {
-                getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_ball" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+                getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_ball" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                     imageView.image = image
-                    if !result {
-                        imageView.tintColor = .mainColor
-                    }
-                    if isDownloaded {
-                        tableView.reloadRows(at: [indexPath], with: .none)
-                    }
                 })
             }
             let titleView = UILabel()
@@ -686,11 +781,8 @@ extension ContactChatViewController {
                 data = contacts[indexPath.row]
             }
             content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-            getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+            getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                 content.image = image
-                if isDownloaded {
-                    tableView.reloadRows(at: [indexPath], with: .none)
-                }
             })
             if (data.official == "1") {
                 content.attributedText = self.set(image: UIImage(named: "ic_official_flag", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: "  \(data.fullName)", size: 15, y: -4)
@@ -741,10 +833,19 @@ extension ContactChatViewController {
             } else {
                 content.text = group.name
             }
+            if group.childs.count > 0 {
+                let iconName = (group.isSelected) ? "chevron.up.circle" : "chevron.down.circle"
+                let imageView = UIImageView(image: UIImage(systemName: iconName))
+                imageView.tintColor = .black
+                cell.accessoryView = imageView
+            }
+            else {
+                cell.accessoryView = nil
+                cell.accessoryType = .none
+            }
             content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-            getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+            getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                 content.image = image
-                tableView.reloadRows(at: [indexPath], with: .none)
             }
             cell.contentConfiguration = content
         default:
@@ -757,7 +858,7 @@ extension ContactChatViewController {
     }
     
     override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
-        return 70.0
+        return 70
     }
     
 }

+ 3 - 3
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/DocumentPicker.swift

@@ -7,15 +7,15 @@
 
 import UIKit
 
-public class GroupCreateViewController: UITableViewController {
+class GroupCreateViewController: UITableViewController {
 
     @IBOutlet weak var name: UITextField!
     
     private let id = Date().currentTimeMillis().toHex()
     
-    public var isDismiss: ((String) -> ())?
+    var isDismiss: ((String) -> ())?
     
-    public override func viewDidLoad() {
+    override func viewDidLoad() {
         super.viewDidLoad()
         
         title = "Create Group"

+ 139 - 26
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupDescViewController.swift

@@ -6,8 +6,10 @@
 //
 
 import UIKit
+import NotificationBannerSwift
+import nuSDKService
 
-public class GroupDetailViewController: UITableViewController {
+class GroupDetailViewController: UITableViewController {
     
     enum Flag {
         case edit
@@ -16,7 +18,9 @@ public class GroupDetailViewController: UITableViewController {
     
     var flag: Flag = .edit
     
-    public var data: String = ""
+    var data: String = ""
+    
+    static let SUBGROUP_LEVEL_LIMIT = 5
     
     private var group: Group?
     
@@ -50,7 +54,9 @@ public class GroupDetailViewController: UITableViewController {
     
     var checkReadMessage: (() -> ())?
     
-    public override func viewDidLoad() {
+    private let idSubGroup = Date().currentTimeMillis().toHex()
+    
+    override func viewDidLoad() {
         super.viewDidLoad()
         
         imageVideoPicker = ImageVideoPicker(presentationController: self, delegate: self)
@@ -66,11 +72,13 @@ public class GroupDetailViewController: UITableViewController {
         center.addObserver(self, selector: #selector(updateData(notification:)), name: NSNotification.Name(rawValue: "onMember"), object: nil)
     }
     
-    public override func viewWillDisappear(_ animated: Bool) {
+    override func viewWillDisappear(_ animated: Bool) {
         if self.isMovingFromParent {
             self.checkReadMessage?()
         }
     }
+    var alert2: UIAlertController?
+    var textFields = [UITextField]()
     
     // MARK: - Data source
     
@@ -105,13 +113,69 @@ public class GroupDetailViewController: UITableViewController {
                 }
                 
                 if self.isAdmin && group.official != "1" {
-                    self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(self.edit(sender:)))
+                    var children : [UIAction] = []
+                    if Int(group.level)! <= GroupDetailViewController.SUBGROUP_LEVEL_LIMIT {
+                        children.append(UIAction(title: "Add Sub Group".localized(), handler: {(_) in
+                            self.createSubGroup()
+                        }))
+                    }
+                    children.append(UIAction(title: "Change Name Group".localized(), handler: {(_) in
+                        self.edit()
+                    }))
+                    let menu = UIMenu(title: "", children: children)
+                    let moreIcon = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), menu: menu)
+                    self.navigationItem.rightBarButtonItem = moreIcon
                 }
             }
         }
     }
     
-    @objc func edit(sender: Any?) {
+    func createSubGroup() {
+        self.alert2 = UIAlertController(title: "Create Sub Group".localized(), message: nil, preferredStyle: .alert)
+        self.textFields.removeAll()
+        self.alert2?.addTextField{ (texfield) in
+            texfield.placeholder = "Group's Name"
+            texfield.addTarget(self, action: #selector(self.alertTextFieldDidChange(_:)), for: UIControl.Event.editingChanged)
+        }
+        let submitAction = UIAlertAction(title: "Create".localized(), style: .default, handler: { (action) -> Void in
+            let textField = self.alert2?.textFields![0]
+            if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                imageView.tintColor = .white
+                let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                banner.show()
+                return
+            }
+            var level = self.group!.level
+            if level.isEmpty || level == "-1"{
+                level = "2"
+            } else {
+                level = "\(Int(level)! + 1)"
+            }
+            print("level: \(level)")
+            DispatchQueue.main.async {
+                if let response = Nexilis.writeAndWait(message: CoreMessage_TMessageBank.getCreateSubGroup(group_id: self.idSubGroup, group_name: textField!.text!, parent_id: self.group!.id, level: level)) {
+                    if response.isOk() {
+                        let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "groupDetailView") as! GroupDetailViewController
+                        controller.data = self.idSubGroup
+                        self.navigationController?.show(controller, sender: nil)
+                        self.navigationController?.viewControllers.removeSubrange(1...(self.navigationController?.viewControllers.count)! - 2)
+                    } else {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                }
+            }
+        })
+        self.alert2?.addAction(submitAction)
+        self.alert2?.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+        
+        self.present(self.alert2!, animated: true, completion: nil)
+    }
+    
+    func edit() {
         let controller = AppStoryBoard.Palio.instance.instantiateViewController(identifier: "groupNameView") as! GroupNameViewController
         controller.data = self.data
         controller.name = group?.name
@@ -127,7 +191,7 @@ public class GroupDetailViewController: UITableViewController {
             DispatchQueue.main.async {
                 self.navigationController?.popViewController(animated: true)
             }
-        } else if data["f_pin"] as! String != UserDefaults.standard.string(forKey: "me")! {
+        } else if data["f_pin"] as! String != UserDefaults.standard.string(forKey: "me")! || data["code"] as! String == "BD" {
             DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                 self.reload()
             }
@@ -136,7 +200,7 @@ public class GroupDetailViewController: UITableViewController {
     
     private func getData(completion: @escaping (Group) -> ()) {
         DispatchQueue.global().async {
-            let query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official from GROUPZ g where g.group_id = '\(self.data)'"
+            let query = "select g.group_id, g.f_name, g.image_id, g.quote, g.created_by, g.created_date, g.parent, g.group_type, g.is_open, g.official, g.level from GROUPZ g where g.group_id = '\(self.data)'"
             Database.shared.database?.inTransaction({ fmdb, rollback in
                 if let cursor = Database.shared.getRecords(fmdb: fmdb, query: query), cursor.next() {
                     let group = Group(id: cursor.string(forColumnIndex: 0) ?? "",
@@ -148,7 +212,8 @@ public class GroupDetailViewController: UITableViewController {
                                        parent: cursor.string(forColumnIndex: 6) ?? "",
                                        groupType: cursor.string(forColumnIndex: 7) ?? "",
                                        isOpen: cursor.string(forColumnIndex: 8) ?? "",
-                                       official: cursor.string(forColumnIndex: 9) ?? "")
+                                       official: cursor.string(forColumnIndex: 9) ?? "",
+                                    level: cursor.string(forColumnIndex: 10) ?? "")
                     cursor.close()
                     
                     group.topics.append(Topic(chatId: "", title: "Lounge".localized(), thumb: ""))
@@ -188,7 +253,7 @@ public class GroupDetailViewController: UITableViewController {
     
     // MARK: - Cell selected
     
-    public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
         tableView.deselectRow(at: indexPath, animated: false)
         switch sections[indexPath.section] {
         case .profile:
@@ -281,6 +346,51 @@ public class GroupDetailViewController: UITableViewController {
                         self.navigationController?.viewControllers.first?.show(editorGroupVC, sender: nil)
                     }
                 }))
+                alert.addAction(UIAlertAction(title: "Rename Topic", style: .default, handler: { action in
+                    self.alert2 = UIAlertController(title: "Change Topic's Name".localized(), message: nil, preferredStyle: .alert)
+                    self.textFields.removeAll()
+                    self.alert2?.addTextField{ (texfield) in
+                        texfield.text = topic.title
+                        texfield.placeholder = "Topic's Name"
+                        texfield.addTarget(self, action: #selector(self.alertTextFieldDidChange(_:)), for: UIControl.Event.editingChanged)
+                    }
+                    let submitAction = UIAlertAction(title: "Rename".localized(), style: .default, handler: { (action) -> Void in
+                        let textField = self.alert2?.textFields![0]
+                        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                            return
+                        }
+                        if textField!.text! == topic.title {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Topic name has been used. Enter another topic name".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                            return
+                        }
+                        if textField!.text! == "Lounge" || textField!.text! == "Beranda" {
+                            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Topic already registered".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                            banner.show()
+                            return
+                        }
+                        DispatchQueue.main.async {
+                            if let g = self.group, let _ = Nexilis.write(message: CoreMessage_TMessageBank.getUpdateChat(p_chat_id: topic.chatId, p_f_pin: g.id, p_title: textField!.text!, p_anonym: "", p_image: topic.thumb)) {
+                                let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                                imageView.tintColor = .white
+                                let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                                banner.show()
+                            }
+                        }
+                    })
+                    self.alert2?.addAction(submitAction)
+                    self.alert2?.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: nil))
+                    
+                    self.present(self.alert2!, animated: true, completion: nil)
+                }))
                 alert.addAction(UIAlertAction(title: "Remove Topic".localized(), style: .destructive, handler: { action in
                     let message = "Remove \(topic.title) from the \"\(g.name)\" group?"
                     let notif = UIAlertController(title: nil, message: message, preferredStyle: .actionSheet)
@@ -492,6 +602,15 @@ public class GroupDetailViewController: UITableViewController {
         }
     }
     
+    @objc func alertTextFieldDidChange(_ sender: UITextField) {
+        if(!textFields.isEmpty){
+            alert2?.actions[0].isEnabled = textFields[0].text!.trimmingCharacters(in: .whitespaces).count > 0
+        }
+        else {
+            alert2?.actions[0].isEnabled = sender.text!.trimmingCharacters(in: .whitespaces).count > 0
+        }
+    }
+    
     private func removeTopic(chatId: String, completion: @escaping (Bool) -> ()) {
         DispatchQueue.global().async {
             var result: Bool = false
@@ -546,11 +665,11 @@ public class GroupDetailViewController: UITableViewController {
     
     // MARK: - Table view data source
     
-    public override func numberOfSections(in tableView: UITableView) -> Int {
+    override func numberOfSections(in tableView: UITableView) -> Int {
         return sections.count
     }
     
-    public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
         guard let g = group else {
             return 1
         }
@@ -566,7 +685,7 @@ public class GroupDetailViewController: UITableViewController {
         }
     }
     
-    public override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
+    override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
         switch sections[section] {
         case .description:
             return "Description".localized()
@@ -581,7 +700,7 @@ public class GroupDetailViewController: UITableViewController {
         }
     }
     
-    public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         switch sections[indexPath.section] {
         case .profile:
             let cell = tableView.dequeueReusableCell(withIdentifier: "profileCell", for: indexPath) as! ProfileCell
@@ -592,7 +711,7 @@ public class GroupDetailViewController: UITableViewController {
             if let image = tempImage {
                 cell.profile.image = image
             } else {
-                getImage(name: g.profile, placeholderImage: UIImage(systemName: "person.2.circle.fill")) { result, isDownloaded, image in
+                getImage(name: g.profile, placeholderImage: UIImage(systemName: "person.2.circle.fill"), tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                     cell.profile.image = image
                 }
             }
@@ -648,14 +767,11 @@ public class GroupDetailViewController: UITableViewController {
                     cell.selectionStyle = .default
                 } else {
                     let topic = g.topics[isAdmin ? indexPath.row - 1 : indexPath.row]
-                    getImage(name: topic.thumb, placeholderImage: UIImage(systemName: "message.fill"), isCircle: true) { result, isDownloaded, image in
+                    getImage(name: topic.thumb, placeholderImage: UIImage(systemName: "message.fill"), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                         content.image = image
                         if !result {
                             content.imageProperties.tintColor = .mainColor
                         }
-                        if isDownloaded {
-                            tableView.reloadRows(at: [indexPath], with: .none)
-                        }
                     }
                     content.text = topic.title
                     content.secondaryText = topic.description
@@ -708,14 +824,11 @@ public class GroupDetailViewController: UITableViewController {
                 } else {
                     let member = g.members[isAdmin ? indexPath.row - 1 : indexPath.row]
                     content.imageProperties.maximumSize = CGSize(width: 20, height: 20)
-                    getImage(name: member.thumb, placeholderImage: UIImage(systemName: "person.fill"), isCircle: true) { result, isDownloaded, image in
+                    getImage(name: member.thumb, placeholderImage: UIImage(systemName: "person.fill"), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                         content.image = image
                         if !result {
                             content.imageProperties.tintColor = .mainColor
                         }
-                        if isDownloaded {
-                            tableView.reloadRows(at: [indexPath], with: .none)
-                        }
                     }
                     if member.userType == "23" || member.official == "1" {
                         content.attributedText = self.set(image: UIImage(named: "ic_internal", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: " " + (member.firstName + " " + member.lastName).trimmingCharacters(in: .whitespaces), size: 15, y: 0)
@@ -751,7 +864,7 @@ public class GroupDetailViewController: UITableViewController {
         }
     }
     
-    public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
+    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
         switch sections[indexPath.section] {
         case .profile:
             return 200
@@ -781,7 +894,7 @@ class ProfileCell: UITableViewCell {
 
 extension GroupDetailViewController: ImageVideoPickerDelegate {
     
-    public func didSelect(imagevideo: Any?) {
+    func didSelect(imagevideo: Any?) {
         if let info = imagevideo as? [UIImagePickerController.InfoKey: Any], let cell = tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? ProfileCell, let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
             
             guard let g = group else {
@@ -812,7 +925,7 @@ extension GroupDetailViewController: ImageVideoPickerDelegate {
         }
     }
     
-    public func set(image: UIImage, image2: UIImage = UIImage(), with text: String, size: CGFloat, y: CGFloat, moreImage: Bool = false) -> NSAttributedString {
+    func set(image: UIImage, image2: UIImage = UIImage(), with text: String, size: CGFloat, y: CGFloat, moreImage: Bool = false) -> NSAttributedString {
         let attachment = NSTextAttachment()
         let attachment2 = NSTextAttachment()
         attachment.image = image

+ 1 - 4
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupMemberViewController.swift

@@ -188,14 +188,11 @@ class GroupMemberViewController: UITableViewController {
             user = availableUser[indexPath.row]
         }
         content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
             content.image = image
             if !result {
                 content.imageProperties.tintColor = .mainColor
             }
-            if isDownloaded {
-                tableView.reloadRows(at: [indexPath], with: .none)
-            }
         }
         if user.userType == "23" {
             content.attributedText = self.set(image: UIImage(named: "ic_internal", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)!, with: " " + (user.firstName + " " + user.lastName).trimmingCharacters(in: .whitespaces), size: 15, y: -4)

+ 31 - 7
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/GroupNameViewController.swift

@@ -26,7 +26,7 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
         super.viewDidLoad()
         view.backgroundColor = .secondaryColor
         
-        title = "Broadcast History".localized()
+        title = "Notification Center".localized()
         
         navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(cancel(sender:)))
         
@@ -60,6 +60,30 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
 //                self.historyTableView.reloadData()
 //            }
 //        }
+        NotificationCenter.default.addObserver(self, selector: #selector(onReceiveMessage(notification:)), name: NSNotification.Name(rawValue: "onReceiveChat"), object: nil)
+    }
+    
+    @objc func onReceiveMessage(notification: NSNotification) {
+        let data:[AnyHashable : Any] = notification.userInfo!
+        guard let dataMessage = data["message"] as? TMessage else {
+            return
+        }
+        let isUser = User.getData(pin: dataMessage.getBody(key: CoreMessage_TMessageKey.L_PIN)) != nil
+        let chatId = dataMessage.getBody(key: CoreMessage_TMessageKey.CHAT_ID, default_value: "").isEmpty ? dataMessage.getBody(key: CoreMessage_TMessageKey.L_PIN) : dataMessage.getBody(key: CoreMessage_TMessageKey.CHAT_ID, default_value: "")
+        let pin = isUser ? dataMessage.getBody(key: CoreMessage_TMessageKey.F_PIN) : chatId
+        if let _ = chats.firstIndex(of: Chat(pin: pin)) {
+            getChats {
+                DispatchQueue.main.async {
+                    self.historyTableView.reloadData()
+                }
+            }
+        } else {
+            getChats {
+                DispatchQueue.main.async {
+                    self.historyTableView.reloadData()
+                }
+            }
+        }
     }
     
     func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@@ -82,13 +106,16 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
     
     func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = tableView.dequeueReusableCell(withIdentifier: "historyBroadcastCell", for: indexPath)
+        historyTableView.separatorStyle = .singleLine
+        cell.textLabel!.text = ""
+        cell.selectionStyle = .gray
         let content = cell.contentView
         if content.subviews.count > 0 {
             content.subviews.forEach { $0.removeFromSuperview() }
         }
         if chats.count == 0 {
             historyTableView.separatorStyle = .none
-            cell.textLabel!.text = "No broadcast history"
+            cell.textLabel!.text = "No History".localized()
             cell.textLabel?.font = UIFont.systemFont(ofSize: 10)
             cell.selectionStyle = .none
             cell.textLabel?.textAlignment = .center
@@ -114,14 +141,11 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
                     imageView.image = UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
                 }
             } else {
-                getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_ball" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, completion: { result, isDownloaded, image in
+                getImage(name: data.profile, placeholderImage: UIImage(named: data.pin == "-999" ? "pb_ball" : "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, image in
                     imageView.image = image
                     if !result {
                         imageView.tintColor = .mainColor
                     }
-                    if isDownloaded {
-                        tableView.reloadRows(at: [indexPath], with: .none)
-                    }
                 })
             }
             let titleView = UILabel()
@@ -203,7 +227,7 @@ class HistoryBroadcastViewController: UIViewController, UITableViewDelegate, UIT
         DispatchQueue.global().async {
             self.chats.removeAll()
             self.chats.append(contentsOf: Chat.getData())
-            self.chats = self.chats.filter({$0.official == "1" && $0.messageScope == "3"})
+            self.chats = self.chats.filter({($0.official == "1" || $0.pin == "-999") && $0.messageScope == "3"})
             completion()
         }
     }

+ 170 - 16
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/HistoryCCViewController.swift

@@ -6,35 +6,56 @@
 //
 
 import UIKit
+import QuickLook
 
-public class HistoryCCViewController: UITableViewController {
+public class HistoryCCViewController: UITableViewController, QLPreviewControllerDataSource {
     
     var data: [[String: Any?]]  = []
     public var isOfficer = false
+    
+    var previewItem: NSURL?
 
     public override func viewDidLoad() {
         super.viewDidLoad()
+        self.title = "History Contact Center".localized()
         
-        if isOfficer {
-            navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapExit(sender:)))
+        let randomInt = Int.random(in: 5..<11)
+        let image = UIImage(named: "lbackground_\(randomInt)")
+        if image != nil {
+            self.view.backgroundColor = UIColor.init(patternImage: image!)
         }
-        self.title = "History Contact Center".localized()
         
     }
     
-    @objc func didTapExit(sender: Any) {
-        self.dismiss(animated: true, completion: nil)
-    }
-    
     public override func viewWillAppear(_ animated: Bool) {
         getData()
     }
     
     public override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+        if data.count == 0 {
+            return 1
+        }
         return data.count
     }
     
     public override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        if data.count == 0 {
+            let cellNoData = UITableViewCell()
+            cellNoData.backgroundColor = .clear
+            cellNoData.selectionStyle = .none
+            let contentData = cellNoData.contentView
+            let viewContainer = UIView()
+            contentData.addSubview(viewContainer)
+            viewContainer.anchor(top: contentData.topAnchor, left: contentData.leftAnchor, bottom: contentData.bottomAnchor, right: contentData.rightAnchor)
+            
+            let textNoData = UILabel()
+            viewContainer.addSubview(textNoData)
+            textNoData.anchor(top: viewContainer.topAnchor, left:viewContainer.leftAnchor, right:viewContainer.rightAnchor, paddingTop: 20.0)
+            textNoData.textAlignment = .center
+            textNoData.text = "No call center history".localized()
+            textNoData.font = .systemFont(ofSize: 14)
+            return cellNoData
+        }
         let cell = tableView.dequeueReusableCell(withIdentifier: "cellHistoryCC", for: indexPath) as! CellMyHistory
         let dataOfficer = getDataProfile(f_pin: data[indexPath.row]["officer"] as! String)
         let dataRequester = getDataProfile(f_pin: data[indexPath.row]["requester"] as! String)
@@ -47,11 +68,11 @@ public class HistoryCCViewController: UITableViewController {
             }
             if !(dataOfficer["image"]!).isEmpty || !(dataRequester["image"]!).isEmpty {
                 if isOfficer {
-                    getImage(name: dataRequester["image"]!, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true) { result, isDownloaded, image in
+                    getImage(name: dataRequester["image"]!, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                         cell.imageOfficer.image = image
                     }
                 } else {
-                    getImage(name: dataOfficer["image"]!, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true) { result, isDownloaded, image in
+                    getImage(name: dataOfficer["image"]!, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                         cell.imageOfficer.image = image
                     }
                 }
@@ -77,7 +98,7 @@ public class HistoryCCViewController: UITableViewController {
         }
         cell.labelComplaintId.text = data[indexPath.row]["complaint_id"] as? String
         let stringDate = data[indexPath.row]["date_start"] as! String
-        let date = Date(milliseconds: Int64(stringDate)!)
+        let date = Date(milliseconds: Int64(stringDate) ?? 0)
         let formatter = DateFormatter()
         formatter.dateFormat = "dd/MM/yyyy HH:mm"
         formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
@@ -89,6 +110,9 @@ public class HistoryCCViewController: UITableViewController {
         
         cell.buttonEditor.addTarget(self, action: #selector(buttonClicked(sender:)), for: .touchUpInside)
         cell.buttonEditor.tag = indexPath.row
+        
+        cell.buttonPDF.addTarget(self, action: #selector(buttonPDFClicked(sender:)), for: .touchUpInside)
+        cell.buttonPDF.tag = indexPath.row
         return cell
     }
     
@@ -99,15 +123,15 @@ public class HistoryCCViewController: UITableViewController {
     func getData() {
         Database.shared.database?.inTransaction({ fmdb, rollback in
             var data: [[String: Any?]] = []
-            if let cursorHistory = Database.shared.getRecords(fmdb: fmdb, query: "SELECT * FROM CALL_CENTER_HISTORY order by time desc") {
+            if let cursorHistory = Database.shared.getRecords(fmdb: fmdb, query: "SELECT type, f_pin, complaint_id, time, time_end, requester FROM CALL_CENTER_HISTORY order by time desc") {
                 while cursorHistory.next() {
                     var row: [String: Any?] = [:]
                     row["type"] = cursorHistory.string(forColumnIndex: 0)
-                    row["officer"] = cursorHistory.string(forColumnIndex: 4)
-                    row["complaint_id"] = cursorHistory.string(forColumnIndex: 7)
+                    row["officer"] = cursorHistory.string(forColumnIndex: 1)
+                    row["complaint_id"] = cursorHistory.string(forColumnIndex: 2)
                     row["date_start"] = cursorHistory.string(forColumnIndex: 3)
-                    row["date_end"] = cursorHistory.string(forColumnIndex: 6)
-                    row["requester"] = cursorHistory.string(forColumnIndex: 9)
+                    row["date_end"] = cursorHistory.string(forColumnIndex: 4)
+                    row["requester"] = cursorHistory.string(forColumnIndex: 5)
                     data.append(row)
                 }
                 cursorHistory.close()
@@ -134,6 +158,135 @@ public class HistoryCCViewController: UITableViewController {
         editorGroupVC.complaintId = data[sender.tag]["complaint_id"] as! String
         navigationController?.show(editorGroupVC, sender: nil)
     }
+    
+    @objc func buttonPDFClicked(sender: UIButton) {
+        var dataMessages: [[String: Any?]] = []
+        Database.shared.database?.inTransaction({ (fmdb, rollback) in
+            let query = "SELECT f_pin, l_pin, message_text, audio_id, video_id, image_id, thumb_id, file_id FROM MESSAGE where call_center_id='\(data[sender.tag]["complaint_id"] as! String)' order by server_date asc"
+            if let cursorData = Database.shared.getRecords(fmdb: fmdb, query: query) {
+                while cursorData.next() {
+                    var row: [String: Any?] = [:]
+                    row["f_pin"] = cursorData.string(forColumnIndex: 0)
+                    row["l_pin"] = cursorData.string(forColumnIndex: 1)
+                    row["message_text"] = cursorData.string(forColumnIndex: 2)
+                    row["audio_id"] = cursorData.string(forColumnIndex: 3)
+                    row["video_id"] = cursorData.string(forColumnIndex: 4)
+                    row["image_id"] = cursorData.string(forColumnIndex: 5)
+                    row["thumb_id"] = cursorData.string(forColumnIndex: 6)
+                    row["file_id"] = cursorData.string(forColumnIndex: 7)
+                    dataMessages.append(row)
+                }
+                cursorData.close()
+            }
+        })
+        let dataOfficer = getDataProfile(f_pin: data[sender.tag]["officer"] as! String)
+        let dataRequester = getDataProfile(f_pin: data[sender.tag]["requester"] as! String)
+        let stringDate = data[sender.tag]["date_start"] as! String
+        let date = Date(milliseconds: Int64(stringDate) ?? 0)
+        let formatter = DateFormatter()
+        formatter.dateFormat = "dd MMMM yyyy HH:mm"
+        formatter.locale = NSLocale(localeIdentifier: "id") as Locale?
+        var textPDF = """
+        <html>
+            <head>
+                <style>
+                h1 {
+                  text-align: center;
+                }
+                .column {
+                  float: left;
+                  width: 50%;
+                }
+                .row:after {
+                  content: "";
+                  display: table;
+                  clear: both;
+                }
+                .customh3 {
+                    text-align: right;
+                }
+                </style>
+            </head>
+            <body>
+                <h1>Call Center History</h1>
+                <div class="row">
+                  <div class="column">
+                    <h3>\(dataOfficer["name"]!) (Officer)</h3>
+                    <h3>\(dataRequester["name"]!) (Requester)<h3>
+                  </div>
+                  <div class="column">
+                    <h3 class="customh3">\(formatter.string(from: date as Date))</h3>
+                  </div>
+                </div>
+        """
+        for i in 0..<dataMessages.count {
+            let name = getDataProfile(f_pin: dataMessages[i]["f_pin"] as! String)["name"]!
+            textPDF = textPDF + """
+            <p>\(name) : \(dataMessages[i]["message_text"]!!)<p>
+            """
+        }
+        textPDF = textPDF + """
+                    </body>
+                </html>
+        """
+        convertToPdfFileAndShare(textMessage: textPDF)
+    }
+    
+    func convertToPdfFileAndShare(textMessage: String){
+        
+        let fmt = UIMarkupTextPrintFormatter(markupText: textMessage)
+        
+        // 2. Assign print formatter to UIPrintPageRenderer
+        let render = UIPrintPageRenderer()
+        render.addPrintFormatter(fmt, startingAtPageAt: 0)
+        
+        // 3. Assign paperRect and printableRect
+        let page = CGRect(x: 0, y: 0, width: 595.2, height: 841.8) // A4, 72 dpi
+        render.setValue(page, forKey: "paperRect")
+        render.setValue(page, forKey: "printableRect")
+        
+        // 4. Create PDF context and draw
+        let pdfData = NSMutableData()
+        UIGraphicsBeginPDFContextToData(pdfData, .zero, nil)
+        
+        for i in 0..<render.numberOfPages {
+            UIGraphicsBeginPDFPage();
+            render.drawPage(at: i, in: UIGraphicsGetPDFContextBounds())
+        }
+        
+        UIGraphicsEndPDFContext();
+        
+        // 5. Save PDF file
+        guard let outputURL = try? FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("ContactCenter-\(Date().millisecondsSince1970)").appendingPathExtension("pdf")
+            else { fatalError("Destination URL not created") }
+        
+        pdfData.write(to: outputURL, atomically: true)
+        print("open \(outputURL.path)")
+        
+        if FileManager.default.fileExists(atPath: outputURL.path){
+            
+            let url = URL(fileURLWithPath: outputURL.path)
+            self.previewItem = url as NSURL
+            let previewController = QLPreviewController()
+            let rightBarButton = UIBarButtonItem()
+            previewController.navigationItem.rightBarButtonItem = rightBarButton
+            previewController.dataSource = self
+            previewController.modalPresentationStyle = .custom
+            self.present(previewController, animated: true, completion: nil)
+        }
+        else {
+            print("document was not found")
+        }
+        
+    }
+    
+    public func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
+        return 1
+    }
+    
+    public func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
+        return self.previewItem!
+    }
 
 }
 
@@ -146,4 +299,5 @@ class CellMyHistory: UITableViewCell {
     @IBOutlet weak var labelComplaintId: UILabel!
     @IBOutlet weak var labelDate: UILabel!
     @IBOutlet weak var buttonEditor: UIButton!
+    @IBOutlet weak var buttonPDF: UIButton!
 }

+ 4 - 4
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ImageVideoPicker.swift

@@ -40,24 +40,24 @@ open class ImageVideoPicker: NSObject {
         if (sourceView == .imageAlbum) {
             self.pickerController.mediaTypes = ["public.image"]
             self.pickerController.sourceType = .savedPhotosAlbum
-            self.pickerController.modalPresentationStyle = .fullScreen
+            self.pickerController.modalPresentationStyle = .custom
             self.presentationController?.present(self.pickerController, animated: true)
         } else if (sourceView == .videoAlbum) {
             self.pickerController.mediaTypes = ["public.movie"]
             self.pickerController.sourceType = .savedPhotosAlbum
             self.pickerController.videoQuality = .typeHigh
-            self.pickerController.modalPresentationStyle = .fullScreen
+            self.pickerController.modalPresentationStyle = .custom
             self.presentationController?.present(self.pickerController, animated: true)
         } else if (sourceView == .imageCamera) {
             self.pickerController.mediaTypes = ["public.image"]
             self.pickerController.sourceType = .camera
-            self.pickerController.modalPresentationStyle = .fullScreen
+            self.pickerController.modalPresentationStyle = .custom
             self.presentationController?.present(self.pickerController, animated: true)
         } else if (sourceView == .videoCamera) {
             self.pickerController.mediaTypes = ["public.movie"]
             self.pickerController.sourceType = .camera
             self.pickerController.videoQuality = .typeHigh
-            self.pickerController.modalPresentationStyle = .fullScreen
+            self.pickerController.modalPresentationStyle = .custom
             self.presentationController?.present(self.pickerController, animated: true)
         }
     }

+ 210 - 28
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ProfileViewController.swift

@@ -7,6 +7,7 @@
 
 import UIKit
 import NotificationBannerSwift
+import nuSDKService
 
 public class ProfileViewController: UITableViewController {
     
@@ -24,6 +25,8 @@ public class ProfileViewController: UITableViewController {
     @IBOutlet weak var buttonEditPass: UIButton!
     @IBOutlet weak var switchAcceptCall: UISwitch!
     @IBOutlet weak var buttonHistoryCC: UIButton!
+    @IBOutlet weak var viewFriend: UIView!
+    @IBOutlet weak var countFriend: UILabel!
     
     private var imageVideoPicker : ImageVideoPicker!
     
@@ -33,23 +36,25 @@ public class ProfileViewController: UITableViewController {
         case invite
     }
     
-    public var user: User?
+    var user: User?
     
     public var data: String = ""
     
     public var flag: Flag = Flag.friend
     
-    public var name: String = ""
+    var name: String = ""
     
-    public var picture: String = ""
+    var picture: String = ""
     
-    public var checkReadMessage: (() -> ())?
+    var checkReadMessage: (() -> ())?
     
     public var isDismiss: (() -> ())?
     
     public var dismissImage: ((UIImage, String) -> ())?
     
-    public var fromRootViewController = false
+    var fromRootViewController = false
+    
+    var isBNI = false
     
     private func reload() {
         if let user = self.user {
@@ -64,14 +69,36 @@ public class ProfileViewController: UITableViewController {
                     guard let user = user else {
                         return
                     }
+                    if let me = UserDefaults.standard.string(forKey: "me"), me == self.data || self.flag == Flag.me {
+                        Database.shared.database?.inTransaction({ fmdb, rollback in
+                            let idMe = UserDefaults.standard.string(forKey: "me")!
+                            if let cursorCount = Database.shared.getRecords(fmdb: fmdb, query: "select COUNT(*) from BUDDY where f_pin <> '\(idMe)' "), cursorCount.next() {
+                                let count = cursorCount.string(forColumnIndex: 0)!
+                                self.countFriend.text = count + " " + "Friends".localized()
+                                self.countFriend.font = .systemFont(ofSize: 12)
+                                self.viewFriend.layer.cornerRadius = 5.0
+                                self.viewFriend.clipsToBounds = true
+                                self.viewFriend.isHidden = false
+                                
+                                self.viewFriend.isUserInteractionEnabled = true
+                                self.viewFriend.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.friendsTapped)))
+                                cursorCount.close()
+                            }
+                        })
+                    }
                     if user.userType == "23" || user.userType == "24" {
                         self.viewUserType.layer.cornerRadius = 5.0
                         self.viewUserType.clipsToBounds = true
                         self.viewUserType.isHidden = false
                         if user.userType == "24" {
+                            let dataCategory = CategoryCC.getDataFromServiceId(service_id: user.ex_offmp!)
                             self.imageUserType.image = UIImage(named: "pb_call_center", in: Bundle.resourceBundle(for: Nexilis.self), with: nil)
-                            self.labelUserType.text = "Customer Service".localized()
-                            self.buttonHistoryCC.isHidden = true
+                            if dataCategory != nil {
+                                self.labelUserType.text = "Call Center (\(dataCategory!.service_name))".localized()
+                            } else {
+                                self.labelUserType.text = "Call Center".localized()
+                            }
+//                            self.buttonHistoryCC.isHidden = true
                         }
                     }
                     self.title = "\(user.firstName) \(user.lastName)"
@@ -87,15 +114,23 @@ public class ProfileViewController: UITableViewController {
         DispatchQueue.global().async {
             var r: User?
             Database.shared.database?.inTransaction({ fmdb, rollback in
-                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select FIRST_NAME, LAST_NAME, IMAGE_ID, USER_TYPE from BUDDY where F_PIN = '\(self.data)'"), cursor.next() {
+                if let cursor = Database.shared.getRecords(fmdb: fmdb, query: "select FIRST_NAME, LAST_NAME, IMAGE_ID, USER_TYPE, ex_offmp from BUDDY where F_PIN = '\(self.data)'"), cursor.next() {
                     r = User(pin: self.data,
                              firstName: cursor.string(forColumnIndex: 0) ?? "",
                              lastName: cursor.string(forColumnIndex: 1) ?? "",
                              thumb: cursor.string(forColumnIndex: 2) ?? "",
-                             userType: cursor.string(forColumnIndex: 3) ?? "")
+                             userType: cursor.string(forColumnIndex: 3) ?? "",
+                            ex_offmp: cursor.string(forColumnIndex: 4) ?? "")
                     //
                     cursor.close()
                 }
+                let idMe = UserDefaults.standard.string(forKey: "me")!
+                if let cursorCount = Database.shared.getRecords(fmdb: fmdb, query: "select COUNT(*) from BUDDY where f_pin <> '\(idMe)' "), cursorCount.next() {
+                    DispatchQueue.main.async {
+                        self.countFriend.text = cursorCount.string(forColumnIndex: 0) ?? "" + " " + "Friends".localized()
+                    }
+                    cursorCount.close()
+                }
             })
             completion(r)
         }
@@ -113,16 +148,33 @@ public class ProfileViewController: UITableViewController {
         profile.circle()
         profile.contentMode = .scaleAspectFill
         
+        profile.isUserInteractionEnabled = true
+        profile.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(profileTapped)))
+        
+        let randomInt = Int.random(in: 5..<11)
+        let image = UIImage(named: "lbackground_\(randomInt)")
+        if image != nil {
+            self.view.backgroundColor = UIColor.init(patternImage: image!)
+        }
+        
         if fromRootViewController {
             navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapExit(sender:)))
         }
-        
+        let myData = User.getData(pin: self.data)
         if let me = UserDefaults.standard.string(forKey: "me"), me == data || flag == Flag.me {
             buttonGroup.removeFromSuperview()
             navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(didTapEdit(sender:)))
             imageVideoPicker = ImageVideoPicker(presentationController: self, delegate: self)
             buttonEditPass.addTarget(self, action: #selector(editPassword(sender:)), for: .touchUpInside)
             buttonHistoryCC.addTarget(self, action: #selector(historyCC(sender:)), for: .touchUpInside)
+            if myData?.privacy_flag == "1" {
+                switchPrivateAccount.setOn(true, animated: false)
+            }
+            if myData?.offline_mode == "1" {
+                switchAcceptCall.setOn(false, animated: false)
+            }
+            switchPrivateAccount.addTarget(self, action: #selector(privateAccountSwitch), for: .valueChanged)
+            switchAcceptCall.addTarget(self, action: #selector(acceptCallSwitch), for: .valueChanged)
             reload()
         } else if flag == Flag.invite {
             navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(didTapAdd(sender:)))
@@ -130,18 +182,104 @@ public class ProfileViewController: UITableViewController {
             video.isEnabled = false
             message.isEnabled = false
             myViewGroup.removeFromSuperview()
+            buttonGroup.removeFromSuperview()
             title = name
             profile.setImage(name: picture)
         } else if flag == Flag.friend {
             navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "person.crop.circle.badge.xmark"), style: .plain, target: self, action: #selector(didTapUnfriend(sender:)))
-            call.addTarget(self, action: #selector(call(sender:)), for: .touchUpInside)
-            video.addTarget(self, action: #selector(video(sender:)), for: .touchUpInside)
-            message.addTarget(self, action: #selector(chat(sender:)), for: .touchUpInside)
+            if !isBNI {
+                call.addTarget(self, action: #selector(call(sender:)), for: .touchUpInside)
+                video.addTarget(self, action: #selector(video(sender:)), for: .touchUpInside)
+                message.addTarget(self, action: #selector(chat(sender:)), for: .touchUpInside)
+            } else {
+                call.isEnabled = false
+                video.isEnabled = false
+                message.isEnabled = false
+                buttonGroup.removeFromSuperview()
+            }
             myViewGroup.removeFromSuperview()
             reload()
         }
     }
     
+    @objc func acceptCallSwitch(mySwitch: UISwitch) {
+        let value = mySwitch.isOn
+        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            self.switchPrivateAccount.setOn(!value, animated: true)
+            return
+        }
+        DispatchQueue.global().async {
+            let tMessage = CoreMessage_TMessageBank.getChangePersonInfo_New(p_f_pin: self.data)
+            tMessage.mBodies[CoreMessage_TMessageKey.OFFLINE_MODE] = value ? "0" : "1"
+            if let resp = Nexilis.writeAndWait(message: tMessage) {
+                if resp.isOk() {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                            "offline_mode" : value ? "0" : "1"
+                        ], _where: "f_pin = '\(self.data)'")
+                    })
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                        self.switchPrivateAccount.setOn(!value, animated: true)
+                    }
+                }
+            }
+        }
+    }
+    
+    @objc func privateAccountSwitch(mySwitch: UISwitch) {
+        let value = mySwitch.isOn
+        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            self.switchPrivateAccount.setOn(!value, animated: true)
+            return
+        }
+        DispatchQueue.global().async {
+            let tMessage = CoreMessage_TMessageBank.getChangePersonInfo_New(p_f_pin: self.data)
+            tMessage.mBodies[CoreMessage_TMessageKey.PRIVACY_FLAG] = value ? "1" : "0"
+            if let resp = Nexilis.writeAndWait(message: tMessage) {
+                if resp.isOk() {
+                    Database.shared.database?.inTransaction({ (fmdb, rollback) in
+                        _ = Database.shared.updateRecord(fmdb: fmdb, table: "BUDDY", cvalues: [
+                            "privacy_flag" : value ? "1" : "0"
+                        ], _where: "f_pin = '\(self.data)'")
+                    })
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                        self.switchPrivateAccount.setOn(!value, animated: true)
+                    }
+                }
+            }
+        }
+    }
+    
     @objc func editPassword(sender: Any) {
         let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "changePWD") as! ChangePasswordViewController
         navigationController?.show(controller, sender: nil)
@@ -149,11 +287,27 @@ public class ProfileViewController: UITableViewController {
     
     @objc func historyCC(sender: Any) {
         let controller = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "myHistoryCC") as! HistoryCCViewController
-        controller.isOfficer = false
+        if user?.userType == "24" {
+            controller.isOfficer = true
+        } else {
+            controller.isOfficer = false
+        }
         navigationController?.show(controller, sender: nil)
     }
     
     @objc func call(sender: Any) {
+        let myData = User.getData(pin: self.data)
+        if myData?.ex_block == "1" || myData?.ex_block == "-1" {
+            var title = "You blocked this user".localized()
+            if myData?.ex_block == "-1" {
+                title = "You have been blocked by this user".localized()
+            }
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: title, subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            return
+        }
         let controller = QmeraAudioViewController()
         controller.user = user
         controller.isOutgoing = true
@@ -162,6 +316,18 @@ public class ProfileViewController: UITableViewController {
     }
     
     @objc func video(sender: Any) {
+        let myData = User.getData(pin: self.data)
+        if myData?.ex_block == "1" || myData?.ex_block == "-1" {
+            var title = "You blocked this user".localized()
+            if myData?.ex_block == "-1" {
+                title = "You have been blocked by this user".localized()
+            }
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: title, subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+            return
+        }
         if let user = user {
             let videoVC = AppStoryBoard.Palio.instance.instantiateViewController(withIdentifier: "videoVCQmera") as! QmeraVideoViewController
             videoVC.fPin = user.pin
@@ -240,9 +406,10 @@ public class ProfileViewController: UITableViewController {
                         DispatchQueue.main.async {
                             self.profile.image = UIImage(systemName: "person.circle.fill")!
                             self.profile.backgroundColor = .white
+                            self.user?.thumb = ""
                             let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
                             imageView.tintColor = .white
-                            let banner = FloatingNotificationBanner(title: "Successfully removed image".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+                            let banner = FloatingNotificationBanner(title: "Successfully removed image".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                             banner.show()
                             self.dismissImage?(UIImage(systemName: "person.circle.fill")!, "")
                         }
@@ -275,7 +442,7 @@ public class ProfileViewController: UITableViewController {
                 } else {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Server busy, please try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Server busy, please try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                 }
             }
@@ -302,7 +469,7 @@ public class ProfileViewController: UITableViewController {
                                 return
                             }
                         })
-                        if self.previousViewController is GroupDetailViewController {
+                        if self.previousViewController is GroupDetailViewController || self.isBNI {
                             self.isDismiss?()
                             self.navigationController?.popViewController(animated: true)
                         } else {
@@ -311,7 +478,7 @@ public class ProfileViewController: UITableViewController {
                     } else {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Server busy, please try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Server busy, please try again later".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                     }
                 }
@@ -342,27 +509,32 @@ public class ProfileViewController: UITableViewController {
             }
             let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
             imageView.tintColor = .white
-            let banner = FloatingNotificationBanner(title: "Successfully changed named".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+            let banner = FloatingNotificationBanner(title: "Successfully changed named".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
             banner.show()
         }
         navigationItem.backButtonTitle = ""
         navigationController?.show(controller, sender: nil)
     }
     
-    public override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-        if indexPath.section == 0 {
-            if let me = UserDefaults.standard.string(forKey: "me"), me == data || flag == Flag.me {
-                didTapProfile()
-            }
+    @objc func profileTapped() {
+        if let me = UserDefaults.standard.string(forKey: "me"), me == data || flag == Flag.me {
+            didTapProfile()
+        }
+    }
+    
+    @objc func friendsTapped() {
+        if let me = UserDefaults.standard.string(forKey: "me"), me == data || flag == Flag.me {
+            let controller = QmeraCallContactViewController()
+            controller.modalPresentationStyle = .custom
+            controller.isInviteCC = true
+            controller.listFriends = true
+            show(controller, sender: nil)
         }
     }
     
     public override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
         if indexPath.section == 1 {
             if let me = UserDefaults.standard.string(forKey: "me"), me == data || flag == Flag.me {
-                if user?.userType == "24" {
-                    return 120
-                }
                 return 170
             }
             return 56
@@ -398,8 +570,9 @@ extension ProfileViewController: ImageVideoPickerDelegate {
                             DispatchQueue.main.async {
                                 self.profile.image = image
                                 let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                                self.user?.thumb = fileDir.lastPathComponent
                                 imageView.tintColor = .white
-                                let banner = FloatingNotificationBanner(title: "Successfully changed image".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .top)
+                                let banner = FloatingNotificationBanner(title: "Successfully changed image".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
                                 banner.show()
                                 self.dismissImage?(image, fileDir.lastPathComponent)
                             }
@@ -411,3 +584,12 @@ extension ProfileViewController: ImageVideoPickerDelegate {
     }
     
 }
+
+//let auto = UserDefaults.standard.bool(forKey: "autoDownload")
+//if auto {
+//    DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
+//        let objectTapAuto = ObjectGesture()
+//        objectTapAuto.image_id = imageChat
+//        self.contentMessageTapped(objectTap)
+//    })
+//}

+ 122 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/QRScannerView.swift

@@ -0,0 +1,122 @@
+//
+//  QRScannerView.swift
+//  Gaspol
+//
+//  Created by Qindi on 19/05/22.
+//
+
+import Foundation
+import UIKit
+import AVFoundation
+
+/// Delegate callback for the QRScannerView.
+protocol QRScannerViewDelegate: AnyObject {
+    func qrScanningDidFail()
+    func qrScanningSucceededWithCode(_ str: String?)
+    func qrScanningDidStop()
+}
+
+class QRScannerView: UIView {
+    
+    weak var delegate: QRScannerViewDelegate?
+    
+    /// capture settion which allows us to start and stop scanning.
+    var captureSession: AVCaptureSession?
+    
+    // Init methods..
+    required init?(coder aDecoder: NSCoder) {
+        super.init(coder: aDecoder)
+        doInitialSetup()
+    }
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        doInitialSetup()
+    }
+    
+    //MARK: overriding the layerClass to return `AVCaptureVideoPreviewLayer`.
+    override class var layerClass: AnyClass  {
+        return AVCaptureVideoPreviewLayer.self
+    }
+    override var layer: AVCaptureVideoPreviewLayer {
+        return super.layer as! AVCaptureVideoPreviewLayer
+    }
+}
+extension QRScannerView {
+    
+    var isRunning: Bool {
+        return captureSession?.isRunning ?? false
+    }
+    
+    func startScanning() {
+       captureSession?.startRunning()
+    }
+    
+    func stopScanning() {
+        captureSession?.stopRunning()
+        delegate?.qrScanningDidStop()
+    }
+    
+    /// Does the initial setup for captureSession
+    private func doInitialSetup() {
+        clipsToBounds = true
+        captureSession = AVCaptureSession()
+        
+        guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
+        let videoInput: AVCaptureDeviceInput
+        do {
+            videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
+        } catch let error {
+            print(error)
+            return
+        }
+        
+        if (captureSession?.canAddInput(videoInput) ?? false) {
+            captureSession?.addInput(videoInput)
+        } else {
+            scanningDidFail()
+            return
+        }
+        
+        let metadataOutput = AVCaptureMetadataOutput()
+        
+        if (captureSession?.canAddOutput(metadataOutput) ?? false) {
+            captureSession?.addOutput(metadataOutput)
+            
+            metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
+            metadataOutput.metadataObjectTypes = [.qr, .ean8, .ean13, .pdf417]
+        } else {
+            scanningDidFail()
+            return
+        }
+        
+        self.layer.session = captureSession
+        self.layer.videoGravity = .resizeAspectFill
+        
+        captureSession?.startRunning()
+    }
+    func scanningDidFail() {
+        delegate?.qrScanningDidFail()
+        captureSession = nil
+    }
+    
+    func found(code: String) {
+        delegate?.qrScanningSucceededWithCode(code)
+    }
+    
+}
+
+extension QRScannerView: AVCaptureMetadataOutputObjectsDelegate {
+    func metadataOutput(_ output: AVCaptureMetadataOutput,
+                        didOutput metadataObjects: [AVMetadataObject],
+                        from connection: AVCaptureConnection) {
+        stopScanning()
+        
+        if let metadataObject = metadataObjects.first {
+            guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
+            guard let stringValue = readableObject.stringValue else { return }
+            AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
+            found(code: stringValue)
+        }
+    }
+    
+}

+ 107 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/ScannerViewController.swift

@@ -0,0 +1,107 @@
+//
+//  GaspolScannerViewController.swift
+//  Gaspol
+//
+//  Created by Qindi on 19/05/22.
+//
+
+import Foundation
+import UIKit
+import NotificationBannerSwift
+import nuSDKService
+
+class ScannerViewController: UIViewController, QRScannerViewDelegate {
+    
+    var scannerView: QRScannerView = {
+        let scannerView = QRScannerView()
+        return scannerView
+    }()
+    
+    var titleInfo: UILabel = {
+        let titleInfo = UILabel()
+        titleInfo.text = "Scan the QR login code on your web browser".localized()
+        titleInfo.font = .boldSystemFont(ofSize: 16.0)
+        titleInfo.numberOfLines = 0
+        titleInfo.textAlignment = .center
+        return titleInfo
+    }()
+    
+    override func viewDidDisappear(_ animated: Bool) {
+        if scannerView.isRunning {
+            scannerView.stopScanning()
+        }
+        self.navigationController?.navigationBar.topItem?.title = nil
+    }
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        let randomInt = Int.random(in: 5..<11)
+        let image = UIImage(named: "lbackground_\(randomInt)")
+        if image != nil {
+            self.view.backgroundColor = UIColor.init(patternImage: image!)
+        } else {
+            self.view.backgroundColor = .white
+        }
+
+        self.title = "Scan QR Code".localized()
+        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel".localized(), style: .plain, target: self, action: #selector(cancel(sender:)))
+        
+        self.view.addSubview(scannerView)
+        self.view.addSubview(titleInfo)
+        scannerView.anchor(left: self.view.leftAnchor, right: self.view.rightAnchor, centerX: self.view.centerXAnchor, centerY: self.view.centerYAnchor, height: self.view.bounds.height / 2)
+        titleInfo.anchor(left: self.view.leftAnchor, bottom: scannerView.topAnchor, right: self.view.rightAnchor, paddingBottom: 10.0, centerX: self.view.centerXAnchor)
+        
+        scannerView.delegate = self
+    }
+    
+    @objc func cancel(sender: Any) {
+        navigationController?.dismiss(animated: true, completion: nil)
+    }
+    
+    override func viewDidAppear(_ animated: Bool) {
+        scannerView.startScanning()
+    }
+    
+    func qrScanningDidFail() {
+        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+        imageView.tintColor = .white
+        let banner = FloatingNotificationBanner(title: "Scanning Failed. Please try again".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+        banner.show()
+        cancel(sender: "")
+    }
+    
+    func qrScanningSucceededWithCode(_ str: String?) {
+        if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+            let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+            imageView.tintColor = .white
+            let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+            banner.show()
+        } else {
+            DispatchQueue.global().async {
+                if let response = Nexilis.writeSync(message: CoreMessage_TMessageBank.getWebLoginQRCode(f_qrcode: str ?? "")) {
+                    if response.isOk() {
+                        DispatchQueue.main.async {
+                            let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                            imageView.tintColor = .white
+                            let banner = FloatingNotificationBanner(title: "Successfully Login".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                            banner.show()
+                        }
+                    }
+                } else {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                }
+            }
+        }
+        cancel(sender: "")
+    }
+    
+    func qrScanningDidStop() {
+        print("SCANNED STOP")
+    }
+}

+ 408 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/SetOfficerBNI.swift

@@ -0,0 +1,408 @@
+//
+//  SetOfficerBNI.swift
+//  NexilisLite
+//
+//  Created by Qindi on 20/06/22.
+//
+
+import UIKit
+import NotificationBannerSwift
+import nuSDKService
+
+class SetOfficerBNI: UIViewController {
+    
+    var nextData: [Int: [CategoryCC]] = [:]
+    var chosenData: [CategoryCC] = []
+    var dataSecondLayer: [String] = ["No".localized(),"Yes".localized()]
+    var chosenSecondLayer: String = "No".localized()
+    var chosenWorkingArea: WorkingArea?
+    let subContainerView = UIView()
+    let titleSetOfficer = UILabel()
+    var showPickerInt = 0
+    var isSecondLayerChooser = false
+    var isWorkingArea = false
+    var paddingTopFinishButton = NSLayoutConstraint()
+    var paddingBottomSLButton = NSLayoutConstraint()
+    var f_pin = ""
+    var name = ""
+    let buttonSet = UIButton()
+    let secondLayerButton = UIView()
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        
+        getInisialData()
+
+        view.backgroundColor = .black.withAlphaComponent(0.3)
+        
+        let containerView = UIView()
+        view.addSubview(containerView)
+        containerView.anchor(centerX: view.centerXAnchor, centerY: view.centerYAnchor, width: view.bounds.width - 40, minHeight: 100, maxHeight: view.bounds.height - 100)
+        containerView.backgroundColor = .white.withAlphaComponent(0.9)
+        containerView.layer.cornerRadius = 15.0
+        containerView.clipsToBounds = true
+        
+        subContainerView.backgroundColor = .clear
+        containerView.addSubview(subContainerView)
+        subContainerView.anchor(top: containerView.topAnchor, left: containerView.leftAnchor, bottom: containerView.bottomAnchor, right: containerView.rightAnchor, paddingTop: 20.0, paddingLeft: 10.0, paddingBottom: 20.0, paddingRight: 10.0)
+        
+        let buttonClose = UIButton(type: .close)
+        buttonClose.frame.size = CGSize(width: 30, height: 30)
+        buttonClose.layer.cornerRadius = 15.0
+        buttonClose.clipsToBounds = true
+        buttonClose.backgroundColor = .secondaryColor.withAlphaComponent(0.5)
+        buttonClose.addTarget(self, action: #selector(close), for: .touchUpInside)
+        containerView.addSubview(buttonClose)
+        buttonClose.anchor(top: containerView.topAnchor, right: containerView.rightAnchor, width: 30, height: 30)
+        
+        titleSetOfficer.font = .systemFont(ofSize: 18, weight: .bold)
+        titleSetOfficer.text = "Set Category Officer".localized()
+        titleSetOfficer.textAlignment = .center
+        subContainerView.addSubview(titleSetOfficer)
+        titleSetOfficer.anchor(top: subContainerView.topAnchor, left: subContainerView.leftAnchor, right: subContainerView.rightAnchor)
+        
+        makeFinish()
+        makeButtonSecondLayer()
+        makeButtonType()
+        makeButtonSubType(level: 0)
+    }
+    
+    private func getInisialData() {
+        let data = CategoryCC.getDatafromParent(parent: CategoryCC.default_parent)
+        nextData[0] = data
+        chosenData.append(nextData[0]![0])
+    }
+    
+    @objc func close() {
+        self.dismiss(animated: true)
+    }
+    
+    func makeButtonWorkingArea() {
+        let workingAreaButton = UIView()
+        workingAreaButton.backgroundColor = .white
+        workingAreaButton.isUserInteractionEnabled = true
+        subContainerView.insertSubview(workingAreaButton, belowSubview: secondLayerButton)
+        workingAreaButton.anchor(left: subContainerView.leftAnchor, bottom: buttonSet.topAnchor, right: subContainerView.rightAnchor, paddingBottom: 10, height: 40)
+        workingAreaButton.layer.cornerRadius = 10
+        workingAreaButton.layer.masksToBounds = true
+        let titleTypeButton = UILabel()
+        titleTypeButton.font = .systemFont(ofSize: 14, weight: .bold)
+        titleTypeButton.text = "Working Area".localized()
+        titleTypeButton.textColor = .gray
+        workingAreaButton.addSubview(titleTypeButton)
+        titleTypeButton.anchor(left: workingAreaButton.leftAnchor, paddingLeft: 10, centerY: workingAreaButton.centerYAnchor)
+        let accessoryType = UIImageView()
+        accessoryType.image = UIImage(systemName: "chevron.right")
+        accessoryType.tintColor = .gray
+        workingAreaButton.addSubview(accessoryType)
+        accessoryType.anchor(right: workingAreaButton.rightAnchor, paddingRight: 10, centerY: workingAreaButton.centerYAnchor)
+        let chosenType = UILabel()
+        chosenType.font = .systemFont(ofSize: 14)
+        chosenType.text = chosenWorkingArea == nil ? "" : chosenWorkingArea!.name
+        chosenType.textColor = .gray
+        workingAreaButton.addSubview(chosenType)
+        workingAreaButton.tag = 0
+        workingAreaButton.restorationIdentifier = "workingArea"
+        chosenType.anchor(right: accessoryType.leftAnchor, paddingLeft: 15, centerY: workingAreaButton.centerYAnchor)
+        let tap = UITapGestureRecognizer(target: self, action: #selector(showWorkingAreaPicker(sender:)))
+        workingAreaButton.addGestureRecognizer(tap)
+    }
+    
+    func makeButtonSecondLayer() {
+        secondLayerButton.backgroundColor = .white
+        secondLayerButton.isUserInteractionEnabled = true
+        subContainerView.addSubview(secondLayerButton)
+        secondLayerButton.anchor(left: subContainerView.leftAnchor, right: subContainerView.rightAnchor, height: 40)
+        paddingBottomSLButton = secondLayerButton.bottomAnchor.constraint(equalTo: buttonSet.topAnchor, constant: -10)
+        paddingBottomSLButton.isActive = true
+        secondLayerButton.layer.cornerRadius = 10
+        secondLayerButton.layer.masksToBounds = true
+        let titleTypeButton = UILabel()
+        titleTypeButton.font = .systemFont(ofSize: 14, weight: .bold)
+        titleTypeButton.text = "Second Layer".localized()
+        titleTypeButton.textColor = .gray
+        secondLayerButton.addSubview(titleTypeButton)
+        titleTypeButton.anchor(left: secondLayerButton.leftAnchor, paddingLeft: 10, centerY: secondLayerButton.centerYAnchor)
+        let accessoryType = UIImageView()
+        accessoryType.image = UIImage(systemName: "chevron.right")
+        accessoryType.tintColor = .gray
+        secondLayerButton.addSubview(accessoryType)
+        accessoryType.anchor(right: secondLayerButton.rightAnchor, paddingRight: 10, centerY: secondLayerButton.centerYAnchor)
+        let chosenType = UILabel()
+        chosenType.font = .systemFont(ofSize: 14)
+        chosenType.text = dataSecondLayer[0]
+        chosenType.textColor = .gray
+        secondLayerButton.addSubview(chosenType)
+        secondLayerButton.tag = 0
+        secondLayerButton.restorationIdentifier = "secondLayer"
+        chosenType.anchor(right: accessoryType.leftAnchor, paddingLeft: 15, centerY: secondLayerButton.centerYAnchor)
+        let tap = UITapGestureRecognizer(target: self, action: #selector(showPicker(sender:)))
+        secondLayerButton.addGestureRecognizer(tap)
+    }
+    
+    func makeButtonType() {
+        let typeButton = UIView()
+        typeButton.backgroundColor = .white
+        typeButton.isUserInteractionEnabled = true
+        subContainerView.addSubview(typeButton)
+        typeButton.anchor(top: titleSetOfficer.bottomAnchor, left: subContainerView.leftAnchor, right: subContainerView.rightAnchor, paddingTop: 10, height: 40)
+        typeButton.layer.cornerRadius = 10
+        typeButton.layer.masksToBounds = true
+        let titleTypeButton = UILabel()
+        titleTypeButton.font = .systemFont(ofSize: 14, weight: .bold)
+        titleTypeButton.text = "Product".localized()
+        titleTypeButton.textColor = .gray
+        typeButton.addSubview(titleTypeButton)
+        titleTypeButton.anchor(left: typeButton.leftAnchor, paddingLeft: 10, centerY: typeButton.centerYAnchor)
+        let accessoryType = UIImageView()
+        accessoryType.image = UIImage(systemName: "chevron.right")
+        accessoryType.tintColor = .gray
+        typeButton.addSubview(accessoryType)
+        accessoryType.anchor(right: typeButton.rightAnchor, paddingRight: 10, centerY: typeButton.centerYAnchor)
+        let chosenType = UILabel()
+        chosenType.font = .systemFont(ofSize: 14)
+        chosenType.text = chosenData[0].service_name
+        chosenType.textColor = .gray
+        typeButton.addSubview(chosenType)
+        typeButton.tag = 0
+        chosenType.anchor(right: accessoryType.leftAnchor, paddingLeft: 15, centerY: typeButton.centerYAnchor)
+        let tap = UITapGestureRecognizer(target: self, action: #selector(showPicker(sender:)))
+        typeButton.addGestureRecognizer(tap)
+    }
+    
+    func makeButtonSubType(level: Int) {
+        let data = CategoryCC.getDatafromParent(parent: chosenData[level].service_id)
+        if data.count != 0 {
+            nextData[level + 1] = data
+            if chosenData.count - 1 == level + 1 {
+                chosenData.remove(at: level + 1)
+                chosenData.insert(nextData[level + 1]![0], at: level + 1)
+            } else {
+                chosenData.append(nextData[level + 1]![0])
+            }
+            let typeButton = UIView()
+            typeButton.backgroundColor = .white
+            typeButton.isUserInteractionEnabled = true
+            subContainerView.addSubview(typeButton)
+            let constPaddingTop = level == 0 ? 55 : 55 + (45 * level)
+            typeButton.anchor(top: titleSetOfficer.bottomAnchor, left: subContainerView.leftAnchor, right: subContainerView.rightAnchor, paddingTop: CGFloat(constPaddingTop), height: 40)
+            typeButton.layer.cornerRadius = 10
+            typeButton.layer.masksToBounds = true
+            let titleTypeButton = UILabel()
+            titleTypeButton.font = .systemFont(ofSize: 14, weight: .bold)
+            titleTypeButton.text = level == 0 ? "Category".localized() :"Sub-Category".localized()
+            titleTypeButton.textColor = .gray
+            typeButton.addSubview(titleTypeButton)
+            titleTypeButton.anchor(left: typeButton.leftAnchor, paddingLeft: 10, centerY: typeButton.centerYAnchor)
+            let accessoryType = UIImageView()
+            accessoryType.image = UIImage(systemName: "chevron.right")
+            accessoryType.tintColor = .gray
+            typeButton.addSubview(accessoryType)
+            accessoryType.anchor(right: typeButton.rightAnchor, paddingRight: 10, centerY: typeButton.centerYAnchor)
+            let chosenType = UILabel()
+            chosenType.font = .systemFont(ofSize: 14)
+            chosenType.text = chosenData[level + 1].service_name
+            chosenType.textColor = .gray
+            typeButton.addSubview(chosenType)
+            typeButton.tag = level + 1
+            chosenType.anchor(right: accessoryType.leftAnchor, paddingLeft: 15, centerY: typeButton.centerYAnchor)
+            let tap = UITapGestureRecognizer(target: self, action: #selector(showPicker(sender:)))
+            typeButton.addGestureRecognizer(tap)
+            paddingTopFinishButton.constant = CGFloat(constPaddingTop) + 50 + 45
+            if chosenSecondLayer == "Yes".localized() {
+                paddingTopFinishButton.constant = paddingTopFinishButton.constant + 45
+            }
+        }
+    }
+    
+    @objc func showWorkingAreaPicker(sender: UITapGestureRecognizer) {
+        let workingAreaPickerVC = WorkingAreaPicker()
+        workingAreaPickerVC.selectedData = { selectedData in
+            self.chosenWorkingArea = selectedData
+            let buttonWA = self.subContainerView.subviews[2]
+            let titleChosen = buttonWA.subviews[buttonWA.subviews.count - 1] as! UILabel
+            titleChosen.text = selectedData.name
+        }
+        self.present(workingAreaPickerVC, animated: true, completion: nil)
+    }
+    
+    @objc func showPicker(sender: UITapGestureRecognizer) {
+        showPickerInt = sender.view!.tag
+        
+        let fullData = nextData[showPickerInt]
+        var index = (fullData?.firstIndex(of: chosenData[showPickerInt]))!
+        var titleChooser = "Select Product".localized()
+        if showPickerInt == 1 {
+            titleChooser = "Select Category".localized()
+        } else if showPickerInt > 1 {
+            titleChooser = "Select Sub-Category".localized()
+        }
+        
+        if sender.view!.restorationIdentifier == "workingArea" {
+            isWorkingArea = true
+        } else if sender.view!.restorationIdentifier == "secondLayer" {
+            isSecondLayerChooser = true
+            index = dataSecondLayer.firstIndex(of: chosenSecondLayer)!
+            titleChooser = "Is Second Layer?".localized()
+        } else {
+            isSecondLayerChooser = false
+            if self.chosenData.count - self.showPickerInt > 2 {
+                return
+            }
+        }
+        
+        let pickerView = UIPickerView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 10, height: 100))
+        pickerView.dataSource = self
+        pickerView.delegate = self
+        
+        let vc = UIViewController()
+        vc.preferredContentSize = CGSize(width: UIScreen.main.bounds.width - 10, height: 100)
+        pickerView.selectRow(index, inComponent: 0, animated: false)
+        
+        vc.view.addSubview(pickerView)
+        pickerView.translatesAutoresizingMaskIntoConstraints = false
+        pickerView.centerXAnchor.constraint(equalTo: vc.view.centerXAnchor).isActive = true
+        pickerView.centerYAnchor.constraint(equalTo: vc.view.centerYAnchor).isActive = true
+        
+        let alert = UIAlertController(title: titleChooser, message: "", preferredStyle: .actionSheet)
+        
+        alert.setValue(vc, forKey: "contentViewController")
+        alert.addAction(UIAlertAction(title: "Cancel".localized(), style: .cancel, handler: { (UIAlertAction) in
+        }))
+        
+        alert.addAction(UIAlertAction(title: "Select".localized(), style: .default, handler: { (UIAlertAction) in
+            let selectedIndex = pickerView.selectedRow(inComponent: 0)
+            if self.isSecondLayerChooser {
+                self.isSecondLayerChooser = false
+                var buttonSecondLayer = self.subContainerView.subviews[2]
+                if self.dataSecondLayer[selectedIndex] == "Yes".localized() {
+                    if self.chosenSecondLayer != self.dataSecondLayer[selectedIndex] {
+                        self.makeButtonWorkingArea()
+                        self.paddingBottomSLButton.constant -= 45
+                        self.paddingTopFinishButton.constant += 45
+                    }
+                } else {
+                    if self.chosenSecondLayer != self.dataSecondLayer[selectedIndex] {
+                        let buttonWA = self.subContainerView.subviews[2]
+                        buttonWA.removeConstraints(buttonWA.constraints)
+                        buttonWA.removeFromSuperview()
+                        self.paddingBottomSLButton.constant += 45
+                        self.paddingTopFinishButton.constant -= 45
+                        self.chosenWorkingArea = nil
+                        buttonSecondLayer = self.subContainerView.subviews[2]
+                    }
+                }
+                let titleChosen = buttonSecondLayer.subviews[buttonSecondLayer.subviews.count - 1] as! UILabel
+                titleChosen.text = self.dataSecondLayer[selectedIndex]
+                self.chosenSecondLayer = self.dataSecondLayer[selectedIndex]
+                return
+            }
+            let data = CategoryCC.getDatafromParent(parent: self.nextData[self.showPickerInt]![selectedIndex].service_id)
+            var moreSubViews = 1
+            if self.chosenSecondLayer == "Yes".localized() {
+                moreSubViews = 2
+            }
+            if !(self.subContainerView.subviews[self.subContainerView.subviews.count - (2 + self.showPickerInt + moreSubViews)] is UIButton) {
+                let buttonSubType = self.subContainerView.subviews[self.subContainerView.subviews.count - 1]
+                buttonSubType.removeConstraints(buttonSubType.constraints)
+                buttonSubType.removeFromSuperview()
+                self.chosenData.remove(at: self.showPickerInt + 1)
+            }
+            self.chosenData.remove(at: self.showPickerInt)
+            self.chosenData.insert(self.nextData[self.showPickerInt]![selectedIndex], at: self.showPickerInt)
+            var buttonType = self.subContainerView.subviews[self.subContainerView.subviews.count - 1]
+            if data.count != 0 {
+                self.makeButtonSubType(level: self.showPickerInt)
+                buttonType = self.subContainerView.subviews[self.subContainerView.subviews.count - 2]
+            } else {
+                self.paddingTopFinishButton.constant = CGFloat(self.showPickerInt == 0 ? 60 : 60 + (45 * self.showPickerInt)) + CGFloat(45 * moreSubViews)
+            }
+            let titleChosen = buttonType.subviews[buttonType.subviews.count - 1] as! UILabel
+            titleChosen.text = self.chosenData[self.showPickerInt].service_name
+        }))
+        self.present(alert, animated: true, completion: nil)
+    }
+    
+    func makeFinish() {
+        buttonSet.backgroundColor = .black
+        buttonSet.setImage(UIImage(systemName: "checkmark"), for: .normal)
+        buttonSet.tintColor = .white
+        buttonSet.setTitle("Set Officer".localized(), for: .normal)
+        buttonSet.titleLabel?.font = .boldSystemFont(ofSize: 15)
+        buttonSet.layer.cornerRadius = 8
+        subContainerView.addSubview(buttonSet)
+        buttonSet.anchor(bottom: subContainerView.bottomAnchor, right: subContainerView.rightAnchor, width: (self.view.bounds.width / 2) - 55, height: 40)
+        paddingTopFinishButton = buttonSet.topAnchor.constraint(equalTo: titleSetOfficer.bottomAnchor, constant: 10)
+        paddingTopFinishButton.isActive = true
+        buttonSet.addTarget(self, action: #selector(setOfficer), for: .touchUpInside)
+    }
+    
+    @objc func setOfficer() {
+        let alert = UIAlertController(title: "Set Officer Account".localized(), message: "Are you sure want to add \(name) to Officer Account category \(self.chosenData[self.chosenData.count - 1].service_name)?", preferredStyle: .alert)
+        alert.addAction(UIAlertAction(title: "Yes".localized(), style: .default, handler: { (action) -> Void in
+            if !CheckConnection.isConnectedToNetwork()  || API.nGetCLXConnState() == 0 {
+                let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                imageView.tintColor = .white
+                let banner = FloatingNotificationBanner(title: "Check your connection".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                banner.show()
+                return
+            }
+            let message = CoreMessage_TMessageBank.getManagementContactCenterBNI(l_pin:  self.f_pin, type: "1", category_id: "\(self.chosenData[self.chosenData.count - 1].service_id)", area_id: self.chosenSecondLayer == "Yes".localized() ? "1" : "0", is_second_layer: self.chosenWorkingArea != nil && self.chosenSecondLayer == "Yes".localized() ? self.chosenWorkingArea!.area_id : "")
+//            message.mBodies[CoreMessage_TMessageKey.CATEGORY_ID] = "\(self.chosenData[self.chosenData.count - 1].service_id)"
+            if let response = Nexilis.writeSync(message: message) {
+                if response.isOk() {
+                    DispatchQueue.main.async {
+                        let imageView = UIImageView(image: UIImage(systemName: "checkmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Successfully changed".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .success, colors: nil, iconPosition: .center)
+                        banner.show()
+                        self.dismiss(animated: true)
+                    }
+                }
+                DispatchQueue.main.async {
+                    if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "11" {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Username or password does not match".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    } else if response.getBody(key: CoreMessage_TMessageKey.ERRCOD, default_value: "99") == "20" {
+                        let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                        imageView.tintColor = .white
+                        let banner = FloatingNotificationBanner(title: "Invalid password".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                        banner.show()
+                    }
+                }
+            } else {
+                DispatchQueue.main.async {
+                    let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
+                    imageView.tintColor = .white
+                    let banner = FloatingNotificationBanner(title: "Unable to access servers".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
+                    banner.show()
+                }
+            }
+        }))
+        alert.addAction(UIAlertAction(title: "No".localized(), style: .cancel, handler: nil))
+        self.present(alert, animated: true, completion: nil)
+    }
+
+}
+
+extension SetOfficerBNI: UIPickerViewDelegate, UIPickerViewDataSource {
+    func numberOfComponents(in pickerView: UIPickerView) -> Int {
+        1
+    }
+    
+    func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
+        if isSecondLayerChooser {
+            return dataSecondLayer.count
+        }
+        return nextData[showPickerInt]!.count
+    }
+    
+    func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
+        if isSecondLayerChooser {
+            return dataSecondLayer[row]
+        }
+        return nextData[showPickerInt]![row].service_name
+    }
+}

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 533 - 223
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/SettingTableViewController.swift


+ 6 - 2
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Control/TypeViewController.swift

@@ -26,10 +26,13 @@ class CreateViewController: UITableViewController {
     override func viewDidLoad() {
         super.viewDidLoad()
         
-        title = "Live Video".localized()
+        title = "Live Streaming".localized()
         
         let cancel = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel(sender:)))
         
+        navigationController?.navigationBar.barTintColor = UIColor.black
+        
+        
         navigationItem.leftBarButtonItem = cancel
         navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done".localized(), style: .plain, target: self, action: #selector(didTapRight(sender:)))
         
@@ -143,7 +146,8 @@ class CreateViewController: UITableViewController {
     }
     
     @objc func didTapCancel(sender: AnyObject) {
-        navigationController?.dismiss(animated: true, completion: nil)
+        navigationController?.popViewController(animated: true)
+//        navigationController?.dismiss(animated: true, completion: nil)
     }
     
     func getTypeIndex(value: String) -> Int {

+ 67 - 17
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraCreateStreamingViewController.swift

@@ -7,6 +7,7 @@
 
 import UIKit
 import NotificationBannerSwift
+import AVFoundation
 
 public class QmeraCreateStreamingViewController: UITableViewController {
     
@@ -80,10 +81,16 @@ public class QmeraCreateStreamingViewController: UITableViewController {
         return textField
     }()
     
+    deinit {
+        print(#function, ">>>> TADAA1")
+        NotificationCenter.default.removeObserver(self)
+        NotificationCenter.default.post(name: NSNotification.Name(rawValue: "refreshView"), object: nil, userInfo: nil)
+    }
+    
     public override func viewDidLoad() {
         super.viewDidLoad()
         
-        title = "Live Video".localized()
+        title = "Live Streaming".localized()
         
         navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel(sender:)))
         navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done".localized(), style: .plain, target: self, action: #selector(didTapRight(sender:)))
@@ -95,7 +102,7 @@ public class QmeraCreateStreamingViewController: UITableViewController {
         table.addGestureRecognizer(tapGesture)
         
         if isJoin {
-            navigationItem.rightBarButtonItem?.title = "Join"
+            navigationItem.rightBarButtonItem?.title = "Join".localized()
             titleView.isEnabled = false
             descriptionView.isEditable = false
             taglineView.isEnabled = false
@@ -115,7 +122,7 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                 taglineView.text = e
             }
         } else if !isJoin && !data.isEmpty {
-            navigationItem.rightBarButtonItem?.title = "Start"
+            navigationItem.rightBarButtonItem?.title = "Start".localized()
             titleView.isEnabled = false
             descriptionView.isEditable = false
             taglineView.isEnabled = false
@@ -162,14 +169,14 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                     if response.getBody(key: CoreMessage_TMessageKey.ERRCOD) != "00" {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Live Video Promotion session hasn\'t started yet".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Live Streaming session hasn\'t started yet".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                         return
                     }
                 } else {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "No Network. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "No Network. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                     return
                 }
@@ -177,6 +184,54 @@ public class QmeraCreateStreamingViewController: UITableViewController {
             controller.data = by
             controller.streamingData = data
         } else {
+            let goAudioCall = Nexilis.checkMicPermission()
+            if !goAudioCall {
+                let alert = UIAlertController(title: "Attention!".localized(), message: "Please allow microphone permission in your settings".localized(), preferredStyle: .alert)
+                alert.addAction(UIAlertAction(title: "OK".localized(), style: UIAlertAction.Style.default, handler: { _ in
+                    if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
+                        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+                    }
+                }))
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                }
+                return
+            }
+            var permissionCheck = -1
+            if AVCaptureDevice.authorizationStatus(for: .video) ==  .authorized {
+                permissionCheck = 1
+            } else if AVCaptureDevice.authorizationStatus(for: .video) ==  .denied {
+                permissionCheck = 0
+            } else {
+                AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in
+                    if granted == true {
+                        permissionCheck = 1
+                    } else {
+                        permissionCheck = 0
+                    }
+                })
+            }
+            
+            while permissionCheck == -1 {
+                sleep(1)
+            }
+            
+            if permissionCheck == 0 {
+                let alert = UIAlertController(title: "Attention!".localized(), message: "Please allow camera permission in your settings".localized(), preferredStyle: .alert)
+                alert.addAction(UIAlertAction(title: "OK".localized(), style: UIAlertAction.Style.default, handler: { _ in
+                    if let url = URL(string: UIApplication.openSettingsURLString), UIApplication.shared.canOpenURL(url) {
+                        UIApplication.shared.open(url, options: [:], completionHandler: nil)
+                    }
+                }))
+                if UIApplication.shared.visibleViewController?.navigationController != nil {
+                    UIApplication.shared.visibleViewController?.navigationController?.present(alert, animated: true, completion: nil)
+                } else {
+                    UIApplication.shared.visibleViewController?.present(alert, animated: true, completion: nil)
+                }
+                return
+            }
             var data: [String: Any] = [:]
             if !isJoin && !self.data.isEmpty {
                 data = self.data
@@ -200,13 +255,13 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                 if streamingTitle.trimmingCharacters(in: .whitespaces).isEmpty {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Live Video Promotion title can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Live Streaming title can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                     return
                 } else if streamingDesc.trimmingCharacters(in: .whitespaces).isEmpty {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "Live Video Promotion description can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "Live Streaming description can't be empty".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                     return
                 }
@@ -250,19 +305,20 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                     if response.getBody(key: CoreMessage_TMessageKey.ERRCOD) != "00" {
                         let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                         imageView.tintColor = .white
-                        let banner = FloatingNotificationBanner(title: "Server Busy. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                        let banner = FloatingNotificationBanner(title: "Server Busy. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                         banner.show()
                         return
                     }
                 } else {
                     let imageView = UIImageView(image: UIImage(systemName: "xmark.circle.fill"))
                     imageView.tintColor = .white
-                    let banner = FloatingNotificationBanner(title: "No Network. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .top)
+                    let banner = FloatingNotificationBanner(title: "No Network. Please try again.".localized(), subtitle: nil, titleFont: UIFont.systemFont(ofSize: 16), titleColor: nil, titleTextAlign: .left, subtitleFont: nil, subtitleColor: nil, subtitleTextAlign: nil, leftView: imageView, rightView: nil, style: .danger, colors: nil, iconPosition: .center)
                     banner.show()
                     return
                 }
                 
                 Nexilis.saveMessageBot(textMessage: json, blog_id: data["blog"] as? String ?? "", attachment_type: "27")
+                NotificationCenter.default.post(name: NSNotification.Name(rawValue: "reloadTabChats"), object: nil, userInfo: nil)
             }
             controller.data = UserDefaults.standard.string(forKey: "me")!
             controller.streamingData = data
@@ -459,11 +515,8 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                 cell.selectionStyle = .default
             } else {
                 let data = users[indexPath.row - 1]
-                getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+                getImage(name: data.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                     content.image = image
-                    if isDownloaded {
-                        tableView.reloadRows(at: [indexPath], with: .automatic)
-                    }
                 }
                 content.text = data.fullName
                 content.textProperties.font = UIFont.systemFont(ofSize: 14)
@@ -480,11 +533,8 @@ public class QmeraCreateStreamingViewController: UITableViewController {
                 cell.selectionStyle = .default
             } else {
                 let data = groups[indexPath.row - 1]
-                getImage(name: data.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+                getImage(name: data.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
                     content.image = image
-                    if isDownloaded {
-                        tableView.reloadRows(at: [indexPath], with: .automatic)
-                    }
                 }
                 content.text = data.name
                 content.textProperties.font = UIFont.systemFont(ofSize: 14)

+ 1 - 4
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraGroupChooserViewController.swift

@@ -171,11 +171,8 @@ class QmeraGroupStreamingViewController: UITableViewController {
             group = available[indexPath.row]
         }
         content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-        getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+        getImage(name: group.profile, placeholderImage: UIImage(named: "Conversation---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
             content.image = image
-            if isDownloaded {
-                tableView.reloadRows(at: [indexPath], with: .none)
-            }
         }
         content.text = group.name
         cell.contentConfiguration = content

+ 11 - 11
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraStreamingViewController.swift

@@ -223,13 +223,13 @@ class QmeraStreamingViewController: UIViewController {
         
         Nexilis.shared.streamingDelagate = self
         if isLive {
-            let buttonRotate = UIButton()
-            buttonRotate.frame = CGRect(x:0, y:0, width:30, height:30)
-            buttonRotate.setImage(UIImage(systemName: "arrow.triangle.2.circlepath.camera")?.withTintColor(.mainColor, renderingMode: .alwaysOriginal), for: .normal)
-            buttonRotate.backgroundColor = .white.withAlphaComponent(0.2)
-            buttonRotate.layer.cornerRadius = 15.0
-            buttonRotate.addTarget(self, action: #selector(camera(sender:)), for: .touchUpInside)
-            navigationItem.rightBarButtonItem = UIBarButtonItem(customView: buttonRotate)
+//            let buttonRotate = UIButton()
+//            buttonRotate.frame = CGRect(x:0, y:0, width:30, height:30)
+//            buttonRotate.setImage(UIImage(systemName: "arrow.triangle.2.circlepath.camera")?.withTintColor(.mainColor, renderingMode: .alwaysOriginal), for: .normal)
+//            buttonRotate.backgroundColor = .white.withAlphaComponent(0.2)
+//            buttonRotate.layer.cornerRadius = 15.0
+//            buttonRotate.addTarget(self, action: #selector(camera(sender:)), for: .touchUpInside)
+//            navigationItem.rightBarButtonItem = UIBarButtonItem(customView: buttonRotate)
             
             API.initiateBC(sTitle: data, nCamIdx: 1, nResIdx: 2, nVQuality: 4, ivLocalView: imageView)
         } else {
@@ -396,7 +396,7 @@ class QmeraStreamingViewController: UIViewController {
         _ = Nexilis.write(message: CoreMessage_TMessageBank.joinLiveVideo(broadcast_id: data, request_id: id))
     }
     
-    private func sendLeft() {
+    public func sendLeft() {
         let id = Date().currentTimeMillis().toHex()
         _ = Nexilis.write(message: CoreMessage_TMessageBank.leftLiveVideo(broadcast_id: data, request_id: id))
     }
@@ -452,7 +452,7 @@ extension QmeraStreamingViewController: UITableViewDataSource {
             viewContent.addSubview(image)
             image.anchor(top: viewContent.topAnchor, left: viewContent.leftAnchor, width: 30, height: 30)
             if !chat.thumb.isEmpty {
-                getImage(name: chat.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, completion: { result, isDownloaded, imagePerson in
+                getImage(name: chat.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, imagePerson in
                     image.image = imagePerson
                 })
             } else {
@@ -478,7 +478,7 @@ extension QmeraStreamingViewController: UITableViewDataSource {
             viewContent.addSubview(image)
             image.anchor(left: viewContent.leftAnchor, centerY: viewContent.centerYAnchor, width: 30, height: 30)
             if !chat.thumb.isEmpty {
-                getImage(name: chat.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, completion: { result, isDownloaded, imagePerson in
+                getImage(name: chat.thumb, placeholderImage: UIImage(systemName: "person.circle.fill")!, isCircle: true, tableView: tableView, indexPath: indexPath, completion: { result, isDownloaded, imagePerson in
                     image.image = imagePerson
                 })
             } else {
@@ -575,7 +575,7 @@ extension QmeraStreamingViewController: LiveStreamingDelegate {
             let m = message.split(separator: ",", omittingEmptySubsequences: false)
             let name = m[3].trimmingCharacters(in: .whitespaces)
             let thumb = m[2].trimmingCharacters(in: .whitespaces)
-            let text = "Lefted".localized()
+            let text = "Left".localized()
             chats.append(StreamingChat(name: name, thumb: thumb, messageText: text, isInfo: true))
             DispatchQueue.main.async {
                 self.countViewer.text = "\(Int(self.countViewer.text!)! - 1)"

+ 20 - 9
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/QmeraUserChooserViewController.swift

@@ -66,12 +66,18 @@ class QmeraUserChooserViewController: UITableViewController {
         
         tableView = table
         
-        getData { users in
-            self.availableUser.append(contentsOf: users)
-            DispatchQueue.main.async {
-                self.tableView.reloadData()
+        DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
+            self.getData { users in
+                self.availableUser.append(contentsOf: users)
+                DispatchQueue.main.async {
+                    self.tableView.reloadData()
+                }
             }
-        }
+        })
+    }
+    
+    override func viewWillAppear(_ animated: Bool) {
+        pullBuddy()
     }
     
     override func viewWillDisappear(_ animated: Bool) {
@@ -83,6 +89,14 @@ class QmeraUserChooserViewController: UITableViewController {
         navigationController?.popViewController(animated: true)
     }
     
+    private func pullBuddy() {
+        if let me = UserDefaults.standard.string(forKey: "me") {
+            DispatchQueue.global().async {
+                let _ = Nexilis.write(message: CoreMessage_TMessageBank.getBatchBuddiesInfos(p_f_pin: me, last_update: 0))
+            }
+        }
+    }
+    
     // MARK: - Data source
     
     private func getData(completion: @escaping ([User]) -> ()) {
@@ -157,11 +171,8 @@ class QmeraUserChooserViewController: UITableViewController {
             user = availableUser[indexPath.row]
         }
         content.imageProperties.maximumSize = CGSize(width: 40, height: 40)
-        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true) { result, isDownloaded, image in
+        getImage(name: user.thumb, placeholderImage: UIImage(named: "Profile---Purple", in: Bundle.resourceBundle(for: Nexilis.self), with: nil), isCircle: true, tableView: tableView, indexPath: indexPath) { result, isDownloaded, image in
             content.image = image
-            if isDownloaded {
-                tableView.reloadRows(at: [indexPath], with: .none)
-            }
         }
         content.text = "\(user.firstName) \(user.lastName)"
         cell.contentConfiguration = content

+ 31 - 12
appbuilder-ios/NexilisLite/NexilisLite/Source/View/Streaming/StreamingViewController.swift

@@ -105,25 +105,44 @@ public class WhiteboardCanvas: UIView {
         let dx = abs(touchPoint.x - startingPoint.x)
         let dy = abs(touchPoint.x - startingPoint.x)
         if (dx >= fRThreshold || dy >= fRThreshold) {
-            
-            onDrawing?(touchPoint.x, touchPoint.y, nWidth, nHeight, lineColor.toHexString(), lineWidth, startingPoint.x, startingPoint.y)
+            let onGoingCC = UserDefaults.standard.string(forKey: "onGoingCC") ?? ""
+            if !onGoingCC.isEmpty {
+                var colorDraw = UIColor.red
+                let requester = onGoingCC.components(separatedBy: ",")[0]
+                let idMe = UserDefaults.standard.string(forKey: "me")!
+                if requester == idMe {
+                    colorDraw = UIColor.blue
+                }
+                onDrawing?(touchPoint.x, touchPoint.y, nWidth, nHeight, colorDraw.toHexString(), lineWidth, startingPoint.x, startingPoint.y)
 
-            self.drawWhiteboard(destination: destination ?? "", x: toString(from: touchPoint.x), y: toString(from: touchPoint.y), w: toString(from: nWidth), h: toString(from: nHeight), fc: peerLineColor.toHexString(),
-                           sw: toString(from: lineWidth), xo: toString(from: startingPoint.x), yo: toString(from: startingPoint.y))
-            
-            path = UIBezierPath()
-            path.move(to: startingPoint)
-            path.addLine(to: touchPoint)
-            startingPoint = touchPoint
-            drawShapeLayer()
+                self.drawWhiteboard(destination: destination ?? "", x: toString(from: touchPoint.x), y: toString(from: touchPoint.y), w: toString(from: nWidth), h: toString(from: nHeight), fc: colorDraw.toHexString(),
+                               sw: toString(from: lineWidth), xo: toString(from: startingPoint.x), yo: toString(from: startingPoint.y))
+                
+                path = UIBezierPath()
+                path.move(to: startingPoint)
+                path.addLine(to: touchPoint)
+                startingPoint = touchPoint
+                drawShapeLayer(color: colorDraw)
+            } else {
+                onDrawing?(touchPoint.x, touchPoint.y, nWidth, nHeight, lineColor.toHexString(), lineWidth, startingPoint.x, startingPoint.y)
+
+                self.drawWhiteboard(destination: destination ?? "", x: toString(from: touchPoint.x), y: toString(from: touchPoint.y), w: toString(from: nWidth), h: toString(from: nHeight), fc: peerLineColor.toHexString(),
+                               sw: toString(from: lineWidth), xo: toString(from: startingPoint.x), yo: toString(from: startingPoint.y))
+                
+                path = UIBezierPath()
+                path.move(to: startingPoint)
+                path.addLine(to: touchPoint)
+                startingPoint = touchPoint
+                drawShapeLayer()
+            }
         }
         
     }
     
-    func drawShapeLayer() {
+    func drawShapeLayer(color: UIColor = UIColor.red) {
         let shapelayer = CAShapeLayer()
         shapelayer.path = path.cgPath
-        shapelayer.strokeColor = lineColor.cgColor
+        shapelayer.strokeColor = color.cgColor
         shapelayer.lineWidth = lineWidth
         shapelayer.fillColor = UIColor.clear.cgColor
         self.layer.addSublayer(shapelayer)

+ 0 - 0
appbuilder-ios/NexilisLite/NexilisLite/Source/WhiteboardDelegate.swift


Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott