Repository: bryanberger/figma-discord-presence Branch: master Commit: a0027f0a3b4d Files: 20 Total size: 30.5 KB Directory structure: gitextract_wfp9rim1/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ └── deploy.yml ├── .gitignore ├── .node-version ├── LICENSE ├── README.md ├── build/ │ ├── entitlements.mac.plist │ └── icon.icns ├── dev-app-update.yml ├── package.json └── src/ ├── afterSignHook.js ├── lib/ │ ├── activity.js │ ├── config.js │ ├── events.js │ ├── figma.js │ ├── logger.js │ ├── tray.js │ ├── updater.js │ └── util.js └── main.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: bryanberger --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/workflows/deploy.yml ================================================ name: Build/release on: push: branches: - master workflow_dispatch: {} jobs: release: runs-on: ${{ matrix.os }} strategy: matrix: # use macos-11 for now until we can fix python versions os: [macos-11, windows-latest] steps: - name: Check out Git repository uses: actions/checkout@v2 - name: Install Node.js, NPM and Yarn uses: actions/setup-node@v1 with: node-version: "14.15.0" - name: Build/release Electron app uses: samuelmeuli/action-electron-builder@v1 with: # Apple code signing certs mac_certs: ${{ secrets.mac_certs }} mac_certs_password: ${{ secrets.mac_certs_password }} # GitHub token, automatically provided to the action # (No need to define this secret in the repo settings) github_token: ${{ secrets.github_token }} # If the commit is tagged with a version (e.g. "v1.0.0"), # release the app after building release: ${{ startsWith(github.ref, 'refs/tags/v') }} # Always publish to S3 args: "-p always" env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} ================================================ FILE: .gitignore ================================================ # Packages node_modules **/node_modules # Build artifacts dist/ # Log files logs *.log npm-debug.log* # Runtime data pids *.pid *.seed # Miscellaneous .tmp/ !.vscode/launch.json .env notes.md ================================================ FILE: .node-version ================================================ 14.15.0 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Bryan Berger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Figma Discord Presence [![Build/release](https://github.com/bryanberger/figma-discord-presence/actions/workflows/deploy.yml/badge.svg)](https://github.com/bryanberger/figma-discord-presence/actions/workflows/deploy.yml) Figma Discord Presence - Adds rich presence activity to Discord for Figma | Product Hunt > Update your discord activity status with a rich presence from Figma. > Supports Windows and MacOS ![demo](.github/demo.png?raw=true) ## Features - Shows what you're working on in Figma - Menubar application for convenient control and configuration - Privacy configuration options for hiding filenames, activity status, and Figma view buttons - Idle and active indication if you have tabbed out or are actively using Figma - Respects Discords 15s status update limit, but, privacy options set immediately - Support for manually reconnecting to the Discord Gateway - Support for enabling or disabling presence reporting at will ## How does it work? Figma does not support a native way to monitor the application state in the background (yet?), but, it does drop some state files on your machine. This application periodically reads those files checking for updates and combines some information to determine whether Figma is in the foreground, what the current active file is, and a share link to that file. Every ~15s your Figma activity is reported to Discord via the Discord RPC protocol. ## Troubleshooting > Linux is currently not supported. > This application requires Figma Desktop. > Ensure that you have your activity status enabled in Discord, or your activity won't be visible to anyone. **MacOS:** - MacOS may ask for permission to control other apps. It is required to enable and communicate with Figma and Discord. - This application assumes you install Figma Desktop normally, and have not changed or modified it in any way - `~/Library/Saved\ Application\ State/com.figma.Desktop.savedState/windows.plist` must exist - `~/Library/Application\ Support/Figma/settings.json` must exist - It may take a few seconds for your activity to update to show the latest active/idle status and filename in Discord. Figma's `savedState` does not update in realtime. We could watch this file for changes and update your Discord activity when it does but since we try to honor Discord's 15s activity update limit, we currently just wait for the next tick to update your activity. ## Development ```bash # Clone this repository git clone https://github.com/bryanberger/figma-discord-presence # Change directory cd figma-discord-presence # Copy and edit env vars cp .env.example .env # Install dependencies npm install # Run the app npm start # Build the electron binaries npm run dist # Publish (using the S3 Provider, make sure you're authenticated and have a bucket setup) npm run publish ``` ## Release This project uses Github Actions to build for Windows and Mac. Upon successful build, if a git tag exists it will publish to S3 (given you've provided the proper access tokens). When you want to create a new release, follow these steps: - Update the version in your project's package.json file (e.g. 1.2.3) - Commit that change (git commit -am v1.2.3) - Tag your commit (git tag v1.2.3). Make sure your tag name's format is v*.*.*. Your workflow will use this tag to detect when to create a release - Push your changes to GitHub (git push && git push --tags) After building successfully, the action will publish your release artifacts. ## Contributing To contribute to this repository, feel free to create a new fork of the repository and submit a pull request. 1. Fork / Clone and select the `master` branch. 2. Create a new branch in your fork. 3. Make your changes. 4. Commit your changes, and push them. 5. Submit a Pull Request [here](https://github.com/bryanberger/figma-discord-presence/pulls)! ## Notice While I am a Discord employee, this is by no way endorsed as an "official" integration with Figma. This is a personal project and is actually kind of a hacky solution to bring Rich Presence for Figma to Discord. ## License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ================================================ FILE: build/entitlements.mac.plist ================================================ com.apple.security.cs.allow-jit com.apple.security.cs.allow-unsigned-executable-memory com.apple.security.cs.allow-dyld-environment-variables ================================================ FILE: dev-app-update.yml ================================================ provider: s3 bucket: figma-discord-presence ================================================ FILE: package.json ================================================ { "name": "figma-discord-presence", "productName": "Figma Discord Presence", "version": "1.2.6", "description": "Discord Rich Presence for Figma", "main": "src/main.js", "scripts": { "postinstall": "electron-builder install-app-deps", "start": "electron .", "pack": "electron-builder --dir", "dist": "electron-builder --mac --windows", "dist:mac": "electron-builder --mac", "dist:win": "electron-builder --windows", "publish": "electron-builder --mac --windows --publish always", "publish:mac": "electron-builder --mac --publish always", "publish:win": "electron-builder --windows --publish always" }, "repository": "", "keywords": [ "discord", "figma", "discord-presence", "discord-status", "discord-rpc", "electron" ], "author": "Bryan Berger", "license": "MIT", "dependencies": { "@bberger/win-info-fork": "^0.2.14", "@sentry/electron": "^2.5.1", "@sentry/integrations": "^6.10.0", "bplist-parser": "^0.3.0", "convict": "^6.2.0", "discord-rpc": "^4.0.1", "dotenv": "^10.0.0", "electron-log": "^4.3.5", "electron-notarize": "^1.0.0", "electron-updater": "^4.4.1", "fs-extra": "^10.0.0", "lodash": "^4.17.21", "ps-list": "^7.2.0" }, "devDependencies": { "electron": "^13.1.7", "electron-builder": "^22.11.8" }, "build": { "asar": true, "appId": "com.bryanberger.figma-discord-presence", "productName": "Figma Discord Presence", "afterSign": "./src/afterSignHook.js", "win": { "target": "nsis", "icon": "./build/icon.ico" }, "mac": { "hardenedRuntime": true, "entitlements": "./build/entitlements.mac.plist", "entitlementsInherit": "./build/entitlements.mac.plist", "icon": "./build/icon.icns", "target": [ { "target": "dmg" }, { "target": "zip" } ] }, "publish": { "provider": "s3", "bucket": "figma-discord-presence" } } } ================================================ FILE: src/afterSignHook.js ================================================ const fs = require("fs"); const path = require("path"); const electron_notarize = require("electron-notarize"); require("dotenv").config(); module.exports = async function (params) { // Only notarize the app on Mac OS only and on CI. if (process.platform !== "darwin" || process.env.NODE_ENV === "development") { return; } console.log("afterSign hook triggered", params); // Same appId in electron-builder. let appId = "com.bryanberger.figma-discord-presence"; let appPath = path.join( params.appOutDir, `${params.packager.appInfo.productFilename}.app` ); if (!fs.existsSync(appPath)) { throw new Error(`Cannot find application at: ${appPath}`); } console.log(`Notarizing ${appId} found at ${appPath}`); try { await electron_notarize.notarize({ ascProvider: process.env.APPLE_TEAM_ID, // adding this because I belong to multiple teams appBundleId: appId, appPath: appPath, appleId: process.env.APPLE_ID, // this is your apple ID it should be stored in an .env file appleIdPassword: process.env.APPLE_ID_PASSWORD, // this is NOT your apple ID password. You need to create an application specific password from https://appleid.apple.com under "security" you can generate such a password }); } catch (error) { console.error(error); } console.log(`Done notarizing ${appId}`); }; ================================================ FILE: src/lib/activity.js ================================================ const EventEmitter = require("events"); const RPC = require("discord-rpc"); const { getIsFigmaRunning, getFigmaMetaData, getIsFigmaActive, } = require("./figma"); const logger = require("./logger"); const config = require("./config"); const events = require("./events"); const CLIENT_ID = "866719067092418580"; class Activity extends EventEmitter { constructor() { super(); this.client = null; this.setActivityInterval = null; this.startTime = null; // if (config.get("connectOnStartup")) { // this.login(); // } } async login() { this.emit(events.DISCORD_CONNECTING); this.client = new RPC.Client({ transport: "ipc" }); this.client.on("ready", () => { this.emit(events.DISCORD_READY); this.setActivity(); this.startInterval(); }); this.client.on("disconnected", () => { this.emit(events.DISCORD_DISCONNECTED); this.destroy(); }); try { await this.client.login({ clientId: CLIENT_ID }); } catch (err) { logger.error("activity", err.message); this.emit(events.DISCORD_LOGIN_ERROR); this.client = null; } } async setActivity() { if (this.client === null) return; try { const isFigmaRunning = await getIsFigmaRunning(); if (isFigmaRunning) { if (!this.startTime) { this.startTime = new Date(); } } else { await this.client.clearActivity(); this.startTime = null; return; } const { currentFigmaFilename, shareLink } = await getFigmaMetaData(); if (currentFigmaFilename === null) { return; } const isFigmaActive = await getIsFigmaActive(); // Gather Config Options const isHideFilenames = config.get("hideFilenames"); const isHideStatus = config.get("hideStatus"); const isHideViewButton = config.get("hideViewButton"); // Build detail string const details = [ !isHideStatus ? (isFigmaActive ? "Active" : "Idle") : "", `${!isHideStatus && !isHideFilenames ? " " : ""}`, !isHideFilenames ? `in: "${currentFigmaFilename}"` : undefined, ]; // You'll need to have the logo asset uploaded to // https://discord.com/developers/applications//rich-presence/assets this.client.setActivity({ details: details.join("") || undefined, startTimestamp: this.startTime, largeImageKey: "logo", largeImageText: "Designing in Figma", buttons: !isHideViewButton && shareLink ? [{ label: "View in Figma", url: shareLink }] : undefined, instance: false, }); } catch (err) { logger.error("activity", `Failed to setActivity: ${err}`); } } startInterval() { this.setActivityInterval = setInterval(() => { this.setActivity(); }, 15e3); } async stopInterval() { clearInterval(this.setActivityInterval); this.setActivityInterval = null; this.startTime = null; } async updateOptions() { await this.setActivity(); } async connect() { await this.login(); } async disconnect() { await this.destroy(); } async destroy() { try { await this.client.clearActivity(); await this.client.destroy(); } catch {} this.client = null; this.stopInterval(); } } module.exports = Activity; ================================================ FILE: src/lib/config.js ================================================ const convict = require("convict"); const fs = require("fs-extra"); const logger = require("./logger"); const util = require("./util"); const retries = 10; const conf = convict({ hideFilenames: { doc: "Show or hide filenames", default: false, format: "Boolean", }, hideStatus: { doc: "Show or hide active/idle status", default: false, format: "Boolean", }, hideViewButton: { doc: "Show or hide the view in figma button", default: true, format: "Boolean", }, // connectOnStartup: { // doc: "Connect to Discord on application startup", // default: true, // format: "Boolean", // }, }); function load() { return new Promise(async (resolve, reject) => { try { const json = util.getAppData("/config.json"); conf.loadFile(json); logger.debug("config", "loaded!"); return resolve(conf.validate()); } catch (err) { reject(err); } }); } function save(times, init = false) { times = times || 0; const options = { spaces: 2 }; if (init) { options.flag = "wx"; } try { fs.writeJsonSync( util.getAppData("/config.json"), conf.getProperties(), options ); } catch (err) { // if any other error than 'File already exists' then retry. if (err.code !== "EEXIST") { logger.error("config", err.message); if (times < retries) { setTimeout(() => { save(times + 1); }, 1000); } } } logger.debug("config", "saved!"); } function getAll() { if (conf) { return conf.getProperties(); } } module.exports = conf; module.exports.load = load; module.exports.save = save; module.exports.getAll = getAll; ================================================ FILE: src/lib/events.js ================================================ module.exports = { QUIT: "QUIT", CONNECT: "CONNECT", DISCONNECT: "DISCONNECT", UPDATE_OPTIONS: "UPDATE_OPTIONS", CHECK_FOR_UPDATES: "CHECK_FOR_UPDATES", DISCORD_CONNECTING: "DISCORD_CONNECTING", DISCORD_READY: "DISCORD_READY", DISCORD_DISCONNECTED: "DISCORD_DISCONNECTED", DISCORD_LOGIN_ERROR: "DISCORD_LOGIN_ERROR", }; ================================================ FILE: src/lib/figma.js ================================================ const _ = require("lodash"); const bplist = require("bplist-parser"); const psList = require("ps-list"); const winInfo = require("@bberger/win-info-fork"); const fs = require("fs"); const logger = require("./logger"); const util = require("./util"); async function getFigmaMetaData() { let currentFigmaFilename = null; let shareLink = null; try { if (process.platform === "darwin") { const parsed = await bplist.parseFile( `${util.getHomePath()}/Library/Saved Application State/com.figma.Desktop.savedState/windows.plist` ); currentFigmaFilename = _.flattenDeep(parsed).find((o) => o.hasOwnProperty("NSTitle"))[ "NSTitle" ] || null; } else if (process.platform === "win32") { // Find the main Figma process first const processList = await psList(); const figmaProcesses = processList.filter((p) => p.name.includes("Figma.exe")) || []; // The main Figma process is the one that matches as a parent pid (ppid) from the others // Should only be 1 const mainFigmaProcess = _.intersectionWith( figmaProcesses, (a, b) => a.ppid === b.pid ).shift(); // Lookup its window title if (mainFigmaProcess) { try { const figmaWindow = winInfo.getByPidSync(mainFigmaProcess.pid); if (figmaWindow && figmaWindow.title.includes(" - Figma")) { currentFigmaFilename = figmaWindow.title.split(" - Figma")[0]; } } catch (err) {} } } if (currentFigmaFilename === null) { return { currentFigmaFilename, shareLink }; } const figmaDataFile = fs.readFileSync( `${util.getPath("appData")}/Figma/settings.json`, "utf-8" ); const figmaData = JSON.parse(figmaDataFile); const flatWindows = _.flattenDeep(figmaData.windows); flatWindows.map((window) => { window.tabs.map((tab) => { const { path, title, params } = tab; if (title === currentFigmaFilename) { // based on the current file name, we can lookup the id and generate a "view link" shareLink = encodeURI( `https://www.figma.com${path}/${params ? params : ""}` ); } }); }); } catch (err) { logger.error("figma", err.message); } return { currentFigmaFilename, shareLink }; } async function getIsFigmaRunning() { let isRunning = false; const processList = await psList(); if (process.platform === "darwin") { isRunning = processList.filter((p) => p.cmd.includes("Figma.app/Contents/MacOS/Figma") ).length > 0; } else if (process.platform === "win32") { isRunning = processList.filter((p) => p.name.includes("Figma.exe")).length > 0; } return isRunning; } async function getIsFigmaActive() { let isActive = false; try { const activeWin = await winInfo.getActive(); isActive = activeWin?.owner?.name.includes("Figma") || false; } catch (err) {} return isActive; } module.exports = { getFigmaMetaData, getIsFigmaRunning, getIsFigmaActive, }; ================================================ FILE: src/lib/logger.js ================================================ const Sentry = require("@sentry/electron"); const { CaptureConsole } = require("@sentry/integrations"); const log = require("electron-log"); log.transports.console.format = "[{h}:{i}:{s}.{ms}] {text}"; log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}.{ms}] {text}"; log.transports.file.level = "debug"; Sentry.init({ dsn: "https://b32c5d25554f4e6ebed361104462766a@o940691.ingest.sentry.io/5890015", integrations: [ new CaptureConsole({ levels: ["error"], }), ], ignoreErrors: ["in JSON at position"], }); module.exports = { info: function (tag, msg) { log.info(`[${tag}] [INFO] ${msg}`); }, debug: function (tag, msg) { log.debug(`[${tag}] [DEBUG] ${msg}`); }, error: function (tag, msg) { log.error(`[${tag}] [ERROR] ${msg}`); }, log, }; ================================================ FILE: src/lib/tray.js ================================================ const EventEmitter = require("events"); const path = require("path"); const { shell, nativeTheme, Menu, Tray } = require("electron"); const config = require("./config"); const logger = require("./logger"); const util = require("./util"); const events = require("./events"); const iconOn = path.join(__dirname, `/../../assets/on.png`); const iconOff = path.join(__dirname, `/../../assets/off.png`); // const iconOff = nativeImage.createFromDataURL(offUrl); // todo use nativeImage and png loader class CustomTray extends EventEmitter { tray = null; contextMenu = null; state = null; baseMenuTemplate = [ { type: "separator" }, { label: "Options", submenu: [ { label: "Hide filenames", type: "checkbox", checked: config.get("hideFilenames"), click: (menuItem) => this.saveConfigAndUpdate("hideFilenames", menuItem.checked), }, { label: "Hide active/idle status", type: "checkbox", checked: config.get("hideStatus"), click: (menuItem) => this.saveConfigAndUpdate("hideStatus", menuItem.checked), }, { label: 'Hide "View in Figma" button', type: "checkbox", checked: config.get("hideViewButton"), click: (menuItem) => this.saveConfigAndUpdate("hideViewButton", menuItem.checked), }, // { // label: "Connect to Discord when this app starts", // type: "checkbox", // checked: config.get("connectOnStartup"), // click: (menuItem) => // this.saveConfigAndUpdate("connectOnStartup", menuItem.checked), // }, ], }, { type: "separator" }, { label: "Check for Updates...", click: () => this.emit(events.CHECK_FOR_UPDATES), }, { label: "Show Config", click: () => shell.openPath(util.getAppDataPath()), }, { type: "separator" }, { label: "Exit", click: () => this.emit(events.QUIT), }, ]; constructor(trayState) { super(); logger.debug("tray", "initalized"); this.state = trayState; this.tray = new Tray(this.getIconPath()); this.update(); nativeTheme.on("updated", () => this.update()); } getIconPath() { const iconState = this.state.isDiscordReady ? "On" : "Off"; if (process.platform === "darwin") { return path.join(__dirname, `/../../assets/Icon${iconState}Template.png`); } else if (process.platform === "win32") { // always use the darkmode icon on windows, taskbar seems to be dark regardless of theme return path.join(__dirname, `/../../assets/Icon${iconState}Windows.png`); } } update() { let menuTemplate; if (this.state.isDiscordReady) { menuTemplate = [ { label: `Connected to Discord`, enabled: false, icon: iconOn, }, { type: "separator" }, { label: "Disconnect from Discord", click: () => this.emit(events.DISCONNECT), }, ].concat(this.baseMenuTemplate); } else { if (this.state.isDiscordConnecting) { menuTemplate = [ { label: "Connecting to Discord", enabled: false, icon: iconOff, }, { type: "separator" }, { label: "Stop connecting to Discord", click: () => this.emit(events.DISCONNECT), }, ].concat(this.baseMenuTemplate); } else { menuTemplate = [ { label: "Not connected to Discord", enabled: false, icon: iconOff, }, { type: "separator" }, { label: "Connect to Discord", click: () => this.emit(events.CONNECT), }, ].concat(this.baseMenuTemplate); } } this.tray.setContextMenu(Menu.buildFromTemplate(menuTemplate)); this.tray.setImage(this.getIconPath()); } saveConfigAndUpdate(configKey, value) { config.set(configKey, value); config.save(); // immeditely try a discord activity update this.emit(events.UPDATE_OPTIONS); } setState(state) { this.state = state; this.update(); } } module.exports = CustomTray; ================================================ FILE: src/lib/updater.js ================================================ const { autoUpdater } = require("electron-updater"); const { app, dialog } = require("electron"); const logger = require("./logger"); autoUpdater.autoDownload = false; autoUpdater.on("error", (error) => { dialog.showErrorBox( "Error: ", error == null ? "unknown" : (error.stack || error).toString() ); }); autoUpdater.on("update-available", async () => { const { response } = await dialog.showMessageBox({ type: "question", title: "Found Updates", message: "Found a new version, do you want update now?", defaultId: 0, cancelId: 1, buttons: ["Yes", "No"], }); if (response === 0) { logger.debug("updater", "update available"); await autoUpdater.downloadUpdate(); } }); autoUpdater.on("update-downloaded", async () => { const { response } = await dialog.showMessageBox({ type: "question", title: "Update Download", buttons: ["Install and Relaunch", "Later"], defaultId: 0, cancelId: 1, message: `A new version of ${app.getName()} has been downloaded!`, }); if (response === 0) { setImmediate(() => autoUpdater.quitAndInstall()); } }); autoUpdater.on("download-progress", (progressObj) => logger.debug( "updater", `Update Download progress: ${JSON.stringify(progressObj)}` ) ); async function _simpleCheck() { const { updateInfo } = await _update(); const currentVersion = app.getVersion(); logger.debug( "updater", `Current Version: ${currentVersion} | Server Version: ${updateInfo.version}` ); if (updateInfo && updateInfo.version === currentVersion) { await dialog.showMessageBox({ type: "info", message: "You're up-to-date!", detail: `${app.getName()} ${ updateInfo.version } is currently the newest version available.`, }); } } async function _update() { return new Promise(async (resolve, reject) => { try { autoUpdater.logger = logger.log; // return resolve(autoUpdater.checkForUpdatesAndNotify()); return resolve(autoUpdater.checkForUpdates()); } catch (err) { reject(err); } }); } exports.update = _update; exports.simpleCheck = _simpleCheck; ================================================ FILE: src/lib/util.js ================================================ const { app } = require("electron"); const path = require("path"); function _getAppData(subpath) { return path.join(_getAppDataPath(), subpath); } function _getAppDataPath() { return app.getPath("userData"); } function _getHomePath() { return app.getPath("home"); } function _getPath(path) { return app.getPath(path) } function _timeout(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } exports.getAppData = _getAppData; exports.getAppDataPath = _getAppDataPath; exports.getHomePath = _getHomePath; exports.getPath = _getPath; exports.timeout = _timeout; ================================================ FILE: src/main.js ================================================ const electron = require("electron"); const { dialog } = electron; const psList = require("ps-list"); const events = require("./lib/events"); const updater = require("./lib/updater"); const config = require("./lib/config"); const logger = require("./lib/logger"); const CustomTray = require("./lib/tray"); const Activity = require("./lib/activity"); const { app } = electron; let tray, activity; const state = { isDiscordConnecting: false, isDiscordReady: false, isFigmaReady: false, }; async function quit() { logger.debug("main", "quitting..."); if (activity) await activity.destroy(); app.quit(); } if (!app.requestSingleInstanceLock()) { logger.debug("main", "second instance detected, quitting this one..."); app.quit(); } if (process.platform === "darwin") app.dock.hide(); app .whenReady() .then(() => updater.update()) .then(() => config.save(0, true)) .then(() => config.load()) .then(() => (tray = new CustomTray(state))) .then(() => (activity = new Activity())) .then(() => registerEvents()) .then(() => logger.debug("main", "initalized!")) .catch((err) => logger.error("main", err.message)); function registerEvents() { tray.on(events.QUIT, async () => await quit()); tray.on(events.UPDATE_OPTIONS, async () => { await activity.updateOptions(); }); tray.on(events.CONNECT, async () => { await activity.connect(); }); tray.on(events.DISCONNECT, async () => { await activity.disconnect(); }); tray.on(events.CHECK_FOR_UPDATES, async () => { await updater.simpleCheck(); }); activity.on(events.DISCORD_CONNECTING, () => { logger.debug("main", "discord connecting..."); state.isDiscordReady = false; state.isDiscordConnecting = true; tray.setState(state); }); activity.on(events.DISCORD_READY, () => { logger.debug("main", "discord ready!"); state.isDiscordReady = true; state.isDiscordConnecting = false; tray.setState(state); }); activity.on(events.DISCORD_DISCONNECTED, () => { logger.debug("main", "discord disconnected"); state.isDiscordReady = false; state.isDiscordConnecting = false; tray.setState(state); }); activity.on(events.DISCORD_LOGIN_ERROR, async () => { // Is Discord open? let isRunning = false; const processList = await psList(); if (process.platform === "darwin") { isRunning = processList.filter((p) => p.cmd.includes("MacOS/Discord")).length > 0; } else if (process.platform === "win32") { isRunning = processList.filter((p) => p.name.includes("Discord.exe")).length > 0; } if (!isRunning) { dialog.showErrorBox( "Figma Discord Presence", "Unfortunately it doesn't look like Discord is running. It must be running in order to connect and update your presence status." ); } state.isDiscordReady = false; state.isDiscordConnecting = false; tray.setState(state); }); } app.on("window-all-closed", () => { // should not quit }); process.on("unhandledRejection", (err) => logger.error("unhandledRejection", err.message) ); process.on("uncaughtException", (err) => logger.error("uncaughtException", err.message) );