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