Summary: Read our extensive tutorial to learn how to build a desktop chat application using QuickBlox and Electron.
Welcome to our developer tutorial on building a powerful desktop chat application using QuickBlox and Electron! In this tutorial, we will walk you through the process of creating a robust and feature-rich chat application that runs seamlessly on desktop platforms. By combining the capabilities of QuickBlox, a versatile cloud communication platform, with the flexibility and ease of use of Electron, a framework for building cross-platform desktop applications, we’ll empower you to create a dynamic chat experience for your users. From setting up your development environment to implementing essential chat features such as real-time messaging and file sharing, we’ll cover every step of the process in detail.
Geared towards novice developers, this article simplifies the complexities of cross-platform desktop application development using advanced technologies. We will provide simple instructions to ensure accessibility for those unfamiliar with these tools. Our goal is to help you to develop a cross-platform application capable of running on MacOS, Linux, and Windows operating systems, providing standard chat functionality with messaging and file exchange similar to applications like WhatsApp and Facebook Messenger. Throughout this tutorial, we’ll provide clear explanations, code samples, and practical examples to help you understand the concepts and techniques involved in building a desktop chat application with QuickBlox and Electron. By the end, you’ll have a fully functional chat application that you can customize and extend to meet the specific needs of your project or application.
Before we get started, let’s introduce you to some of the key technologies you’ll be using in this project.
To achieve cross-platform compatibility, we’ll utilize Electron. This open-source framework developed by GitHub enables developers to build cross-platform desktop applications using web technologies such as HTML, CSS, and JavaScript.
For chat functionality implementation, we’ll use QuickBlox JS SDK, which grants access to cloud services for chats and AI functionality. QuickBlox JS SDK is a powerful tool for developers seeking to integrate real-time communication features into web applications. It offers a comprehensive suite of APIs and functionalities to facilitate the development of chat applications, video calling solutions, and other real-time communication tools. With QuickBlox JS SDK, developers can easily implement features such as one-on-one messaging, group chats, file sharing, push notifications, and more.
Additionally, we will employ React Chat UI Kit to create a user-friendly chat interface. Our UI Kit provides a collection of pre-built user interface components specifically designed for developers building chat applications using React.js and QuickBlox. It offers a range of customizable components, including chat bubbles, message input fields, user avatars, and more, to streamline the development process and enhance the visual appeal of chat interfaces.
The QuickBlox React UI Kit will enable us to easily integrate pre-built chat into our project.
In this project we will also be implementing the renderer process using React mechanisms to construct the user interface. React is an open-source JavaScript library developed by Facebook for building user interfaces, particularly for web applications. It provides developers with a declarative and component-based approach to building UIs, making it easier to manage and update complex user interfaces
Let’s Get Started!
Let’s create a project directory and init a node project inside it. To do so, run the following commands:
mkdir electron_chat cd electron_chat npm init
Here are the npm commands for installing the dependencies, categorized as follows:
Installing React:
npm install react react-dom @babel/core @babel/preset-env @babel/preset-react babel-loader
Installing Electron:
npm install electron
Installing Webpack:
npm install webpack webpack-cli css-loader sass-loader style-loader
Installing QuickBlox SDK and React UI Kit:
npm install quickblox quickblox-react-ui-kit
Executing these commands will install all the required dependencies to develop an application utilizing React, Electron, Webpack, and Quickblox SDK with React UI Kit.
Upon completion, your package.json file will resemble the following:
{ "name": "electron_chat", "version": "1.0.0", "description": "", "main": "main.js", "scripts": { "start": "electron .", "watch": "webpack --config webpack.common.js --watch" }, "author": "QB Developer", "license": "ISC", "devDependencies": { "electron": "^23.1.1" }, "dependencies": { "@babel/core": "^7.21.0", "@babel/preset-env": "^7.20.2", "@babel/preset-react": "^7.18.6", "babel-loader": "^9.1.2", "css-loader": "^6.7.3", "quickblox": "^2.16.1", "quickblox-react-ui-kit": "^0.2.8", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.58.3", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "webpack": "^5.75.0", "webpack-cli": "^5.0.1" } }
Pay attention to the line “main”: “main.js”; we have changed it from the original “main”: “index.js”. This modification was necessary to align with the standard React application project structure, where index.js serves as the main entry point and App.js as the main component. Considering that our Electron application comprises both renderer and main processes, main.js functions as the primary entry point for the Electron application.
Note: the line watch": "webpack --config webpack.common.js --watch
will be described in the section “Testing and Debugging” below.
Let’s start by creating the basic structure of the application.
Create Basic Structure:
Set Up Directories:
Add Renderer Process Files:
Set Up Webpack:
Review File Contents:
Content of main.js is:
// main processing use node js const {app, BrowserWindow } = require('electron'); const path = require("path"); function createWindow(){ // renderer processing const win = new BrowserWindow({ width:800, height:600, backgroundColor: "white", webPreferences: { nodeIntegration: false, contextIsolation: true, worldSafeExecuteJavaScript: true, preload: path.join(__dirname, 'preload.js'), } }); win.loadFile('index.html'); win.webContents.openDevTools({ mode: 'detach' }); } app.whenReady().then(createWindow); app.on('window-all-closed', ()=>{ if (process.platform !== 'darwin'){ app.quit(); } }) app.on('activate', ()=>{ if (BrowserWindow.getAllWindows().length === 0){ createWindow(); } })
The main.js file serves as the entry point for the Electron main process, which orchestrates the application’s core functionality. Let’s review the purpose of each function:
These functions and event listeners collectively manage the application’s lifecycle, user sessions, window creation, and data retrieval, ensuring smooth operation and interaction between the main and renderer processes.
Note: The webPreferences parameter in the configuration of the browser window will be explained in the Security and Safety section.
Then we’ll create an index.html file next to main.js and review its contents.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="content-security-policy" content="script-src 'self'"/> <title>Desktop QuickBlox Chat</title> </head> <body> <h1>Hello world from QuickBlox chat desktop application!</h1> <div id="electronChat"></div> </body> <script src="./build/js/app.js"></script> </html>
Pay attention to the element:
<meta http-equiv="content-security-policy" content="script-src 'self'"/>
This meta tag defines the content security policy for the page. In this case, it allows loading JavaScript only from the same source (self).
The line, <div id="electronChat"> </div>
is a placeholder for react content.
This line, <script src="./build/js/app.js"></script>
indicates that we should already have a compiled and built React application located at ‘./build/js/app.js’. Webpack is responsible for this. Below, we’ll go over its settings.
After that, we’ll move on to creating files responsible for the renderer process and configuring Webpack. By this point, our directory structure should look like this:
We’ll continue by reviewing the contents of index.js, App.js, and webpack.common.js, and make necessary adjustments to the index.html file.
Content of index.js is:
import React from 'react'; import { createRoot } from 'react-dom/client'; import App from "./App"; const container = document.getElementById('electronChat'); const root = createRoot(container); // createRoot(container!) if you use TypeScript root.render( <App />);
Content of App.js is
import React, {useEffect} from "react"; export default function App(){ const title = "Test "; const extendedTitle = title + 'React App'; // return ( div> <h1>{extendedTitle}</h1> </div> ) }
Content of webpack.common.js is:
const path = require('path'); const webpack = require("webpack"); module.exports = { mode: 'development', entry: './src/js/index.js', devtool: 'inline-source-map', target: 'electron-renderer', module: { rules: [ { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [[ '@babel/preset-env', { targets: { esmodules: true } }], '@babel/preset-react'] } } }, { test: [/\.s[ac]ss$/i, /\.css$/i], use: [ // Creates style nodes from JS strings 'style-loader', // Translates CSS into CommonJS 'css-loader', // Compiles Sass to CSS 'sass-loader', ], } ] }, plugins: [ new webpack.ProvidePlugin({ adapter: ['webrtc-adapter', 'default'], }), new webpack.ProgressPlugin() ], resolve: { extensions: ['.js'], }, output: { filename: 'app.js', path: path.resolve(__dirname, 'build', 'js'), }, };
This is a fairly standard webpack configuration file, with key values being the entry file name, target and the path for the final build location.
Once we complete this part of the work, we’ll be ready to launch and debug our application.
First, we need to make changes to the package.json file. We should build our bundled app.js in the build folder with webpack. Let’s add the following line to the scripts section:
"watch": "webpack --config webpack.common.js --watch"
After that, you can type the following command in the console:
npm run watch
As a result, the app.js will be built, allowing you to use it in index.html.
To run the Electron application, open another command console and type:
npm start
The line win.webContents.openDevTools({ mode: 'detach' });
shows the developer tools in a separate window.
For detailed information about best practices for security in electron application, check out the following resources:
https://www.electronjs.org/docs/latest/tutorial/ipc
javascript – Electron – Why do we need to communicate between the main process and the renderer processes? – Stack Overflow
In Electron, interaction between the main and renderer processes is carried out through IPC (Inter-process communication). This is a key element in creating feature-rich desktop applications in Electron. Here are some best practices for interacting between the main and renderer processes:
Using IPC channels: In Electron, processes communicate by passing messages through developer-defined “channels” using the ipcMain and ipcRenderer modules. These channels are arbitrary (you can name them whatever you want) and bidirectional (you can use the same channel name for both modules).
Using preload script: You should be familiar with the idea of using a preload script to import Node.js and Electron modules into the context-isolated renderer process.
Sending messages from the renderer to the main process: To send a unidirectional IPC message from the renderer to the main process, you can use the ipcRenderer.send API to send a message, which is then received by the ipcMain.on API. Typically, this pattern is used to invoke the main process API from your web content.
Security: Calling native GUI-related APIs directly from the renderer is restricted due to security issues and potential resource leaks. Therefore, if you want to do something like read or write a file but don’t want to include Node.js API integration in your renderer processes, you’ll need to send a message from the renderer to the main process (which can be checked and scoped), and the main process will use the Node.js API to read/write the file (again, with limitations).
To implement all the recommended steps in our project, we need to create a window with the following security parameters:
The webPreferences parameter in the configuration of the browser window (BrowserWindow) defines the web environment settings for the renderer process. These settings include security, context isolation, and preload scripts.
nodeIntegration: false:
Disables access to Node.js API in the renderer process, enhancing application security.
contextIsolation: true:
Provides context isolation to prevent conflicts of variable names.
worldSafeExecuteJavaScript: true:
Prevents execution of unsafe JavaScript code in the renderer process.
preload: path.join(__dirname, 'preload.js'):
Specifies the path to the preload script for loading modules and configuring the environment before loading content in the browser window.
These parameters help improve the security and manageability of the renderer process by providing controlled access to system resources and preventing vulnerabilities.
We will implement the preload.js file, inside of which we’ll create an object for subscription.
Content of preload.js is:
const { contextBridge, ipcRenderer } = require('electron'); contextBridge.exposeInMainWorld('electronAPI', { requestData: async () => { return new Promise((resolve, reject) => { ipcRenderer.send('request-data'); ipcRenderer.once('response-data', (event, data) => { resolve(data); }); ipcRenderer.once('response-error', (event, error) => { reject(error); }); }); } });
To notify main process from renderer side (App.js) we should use:
async function fetchDataFromMainProcess() { try { // const data = await window.electronAPI.requestData(); const data = await electronAPI.requestData(); setSessionToken(data.sessionToken); } catch (error) { console.error('fetch data from main process error :', error); } }
On the main process side (main.js) to receive and reply we should use:
async function prepareDataForRenderer() { try { // async operation will described in Integration of Quickblox section below const session = await createUserSession(); const data = {config:{}, sessionToken: session.token}; return data; } catch (error) { throw new Error('Ошибка при получении данных: ' + error.message); } } ipcMain.on('request-data', async (event, arg) => { try { const data = await prepareDataForRenderer(); event.reply('response-data', data); } catch (error) { event.reply('response-error', error.message); } });
Now we are ready to integrate the Quickblox SDK into our project and use it to initialize the React UI Kit. Given that our main process is isolated from the renderer process and represents a Node.js application, we can store all configuration data from our account, such as application keys and secret keys, in the main process.
We will follow this logic to set up the chat application:
Main Process:
Renderer Process:
After completing these steps, you will have a ready-made chat application.
Let’s add new code into main.js
// main processing use node js const {app, BrowserWindow, ipcMain } = require('electron'); const path = require("path"); const {QuickBlox} = require("quickblox"); function createWindow(){ // renderer processing } const userRequiredParams = { 'login': 'YOUR_LOGIN', 'password': 'YOUR_PASSWORD' }; const QBConfig = { credentials: { appId: -1, accountKey: 'YOUR_ACCOUNT_KEY', authKey: 'YOUR_AUTH_KEY', authSecret: 'YOUR_AUTH_SECRET', sessionToken: '', }, configAIApi: { AIAnswerAssistWidgetConfig: { organizationName: 'Quickblox', openAIModel: 'gpt-3.5-turbo', apiKey: 'YOUR_OpenAi_API_KEY', maxTokens: 3584, useDefault: true, proxyConfig: { api: 'v1/chat/completions', servername: 'https://api.openai.com/', port: '', }, }, AITranslateWidgetConfig: { organizationName: 'Quickblox', openAIModel: 'gpt-3.5-turbo', apiKey: 'sYOUR_OpenAi_API_KEY', maxTokens: 3584, useDefault: true, defaultLanguage: 'Ukrainian', languages: ['Ukrainian', 'English', 'French', 'Portuguese', 'German'], proxyConfig: { api: 'v1/chat/completions', servername: '', port: '', }, // proxyConfig: { // api: 'v1/chat/completions', // servername: 'http://localhost', // port: '3012', // }, }, AIRephraseWidgetConfig: { organizationName: 'Quickblox', openAIModel: 'gpt-3.5-turbo', apiKey: 'YOUR_OpenAi_API_KEY', maxTokens: 3584, useDefault: true, defaultTone: 'Professional', Tones: [], proxyConfig: { api: 'v1/chat/completions', servername: 'https://api.openai.com/', port: '', }, }, }, appConfig: { maxFileSize: 10 * 1024 * 1024, sessionTimeOut: 122, chatProtocol: { active: 2, }, debug: true, enableForwarding: true, enableReplying: true, regexUserName: '^(?=[a-zA-Z])[-a-zA-Z_ ]{3,49}(? { let QuickBlox = require('quickblox').QuickBlox; const QBOther = new QuickBlox(); const APPLICATION_ID = QBConfig.credentials.appId; const AUTH_KEY = QBConfig.credentials.authKey; const AUTH_SECRET = QBConfig.credentials.authSecret; const ACCOUNT_KEY = QBConfig.credentials.accountKey; const CONFIG = QBConfig.appConfig; QBOther.init(APPLICATION_ID, AUTH_KEY, AUTH_SECRET, ACCOUNT_KEY, CONFIG); return new Promise((resolve, reject) => { QBOther.createSession(userRequiredParams, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); }; async function prepareDataForRenderer() { try { const session = await createUserSession(); const data = {config:{ ...QBConfig, credentials: {appId: -1, accountKey: 'YOUR_ACCOUNT_KEY', authKey: '', authSecret: '', sessionToken: session.token,},}, currentUserName: userRequiredParams.login, sessionToken: session.token}; return data; } catch (error) { throw new Error('Error: ' + error.message); } } ipcMain.on('request-data', async (event, arg) => { // no changes }); app.whenReady().then( () => { // no changes }); app.on('window-all-closed', ()=>{ // no changes }) app.on('activate', ()=>{ // no changes })
This code is for an Electron application where the main process is written in Node.js and interacts with a renderer process. Here’s how it works:
Let’s correct data in App.js
import React, {useEffect} from "react"; import QB from "quickblox/quickblox"; import { QuickBloxUIKitProvider, QuickBloxUIKitDesktopLayout, useQbUIKitDataContext } from 'quickblox-react-ui-kit'; export default function App(){ const qbUIKitContext = useQbUIKitDataContext(); const [isUserAuthorized, setUserAuthorized] = React.useState(false); const [isSDKInitialized, setSDKInitialized] = React.useState(false); const [QBConfig, setQBConfig] = React.useState({}); const [currentUserName, setCurrentUserName] = React.useState(''); const [sessionToken, setSessionToken] = React.useState(''); async function fetchDataFromMainProcess() { try { // const data = await window.electronAPI.requestData(); const data = await electronAPI.requestData(); setQBConfig(data.config); setCurrentUserName(data.currentUserName); setSessionToken(data.sessionToken); } catch (error) { console.error('fetch data from main process error :', error); } } useEffect(() => { fetchDataFromMainProcess(); }, []); useEffect(() => { if (!sessionToken || sessionToken?.length === 0) return; // check if we have installed SDK if (typeof window.QB === 'undefined') { if (typeof QB !== 'undefined') { window.QB = QB; } else { let QBLib = require('quickblox/quickblox.min'); window.QB = QBLib; } } if (isSDKInitialized && isUserAuthorized) return; QB.initWithAppId(QBConfig.credentials.appId, QBConfig.credentials.accountKey,QBConfig.appConfig); console.log('Call on.sessionExpired: '); QB.chat.onSessionExpiredListener = function (error) { if (error) { console.log('onSessionExpiredListener - error: ', error); } else { console.log('Hello from client app SessionExpiredListener'); } } QB.startSessionWithToken(sessionToken, function(err, sessionData){ if (err){ console.log('Error startSessionWithToken'); } else { const userId = sessionData.session.user_id; const password = sessionData.session.token; const paramsConnect = { userId, password }; QB.chat.connect(paramsConnect, async function (errorConnect, resultConnect) { if (errorConnect) { console.log('Can not connect to chat server: ', errorConnect); } else { const authData = { userId: userId, password: password, userName: currentUserName, sessionToken: sessionData.session.token }; console.log(authData); await qbUIKitContext.authorize(authData); setSDKInitialized(true); setUserAuthorized(true); } }); } }); }, [sessionToken]); return ( <div> { isSDKInitialized && isUserAuthorized && QBConfig.credentials ? <QuickBloxUIKitProvider maxFileSize={100 * 1000000} accountData={{...QBConfig.credentials}} loginData={{ login: currentUserName, password: '', }} qbConfig={{...QBConfig}} > <div> { // React states indicating the ability to render UI <QuickBloxUIKitDesktopLayout uikitHeightOffset={'40px'}/> } </div> </QuickBloxUIKitProvider> : <div>wait while SDK is initializing...</div> } </div> ) }
This code is a React component that is responsible for setting up a QuickBlox chat application within an Electron environment. Here’s how it works:
Overall, this component sets up a QuickBlox chat application within an Electron environment, handles user authentication, and manages the state of the application
Well done! You should now have a comprehensive overview of how to create a multifunctional desktop communication application!
As a result of our efforts, we have developed a cross-platform desktop version of our open-source Q-municate messenger, equipped with standard chat functionality and capable of running on various operating systems.
We have built this using the application code example from our GitHub repository: https://github.com/QuickBlox/examples/tree/main/Articles/js/electron_chat.
At part of this project we initialized the React UI Kit using the application token, ensuring ease of use. Although we did not include the implementation of Firebase login for simplification purposes, our main goal was to help novice developers create their own cross-platform desktop communication application.
Possible directions for deeper exploration of the topic include:
We hope that this article will be a useful resource for anyone looking to create their own desktop communication application using modern technologies, and welcome any feedback!
For further exploration of Quickblox, React, and Electron, we recommend referring to the following useful resources:
Quickblox:
Official Quickblox documentation: https://docs.quickblox.com/
Guides on using Quickblox SDK: https://docs.quickblox.com/docs/react-uikit-overview
Code examples and solutions on GitHub Quickblox: https://github.com/QuickBlox/quickblox-javascript-sdk/tree/gh-pages/samples/react-chat-ui-kit-init-with-token-sample
React:
Official React documentation: https://react.dev/ or Getting Started – React (reactjs.org)
Electron:
Official Electron documentation: https://www.electronjs.org/docs/latest/
Additional tutorials:
How to create your own AI ChatBot using OpenAI and JavaScript SDK
AI-Powered Web App: Part 1, Setting up NodeJS with Express
Integrating AI Features into your React App made Easy
How to send first message in React UI Kit