[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.yml]\nindent_style = space\nindent_size = 2\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for more information:\n# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\n# https://containers.dev/guide/dependabot\n\nversion: 2\nupdates:\n - package-ecosystem: \"devcontainers\"\n   directory: \"/\"\n   schedule:\n     interval: weekly\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and publish\non:\n  push:\n    tags:\n      - '*'\njobs:\n  tests:\n    uses: ./.github/workflows/tests.yml\n  build:\n    needs: [tests]\n    runs-on: ${{ matrix.os }}\n    strategy:\n      matrix:\n        os:\n          - macos-latest\n          - ubuntu-latest\n          - windows-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Install dependencies\n        run: npm ci\n      - name: Build Caprine\n        run: npm run build\n      - name: Cleanup tag\n        uses: mad9000/actions-find-and-replace-string@5\n        id: release_tag\n        with:\n          source: ${{ github.ref_name }}\n          find: v\n          replace: ''\n      - name: Install Snapcraft\n        uses: samuelmeuli/action-snapcraft@v1\n        if: startsWith(matrix.os, 'ubuntu')\n      - name: Package Caprine for macOS\n        if: startsWith(matrix.os, 'macos')\n        run: npm run dist:mac\n        env:\n          CSC_LINK: ${{ secrets.CSC_LINK }}\n          CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Package Caprine for Windows\n        if: startsWith(matrix.os, 'windows')\n        run: npm run dist:win\n        env:\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Package Caprine for Linux\n        if: startsWith(matrix.os, 'ubuntu')\n        run: npm run dist:linux\n        env:\n          SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.snapcraft_token }}\n          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Upload to Gemfury\n        if: startsWith(matrix.os, 'ubuntu')\n        run: curl -F package=@dist/caprine_${{ steps.release_tag.outputs.value }}_amd64.deb https://${{ secrets.gemfury_token }}@push.fury.io/lefterisgar/\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches-ignore:\n      - gh-pages\n  pull_request:\n  workflow_call:\n\njobs:\n  npm-cache:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Install dependencies\n        run: npm ci\n  tsc:\n    runs-on: ubuntu-latest\n    needs: npm-cache\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Compile TypeScript\n        run: |\n          npm ci\n          npm run test:tsc\n  xo:\n    runs-on: ubuntu-latest\n    needs: npm-cache\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Lint source code\n        run: |\n          npm ci\n          npm run lint:xo\n  stylelint:\n    runs-on: ubuntu-latest\n    needs: npm-cache\n    steps:\n      - uses: actions/checkout@v4\n      - name: Setup Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Lint styles\n        run: |\n          npm ci\n          npm run lint:stylelint\n  rpmspec:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - name: Lint rpm spec file\n        uses: EyeCantCU/rpmlint-action@v0.1.1\n        with:\n          rpmfiles: packages/rpm/caprine.spec\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nyarn.lock\n/dist\n/dist-js\nmas.provisionprofile\n.vscode\n.DS_store\n/.devcontainer\n"
  },
  {
    "path": ".nvmrc",
    "content": "lts/hydrogen\n"
  },
  {
    "path": ".prettierignore",
    "content": "*.md\n"
  },
  {
    "path": "build/entitlements.mac.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"https://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>com.apple.security.cs.allow-jit</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-unsigned-executable-memory</key>\n\t<true/>\n\t<key>com.apple.security.cs.allow-dyld-environment-variables</key>\n\t<true/>\n\t<key>com.apple.security.device.camera</key>\n\t<true/>\n\t<key>com.apple.security.device.audio-input</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "css/autoplay.css",
    "content": ".disabledAutoPlayImgTopRadius {\n\tborder-top-left-radius: 1.3em;\n\tborder-top-right-radius: 1.3em;\n}\n\n.disabledAutoPlayImgBottomRadius {\n\tborder-bottom-left-radius: 1.3em;\n\tborder-bottom-right-radius: 1.3em;\n}\n"
  },
  {
    "path": "css/browser.css",
    "content": ":root {\n\t--selected-conversation-background: linear-gradient(hsla(209deg 110% 45% / 90%), hsla(209deg 110% 42% / 90%));\n\t--selected-conversation-background-inactive: #d2d2d2;\n\t--black: #000;\n}\n\nhtml {\n\toverflow: hidden;\n}\n\n/* Add OS-specific fonts */\nbody {\n\tfont-family:\n\t\t-apple-system,\n\t\tBlinkMacSystemFont,\n\t\t'Segoe UI',\n\t\tRoboto,\n\t\tOxygen-Sans,\n\t\tUbuntu,\n\t\tCantarell,\n\t\t'Helvetica Neue',\n\t\tsans-serif,\n\t\t'Apple Color Emoji',\n\t\t'Segoe UI Emoji',\n\t\t'Segoe UI Symbol' !important;\n\ttext-rendering: optimizelegibility !important;\n\tfont-feature-settings: 'liga', 'clig', 'kern';\n}\n\n/* Bind the toolbar as the window's draggable region */\n/* Bar above conversation list */\n[role='navigation'] .x78zum5.xdt5ytf.xzd29fr {\n\t-webkit-app-region: drag;\n}\n/* New message button */\n[role='navigation'] .x16n37ib {\n\t-webkit-app-region: no-drag;\n}\n/* Bar above chat */\n[role='main'] .x1u998qt.x1vjfegm {\n\t-webkit-app-region: drag;\n}\n/* Conversation name */\n[role='main'] .x1i10hfl.x1qjc9v5.xjbqb8w.xjqpnuy.xa49m3k.xqeqjp1.x2hbi6w.x13fuv20.xu3j5b3.x1q0q8m5.x26u7qi.x972fbf.xcfux6l.x1qhh985.xm0m39n.x9f619.x1ypdohk.xdl72j9.x2lah0s.xe8uvvx.xdj266r.x11i5rnm.xat24cr.x1mh8g0r.x2lwn1j.xeuugli.xexx8yu.x4uap5.x18d9i69.xkhd6sd.x1n2onr6.x16tdsg8.x1hl2dhg.xggy1nq.x1ja2u2z.x1t137rt.x1o1ewxj.x3x9cwd.x1e5q0jg.x13rtm0m.x1q0g3np.x87ps6o.x1lku1pv.x1a2a7pz.x78zum5 {\n\t-webkit-app-region: no-drag;\n}\n/* Top right buttons */\n[role='main'] .x9f619.x1n2onr6.x1ja2u2z.x78zum5.x2lah0s.x1qughib.x6s0dn4.xozqiw3.x1q0g3np.xykv574.xbmpl8g.x4cne27.xifccgj {\n\t-webkit-app-region: no-drag;\n}\n/* View label above conversation list margin */\n.os-darwin [role='navigation'] .x1heor9g.x1qlqyl8.x1pd3egz.x1a2a7pz {\n\tmargin-left: 60px;\n}\n\n/* Hide footer at login view */\n._210n {\n\tdisplay: none;\n}\n\n/* Don't show outline on clickable elements & input fields */\n*[role='button'],\n*[type='text'],\n*[type='password'] {\n\toutline: none !important;\n}\n\n[role='navigation'] a {\n\tcursor: default !important;\n}\n\n/* Remove top Facebook cookie banner */\n.fbPageBanner {\n\tdisplay: none !important;\n}\n\n/* Cookies notification: Adjust size for smaller windows */\n._9o-g {\n\theight: 290px !important;\n}\n._9xo5 {\n\tpadding-top: 12px !important;\n}\n._59s7._9l2g {\n\theight: 498px !important;\n}\n\n/* Cookies notification: Remove \"allow all cookies\" button */\n._42ft._4jy0._9xo7._4jy3._4jy1.selected._51sy {\n\tdisplay: none;\n}\n\n/* Cookies notification: accept button */\n._42ft._4jy0._9xo6._4jy3._4jy1.selected._51sy {\n\tbackground-color: #1877f2 !important;\n\tcolor: var(--white) !important;\n}\n\n/* Hide disabled scrollbar on the right side */\nbody::-webkit-scrollbar {\n\tdisplay: none;\n}\n\n/* A utility class for temporarily hiding all dropdown menus */\nhtml.hide-dropdowns [role='menu'].x1n2onr6.xi5betq {\n\tvisibility: hidden !important;\n}\n\n/* A utility class for temporarily hiding preferences window */\nhtml.hide-preferences-window div[class='x9f619 x1n2onr6 x1ja2u2z'] > div:nth-of-type(3) > div > div {\n\tdisplay: none;\n}\n\n/* -- Private mode -- */\n/* Preferences button: profile picture */\nhtml.private-mode [role='navigation'] .qi72231t.o9w3sbdw.nu7423ey.tav9wjvu.flwp5yud.tghlliq5.gkg15gwv.s9ok87oh.s9ljgwtm.lxqftegz.bf1zulr9.frfouenu.bonavkto.djs4p424.r7bn319e.bdao358l.fsf7x5fv.tgm57n0e.jez8cy9q.s5oniofx.m8h3af8h.l7ghb35v.kjdc1dyq.kmwttqpk.dnr7xe2t.aeinzg81.srn514ro.oxkhqvkx.rl78xhln.nch0832m.om3e55n1.cr00lzj9.rn8ck1ys.s3jn8y49.g4tp4svg.o9erhkwx.dzqi5evh.hupbnkgi.hvb2xoa8.fxk3tzhb.jl2a5g8c.f14ij5to.l3ldwz01.icdlwmnq {\n\tfilter: blur(5px);\n}\n/* Preferences: profile picture */\nhtml.private-mode .alzwoclg.b0eko5f3.q46jt4gp.r5g9zsuq .aglvbi8b.om3e55n1.i8zpp7h3.g4tp4svg {\n\tfilter: blur(5px);\n}\n/* Preferences: account name */\nhtml.private-mode [href^='https://www.facebook.com/1'] .b6ax4al1.i54nktwv.z2vv26z9.om3e55n1.gvxzyvdx.aeinzg81.t7p7dqev.gh25dzvf.gem102v4.ncib64c9.mrvwc6qr.sx8pxkcf.f597kf1v.cpcgwwas.ocv3nf92.k1z55t6l.tpi2lg9u.pbevjfx6.ztn2w49o.f5mw3jnl.ib8x7mpr.qc5lal2y {\n\tfilter: blur(5px);\n}\n/* Chat list: name, profile picture and last message */\nhtml.private-mode [role='navigation'] [role='row'] .b6ax4al1.gvxzyvdx {\n\tfilter: blur(5px);\n}\n/* Chat list: person tiny heads */\nhtml.private-mode [role='row'] .aglvbi8b.om3e55n1.i8zpp7h3.g4tp4svg {\n\tfilter: blur(3px);\n}\n/* Conversation: titlebar profile picture */\nhtml.private-mode .b6ax4al1.gvxzyvdx.dgxim35p.p9wrh9lq {\n\tfilter: blur(5px);\n}\n/* Conversation: sender profile picture */\nhtml.private-mode .mfclru0v.p9wrh9lq.pytsy3co.aglvbi8b {\n\tfilter: blur(5px);\n}\n/* Conversation: name & last seen */\nhtml.private-mode [role='main'] .alzwoclg.cqf1kptm.hael596l.jcxyg2ei.cgu29s5g.dn6jqzda {\n\tfilter: blur(5px);\n}\n/* Conversation: read indicator */\nhtml.private-mode [role='main'] .iec8yc8l.b7mnygb8.dktd5soj.f14ij5to.qmqpeqxj.e7u6y3za.qwcclf47.nmlomj2f {\n\tfilter: blur(5px);\n}\n/* Conversation: name & details at the beginning */\nhtml.private-mode .h6ft4zvz.rj2hsocd.aesu6q9g.e4ay1f3w .hsphh064.pk1vzqw1.hxfwr5lz.qc5lal2y {\n\tfilter: blur(5px);\n}\n/* Right sidebar: profile picture */\nhtml.private-mode .aglvbi8b.om3e55n1.i8zpp7h3.g4tp4svg {\n\tfilter: blur(5px);\n}\n/* Right sidebar: name */\nhtml.private-mode [role='main'] .qi72231t.nu7423ey.n3hqoq4p.r86q59rh.b3qcqh3k.fq87ekyn.bdao358l.fsf7x5fv.rse6dlih.s5oniofx.m8h3af8h.l7ghb35v.kjdc1dyq.kmwttqpk.srn514ro.oxkhqvkx.rl78xhln.nch0832m.cr00lzj9.rn8ck1ys.s3jn8y49.icdlwmnq.jxuftiz4.cxfqmxzd {\n\tfilter: blur(5px);\n}\n/* Right sidebar: active status */\nhtml.private-mode [role='main'] .b6ax4al1.i54nktwv.z2vv26z9.om3e55n1.gvxzyvdx.aeinzg81.t7p7dqev.gh25dzvf.gem102v4.ncib64c9.mrvwc6qr.sx8pxkcf.f597kf1v.cpcgwwas.ocv3nf92.nfkogyam.gh55jysx.rtxb060y.hsphh064.pk1vzqw1.hxfwr5lz.qc5lal2y {\n\tfilter: blur(5px);\n}\n/* New conversation: profile picture */\nhtml.private-mode .mfclru0v.pytsy3co.qmqpeqxj.e7u6y3za.qwcclf47.nmlomj2f.i8zpp7h3.p9wrh9lq {\n\tfilter: blur(5px);\n}\n/* New conversation: profile picture (groups) */\nhtml.private-mode .qmqpeqxj.e7u6y3za.qwcclf47.nmlomj2f.s8sjc6am {\n\tfilter: blur(5px);\n}\n/* Calls: incoming call dialog account name */\nhtml.private-mode .b6ax4al1.i54nktwv.z2vv26z9.om3e55n1.gvxzyvdx.aeinzg81.t7p7dqev.gh25dzvf.gem102v4.ncib64c9.mrvwc6qr.sx8pxkcf.f597kf1v.cpcgwwas.ocv3nf92.qntmu8s7.o48pnaf2.pbevjfx6.hsphh064.m2nijcs8.pc9ouhwb.qc5lal2y,\nhtml.private-mode .gvxzyvdx.aeinzg81.t7p7dqev.gh25dzvf.rse6dlih.ocv3nf92.nfkogyam.innypi6y.rtxb060y.qc5lal2y {\n\tfilter: blur(10px);\n}\n\n/* Force max-width on videos */\n.ni8dbmo4.stjgntxs.g5ia77u1.ii04i59q.j83agx80.cbu4d94t.ll8tlv6m > span,\n.l9j0dhe7.km676qkl.cxmmr5t8.myj7ivm5.hcukyx3x,\n.opwvks06.hop1g133.linmgsc8.t63ysoy8.qutah8gn.ni8dbmo4.stjgntxs.ktxn16wu.jz9ahs1c.efwgsih4.e72ty7fz.qmr60zad.qlfml3jp.inkptoze {\n\tmax-width: 100%;\n}\n\n/* Hide the \"Messenger App for Mac/Windows\" banner in chat list */\n.x9f619.x1n2onr6.x1ja2u2z.x78zum5.x1r8uery.xs83m0k.xeuugli.x1qughib.x6s0dn4.xozqiw3.x1q0g3np.xknmibj.x1c4vz4f.xt55aet.xexx8yu.xc73u3c.x18d9i69.x5ib6vp.x1lku1pv.xzd29fr {\n\tdisplay: none;\n}\n/* Hide the \"Messenger for Mac/Windows\" menu item and separator in Messenger settings */\n.x4k7w5x.x1h91t0o.x1beo9mf.xaigb6o.x12ejxvf.x3igimt.xarpa2k.xedcshv.x1lytzrv.x1t2pt76.x7ja8zs.x1n2onr6.x1qrby5j.x1jfb8zj > div > div:last-of-type > a {\n\tdisplay: none;\n}\n\n/* Dragable region for macOS */\n.os-darwin .rq0escxv.l9j0dhe7.du4w35lb.j83agx80.pfnyh3mw.i1fnvgqd.bp9cbjyn.owycx6da.btwxx1t3.jei6r52m.wkznzc2l.n851cfcs.dhix69tm.ahb00how,\n.os-darwin .rq0escxv.l9j0dhe7.du4w35lb.j83agx80.pfnyh3mw.i1fnvgqd.bp9cbjyn.owycx6da.btwxx1t3.hv4rvrfc.dati1w0a.f10w8fjw.pybr56ya.b5q2rw42.lq239pai.mysgfdmx.hddg9phg {\n\t-webkit-app-region: drag !important;\n}\n.os-darwin .rq0escxv.l9j0dhe7.du4w35lb.j83agx80.pfnyh3mw.i1fnvgqd.bp9cbjyn.owycx6da.btwxx1t3.jei6r52m.wkznzc2l.n851cfcs.dhix69tm.ahb00how {\n\tmargin: 0 !important;\n\tpadding: 12px 16px 0 !important;\n}\n.os-darwin .rq0escxv.l9j0dhe7.du4w35lb.j83agx80.cbu4d94t.g5gj957u.d2edcug0.hpfvmrgz.kud993qy.buofh1pr {\n\tmargin-top: 24px !important;\n}\n\n@media (max-width: 900px) {\n\t.os-darwin .rq0escxv.l9j0dhe7.du4w35lb.jbae33se.hv4rvrfc.dati1w0a.pybr56ya {\n\t\tdisplay: none !important;\n\t}\n\t.os-darwin .rpm2j7zs.k7i0oixp.gvuykj2m.j83agx80.cbu4d94t.ni8dbmo4.du4w35lb.q5bimw55.ofs802cu.pohlnb88.dkue75c7.mb9wzai9.d8ncny3e.buofh1pr.g5gj957u.tgvbjcpo.l56l04vs.r57mb794.kh7kg01d.eg9m0zos.c3g1iek1.l9j0dhe7.k4xni2cv {\n\t\tpadding-top: 36px !important;\n\t}\n}\n\n/* -- Sidebar views -- */\n\n/* Hidden: Hide sidebar */\nhtml.sidebar-hidden .bdao358l.om3e55n1.alzwoclg.cqf1kptm.gvxzyvdx.aeinzg81.jez8cy9q.fawcizw8.sl4bvocy.mm98tyaj.b0ur3jhr.f76nr8pf {\n\tdisplay: none;\n}\n\n/* Narrow: Hide preferences button */\nhtml.sidebar-force-narrow .pvreidsc.r227ecj6.n68fow1o.gt60zsk1.lth9pzmp {\n\tdisplay: none;\n}\n/* Narrow: Hide conversation previews */\nhtml.sidebar-force-narrow .bdao358l.om3e55n1.g4tp4svg.alzwoclg.cqf1kptm.jez8cy9q.gvxzyvdx.aeinzg81.cgu29s5g {\n\tdisplay: none;\n}\n/* Narrow: Hide search bar */\nhtml.sidebar-force-narrow .bdao358l.om3e55n1.g4tp4svg.r227ecj6.gt60zsk1.rj2hsocd.f9xcifuu {\n\tdisplay: none;\n}\n/* Narrow: Width of conversation list */\nhtml.sidebar-force-narrow .bdao358l.om3e55n1.alzwoclg.cqf1kptm.gvxzyvdx.aeinzg81.jez8cy9q.fawcizw8.sl4bvocy.mm98tyaj.b0ur3jhr.f76nr8pf {\n\twidth: 80px;\n}\n\n/* -- Toggle message buttons -- */\nbody .qi72231t.o9w3sbdw.nu7423ey.tav9wjvu.flwp5yud.tghlliq5.gkg15gwv.s9ok87oh.s9ljgwtm.lxqftegz.bf1zulr9.frfouenu.bonavkto.djs4p424.r7bn319e.bdao358l.fsf7x5fv.tgm57n0e.jez8cy9q.s5oniofx.m8h3af8h.l7ghb35v.kjdc1dyq.dnr7xe2t.aeinzg81.om3e55n1.cr00lzj9.rn8ck1ys.s3jn8y49.g4tp4svg.o9erhkwx.dzqi5evh.hupbnkgi.hvb2xoa8.fxk3tzhb.jl2a5g8c.f14ij5to.l3ldwz01.icdlwmnq.q46jt4gp.b0eko5f3.r5g9zsuq.fwlpnqze.jbg88c62,\nbody .s8sjc6am.z6erz7xo.alzwoclg.jcxyg2ei.i85zmo3j.b0ur3jhr.kjdc1dyq.tjkoo78o.iwr3bmeu.qgj99rie {\n\tdisplay: flex;\n}\n\nbody.show-message-buttons .qi72231t.o9w3sbdw.nu7423ey.tav9wjvu.flwp5yud.tghlliq5.gkg15gwv.s9ok87oh.s9ljgwtm.lxqftegz.bf1zulr9.frfouenu.bonavkto.djs4p424.r7bn319e.bdao358l.fsf7x5fv.tgm57n0e.jez8cy9q.s5oniofx.m8h3af8h.l7ghb35v.kjdc1dyq.dnr7xe2t.aeinzg81.om3e55n1.cr00lzj9.rn8ck1ys.s3jn8y49.g4tp4svg.o9erhkwx.dzqi5evh.hupbnkgi.hvb2xoa8.fxk3tzhb.jl2a5g8c.f14ij5to.l3ldwz01.icdlwmnq.q46jt4gp.b0eko5f3.r5g9zsuq.fwlpnqze.jbg88c62,\nbody.show-message-buttons .s8sjc6am.z6erz7xo.alzwoclg.jcxyg2ei.i85zmo3j.b0ur3jhr.kjdc1dyq.tjkoo78o.iwr3bmeu.qgj99rie {\n\tdisplay: none;\n}\n\nbody.show-message-buttons .cgu29s5g.alzwoclg.oo5upp5e {\n\tmargin-left: 15px !important;\n}\n"
  },
  {
    "path": "css/code-blocks.css",
    "content": "/* Tomorrow light theme for code blocks */\n._wu0 {\n\t--code-block-base: #1d1f21;\n\t--code-block-background: transparent;\n\t--code-block-border: rgb(0 0 0 / 10%);\n\t--code-block-primary: #de935f;\n\t--code-block-meta: #969896;\n\t--code-block-tag: #a3685a;\n\t--code-block-quoted: #b5bd68;\n\t--code-block-variable: #c66;\n\t--code-block-special: #8abeb7;\n\t--code-block-attr-value: #139543;\n\t--code-block-keyword: #b294bb;\n\t--code-block-function: #81a2be;\n\tbackground-color: var(--code-block-background) !important;\n\tborder: 1px solid var(--code-block-border) !important;\n\tcolor: var(--code-block-base) !important;\n}\n._wu0 .token.punctuation {\n\tcolor: var(--code-block-base) !important;\n}\n._wu0 .token.property {\n\tcolor: var(--code-block-base) !important;\n}\n._wu0 .token.operator {\n\tcolor: var(--code-block-base) !important;\n}\n._wu0 .token.boolean {\n\tcolor: var(--code-block-primary) !important;\n}\n._wu0 .token.number {\n\tcolor: var(--code-block-primary) !important;\n}\n._wu0 .token.constant {\n\tcolor: var(--code-block-primary) !important;\n}\n._wu0 .token.selector {\n\tcolor: var(--code-block-primary) !important;\n}\n._wu0 .token.bold {\n\tcolor: var(--code-block-primary) !important;\n\tfont-weight: bold;\n}\n._wu0 .token.comment {\n\tcolor: var(--code-block-meta) !important;\n}\n._wu0 .token.prolog {\n\tcolor: var(--code-block-meta) !important;\n}\n._wu0 .token.doctype {\n\tcolor: var(--code-block-meta) !important;\n}\n._wu0 .token.cdata {\n\tcolor: var(--code-block-meta) !important;\n}\n._wu0 .token.tag {\n\tcolor: var(--code-block-tag) !important;\n}\n._wu0 .token.symbol {\n\tcolor: var(--code-block-quoted) !important;\n}\n._wu0 .token.string {\n\tcolor: var(--code-block-quoted) !important;\n}\n._wu0 .token.char {\n\tcolor: var(--code-block-quoted) !important;\n}\n._wu0 .token.inserted {\n\tcolor: var(--code-block-quoted) !important;\n}\n._wu0 .token.attr-name {\n\tcolor: var(--code-block-variable) !important;\n}\n._wu0 .token.url {\n\tcolor: var(--code-block-variable) !important;\n}\n._wu0 .token.entity {\n\tcolor: var(--code-block-variable) !important;\n}\n._wu0 .token.variable {\n\tcolor: var(--code-block-variable) !important;\n}\n._wu0 .token.deleted {\n\tcolor: var(--code-block-variable) !important;\n}\n._wu0 .token.builtin {\n\tcolor: var(--code-block-special) !important;\n}\n._wu0 .token.hexcode {\n\tcolor: var(--code-block-special) !important;\n}\n._wu0 .token.regex {\n\tcolor: var(--code-block-special) !important;\n}\n._wu0 .token.attr-value {\n\tcolor: var(--code-block-attr-value) !important;\n}\n._wu0 .token.keyword {\n\tcolor: var(--code-block-keyword) !important;\n}\n._wu0 .token.important {\n\tcolor: var(--code-block-keyword) !important;\n}\n._wu0 .token.italic {\n\tcolor: var(--code-block-keyword) !important;\n\tfont-style: italic;\n}\n._wu0 .token.function {\n\tcolor: var(--code-block-function) !important;\n}\n\n/* Tomorrow dark theme for code blocks */\nhtml.dark-mode ._wu0 {\n\t--code-block-base: #c5c8c6;\n\t--code-block-border: var(--base-ten);\n\tcolor: var(--base);\n}\n\n/* Full-window vibrancy */\nhtml.full-vibrancy ._wu0 {\n\t--code-block-background: #fff;\n\t--code-block-border: transparent;\n}\nhtml.full-vibrancy.dark-mode ._wu0 {\n\t--code-block-background: var(--container-color);\n}\n"
  },
  {
    "path": "css/dark-mode.css",
    "content": ":root {\n\t--base: #000;\n\t--base-ninety: rgb(255 255 255 / 90%);\n\t--base-seventy-five: rgb(255 255 255 / 75%);\n\t--base-seventy: rgb(255 255 255 / 70%);\n\t--base-fifty: rgb(255 255 255 / 50%);\n\t--base-fourty: rgb(255 255 255 / 40%);\n\t--base-thirty: rgb(255 255 255 / 30%);\n\t--base-twenty: rgb(255 255 255 / 20%);\n\t--base-ten: rgb(255 255 255 / 10%);\n\t--base-nine: rgb(255 255 255 / 9%);\n\t--base-five: rgb(255 255 255 / 5%);\n\t--container-color: #323232;\n\t--container-dark-color: #1e1e1e;\n\t--list-header-color: #222;\n\t--blue: #0084ff;\n\t--white: #fff;\n}\n\nhtml.dark-mode body {\n\tcolor: var(--base-seventy);\n\tbackground: var(--container-color) !important;\n}\n\n/* Fixes appearance of \"Verify Account\" screen text */\nhtml.dark-mode ._3-mr ._3-mt,\nhtml.dark-mode ._3-mr ._3-mu {\n\tcolor: #fff;\n}\n\nhtml.dark-mode ._3v_o, /* Login screen */\nhtml.dark-mode body.UIPage_LoggedOut ._li, /* 2FA screen */\nhtml.dark-mode body.UIPage_LoggedOut ._4-u5 /* 2FA screen */ {\n\tbackground-color: var(--container-dark-color);\n}\n\n/* Login title and names */\nhtml.dark-mode ._5hy4,\nhtml.dark-mode ._3403 {\n\tcolor: var(--base-fourty) !important;\n}\n\n/* Login inputs */\nhtml.dark-mode ._3v_o ._55r1 {\n\tbackground: var(--base-five);\n\tcolor: var(--base-seventy);\n}\nhtml.dark-mode ._3v_o ._55r1::-webkit-input-placeholder {\n\tcolor: var(--base-thirty) !important;\n}\n\n/* \"Keep me signed in\" checkbox */\nhtml.dark-mode .uiInputLabelInput {\n\tfilter: opacity(70%);\n}\n\n/* \"Keep me signed in\" text */\nhtml.dark-mode .uiInputLabelLabel {\n\tcolor: var(--base-fourty) !important;\n}\n\n/* 2FA screen modal */\nhtml.dark-mode body.UIPage_LoggedOut ._4-u8 {\n\tbackground: var(--container-color);\n\tborder-color: var(--base-five) !important;\n}\n\n/* 2FA screen modal title */\nhtml.dark-mode body.UIPage_LoggedOut ._2e9n {\n\tborder-color: var(--base-five);\n\tcolor: #fff;\n}\n\n/* 2FA screen modal separator */\nhtml.dark-mode body.UIPage_LoggedOut ._p0k ._5hzs {\n\tborder-color: var(--base-five);\n}\n\n/* 2FA screen modal separators */\nhtml.dark-mode body.UIPage_LoggedOut a {\n\tcolor: var(--blue);\n}\n\n/* 2FA screen modal input */\nhtml.dark-mode body.UIPage_LoggedOut input {\n\tbackground: var(--base-ten);\n\tborder-color: var(--base-ten);\n\tcolor: var(--base-ninety);\n}\n\n/* Cookies notification: background */\nhtml.dark-mode ._9o-w ._9o-c {\n\tbackground: var(--container-color) !important;\n}\n/* Cookies notification: text */\nhtml.dark-mode ._9o-g {\n\tcolor: var(--base-seventy) !important;\n}\n/* Cookies notification: collapsible headers */\nhtml.dark-mode ._9o-l {\n\tcolor: var(--base-seventy) !important;\n}\n/* Cookies notification: subheaders */\nhtml.dark-mode ._9si- {\n\tcolor: var(--base-seventy) !important;\n}\n/* Cookies notification: hamburger menu */\nhtml.dark-mode ._42ft._4jy0._55pi._2agf._4o_4._9o-e._p._4jy3._517h._51sy {\n\tbackground: var(--container-color) !important;\n}\n/* Cookies notification: hamburger menu background */\nhtml.dark-mode ._54ng {\n\tbackground: var(--container-color) !important;\n}\n/* Cookies notification: hamburger menu text */\nhtml.dark-mode ._54nh {\n\tcolor: var(--base-seventy) !important;\n}\n/* Cookies notification: hamburger menu column borders */\nhtml.dark-mode ._54nc {\n\tborder-color: var(--container-color) !important;\n}\n/* Cookies notification: icons */\nhtml.dark-mode .img.sp_ng1YXMZLXub {\n\tfilter: invert(0.66);\n}\n/* Cookies notification: rectangular boxes */\nhtml.dark-mode .pam._9o-n.uiBoxGray {\n\tbackground-color: var(--base-ten) !important;\n}\nhtml.dark-mode ._9xq0 {\n\tcolor: var(--base-seventy) !important;\n}\n\n/* Top bar: App menu button color */\n/* Top bar: New message button color */\n.j83agx80.pfnyh3mw .ozuftl9m .a8c37x1j.ms05siws.hwsy1cff.b7h9ocf4 {\n\tfill: currentcolor;\n\tcolor: var(--primary-text);\n}\n\n/* Chat list: Mute icon */\n.bp9cbjyn.j83agx80.btwxx1t3 .dlv3wnog.lupvgy83 .a8c37x1j {\n\tfill: currentcolor;\n\tcolor: var(--primary-text);\n}\n\n/* Right sidebar: icons */\n.x1qhmfi1.x14yjl9h.xudhj91.x18nykt9.xww2gxu.x1fgtraw.x1264ykn.x78zum5.x6s0dn4.xl56j7k svg path {\n\tfill: currentcolor;\n\tcolor: var(--primary-text);\n}\n\n/* Contact list: delivered icon color */\n.aahdfvyu [role='grid'] .a8c37x1j.ms05siws.hwsy1cff.b7h9ocf4 {\n\tfill: currentcolor;\n\tcolor: var(--primary-text);\n}\n\n/* Messenger settings: Privacy & safety icon color */\n.x1lliihq.x1k90msu.x2h7rmj.x1qfuztq.x198g3q0.xxk0z11.xvy4d1p {\n\tfill: currentcolor;\n\tcolor: var(--primary-text);\n}\n\n/* Radio buttons */\n.x14yjl9h.xudhj91.x18nykt9.xww2gxu.x13fuv20.xu3j5b3.x1q0q8m5.x26u7qi.xamhcws.xol2nv.xlxy82.x19p7ews.x9f619.x1rg5ohu.x2lah0s.x1n2onr6.x1tz4bnf.xmds5ef.x25epmt.x11y6y4w.xxk0z11.xvy4d1p {\n\t--accent: var(--primary-text);\n}\n\n/* Create room icon color */\nhtml.dark-mode .x1p6odiv {\n\tcolor: var(--primary-icon);\n}\n"
  },
  {
    "path": "css/scrollbar.css",
    "content": "/* Custom native scrollbar */\n\n/* Light theme */\n::-webkit-scrollbar {\n\twidth: 16px;\n}\n::-webkit-scrollbar-thumb {\n\tbox-shadow: inset 0 0 16px 16px #bcc0c4;\n\tborder-radius: 8px;\n\tborder: solid 4px transparent;\n}\n::-webkit-scrollbar-track {\n\tbox-shadow: inset 0 0 16px 16px #fff;\n}\n::-webkit-scrollbar-track:hover {\n\tbox-shadow: inset 0 0 16px 16px #f0f1f2;\n}\n\n/* Dark theme */\nhtml.dark-mode ::-webkit-scrollbar {\n\twidth: 16px;\n}\nhtml.dark-mode ::-webkit-scrollbar-thumb {\n\tbox-shadow: inset 0 0 16px 16px #ffffff4c;\n\tborder-radius: 8px;\n\tborder: solid 4px transparent;\n}\nhtml.dark-mode ::-webkit-scrollbar-track {\n\tbox-shadow: inset 0 0 16px 16px #0000;\n}\nhtml.dark-mode ::-webkit-scrollbar-track:hover {\n\tbox-shadow: inset 0 0 16px 16px #ffffff10;\n}\n"
  },
  {
    "path": "css/vibrancy.css",
    "content": "/* -- BLOCK START: sidebar-only vibrancy -- */\n\nhtml.sidebar-vibrancy body,\nhtml.sidebar-vibrancy ._4sp8,\nhtml.sidebar-vibrancy.dark-mode body,\nhtml.sidebar-vibrancy.dark-mode ._4sp8 {\n\tbackground: transparent !important;\n}\n\n/* Login screen */\nhtml.sidebar-vibrancy ._3v_o {\n\tbackground-color: #fff;\n}\nhtml.sidebar-vibrancy.dark-mode ._3v_o {\n\tbackground-color: var(--container-dark-color);\n}\n\n/* Message placeholder text color */\nhtml.sidebar-vibrancy ._kmc ._1p1t {\n\tcolor: #999 !important;\n\t-webkit-text-fill-color: #999 !important;\n}\n\n/* Contact list: header above */\nhtml.sidebar-vibrancy.dark-mode ._36ic {\n\tbackground: transparent !important;\n}\n\n/* Contact list: search input */\nhtml.sidebar-vibrancy ._5iwm ._58al {\n\tbackground-color: rgb(246 247 249 / 50%) !important;\n}\nhtml.sidebar-vibrancy.dark-mode ._5iwm ._58al {\n\tbackground: var(--base-five) !important;\n}\n\n/* Chat title bar */\nhtml.sidebar-vibrancy ._673w {\n\tbackground-color: #fff !important;\n}\n\nhtml.sidebar-vibrancy.dark-mode ._673w {\n\tbackground-color: var(--container-dark-color) !important;\n\tborder-bottom: 1px solid var(--base-five) !important;\n}\n\n/* Share previews: title and subtitle */\nhtml.sidebar-vibrancy .__6k,\nhtml.sidebar-vibrancy .__6l {\n\tbackground-color: transparent !important;\n}\n\n/* Message container + right sidebar */\nhtml.sidebar-vibrancy ._4_j4,\nhtml.sidebar-vibrancy ._4_j5 {\n\tbackground: #fff !important;\n}\nhtml.sidebar-vibrancy.dark-mode ._4_j4,\nhtml.sidebar-vibrancy.dark-mode ._4_j5 {\n\tbackground: var(--container-dark-color) !important;\n}\n\n/* Message list: header above */\nhtml.sidebar-vibrancy.dark-mode ._5742 {\n\tbackground: transparent !important;\n}\n\n/* New conversation name input field */\nhtml.sidebar-vibrancy ._2y8y {\n\tbackground: #fff !important;\n}\nhtml.sidebar-vibrancy.dark-mode ._2y8y {\n\tbackground: var(--container-dark-color) !important;\n}\n\n/* Message text bar */\nhtml.sidebar-vibrancy.dark-mode ._5irm._7mkm {\n\tbackground: var(--container-dark-color);\n}\n\n/* -- BLOCK END: sidebar-only vibrancy -- */\n\n/* -- BLOCK START: full-window vibrancy -- */\n\nhtml.full-vibrancy body,\nhtml.full-vibrancy ._4sp8 {\n\tbackground: transparent !important;\n}\n\n\nhtml.full-vibrancy ._5hy2 ._43dh, /* Login button */\nhtml.full-vibrancy ._3-mr ._3-mv /* Verification \"Continue\" button */ {\n\tbackground-color: transparent !important;\n}\n\n/* Message placeholder text color */\nhtml.full-vibrancy ._kmc ._1p1t {\n\tcolor: #999 !important;\n\t-webkit-text-fill-color: #999 !important;\n}\n\n/* Messages list: start conversation with a chat bot */\nhtml.full-vibrancy ._2xh0 ._3zc8 {\n\tbackground-color: transparent;\n}\n\n/* Messages list: start conversation with a chat bot buttons */\nhtml.full-vibrancy ._2xh0 ._2xh4 {\n\tbackground-color: transparent;\n}\n\n/* Contact list: search input */\nhtml.full-vibrancy ._5iwm ._58al {\n\tbackground-color: rgb(246 247 249 / 50%) !important;\n}\nhtml.full-vibrancy.dark-mode ._5iwm ._58al {\n\tbackground: var(--base-five) !important;\n}\n\n/* Chat title bar */\nhtml.full-vibrancy ._673w,\nhtml.full-vibrancy.dark-mode ._673w {\n\tbackground-color: transparent !important;\n}\n\n/* Share previews: title and subtitle */\nhtml.full-vibrancy .__6k,\nhtml.full-vibrancy .__6l {\n\tbackground-color: transparent !important;\n}\n\n/* Contact list: person container */\nhtml.full-vibrancy ._1qt4 {\n\tborder-top: solid 1px rgb(0 0 0 / 6%);\n}\n\n/* Main content */\nhtml.full-vibrancy.dark-mode ._1q5- {\n\tbackground: transparent !important;\n}\n\n/* Message list: header above */\nhtml.full-vibrancy.dark-mode ._5742 {\n\tbackground: transparent !important;\n}\n\n/* Contact list: header above */\nhtml.full-vibrancy.dark-mode ._36ic {\n\tbackground: transparent !important;\n}\n\n/* Message container + right sidebar */\nhtml.full-vibrancy ._4_j4,\nhtml.full-vibrancy ._4_j5,\nhtml.full-vibrancy.dark-mode ._4_j4,\nhtml.full-vibrancy.dark-mode ._4_j5 {\n\tbackground: transparent !important;\n}\n\n/* New conversation name input field */\nhtml.full-vibrancy ._2y8y,\nhtml.full-vibrancy.dark-mode ._2y8y {\n\tbackground: transparent !important;\n}\n\n/* Message composer buttons */\nhtml.full-vibrancy ._39bj {\n\tfilter: brightness(0.8);\n}\nhtml.full-vibrancy.dark-model ._39bj {\n\tfilter: brightness(1);\n}\n\n/* Deleted message */\nhtml.full-vibrancy ._7301._hh7 {\n\tbackground-color: #fff;\n\tborder-color: transparent !important;\n}\nhtml.full-vibrancy.dark-mode ._7301._hh7 {\n\tbackground-color: var(--container-color);\n}\n\n/* Message list: link preview */\nhtml.full-vibrancy ._5i_d {\n\tbackground-color: #fff;\n\tborder-color: transparent !important;\n}\nhtml.full-vibrancy.dark-mode ._5i_d {\n\tbackground-color: var(--container-color);\n}\n\n/* Message composer: attached files */\nhtml.full-vibrancy ._2zl5 {\n\tbackground-color: #fff;\n\tborder-color: transparent;\n}\nhtml.full-vibrancy.dark-mode ._2zl5 {\n\tbackground-color: var(--container-color);\n}\n\n/* Reply tag icon */\nhtml.full-vibrancy ._6e38 {\n\tfilter: brightness(0.66);\n}\n\n/* Message composer: link preview */\nhtml.full-vibrancy .chatAttachmentShelf,\nhtml.full-vibrancy .fbNubFlyoutAttachments,\nhtml.full-vibrancy.dark-mode .chatAttachmentShelf,\nhtml.full-vibrancy.dark-mode .fbNubFlyoutAttachments {\n\tbackground: transparent !important;\n}\nhtml.full-vibrancy .chatAttachmentShelf,\nhtml.full-vibrancy.dark-mode .chatAttachmentShelf {\n\tborder-top-color: rgb(0 0 0 / 10%);\n}\n\n/* Message text bar */\nhtml.full-vibrancy ._5irm._7mkm {\n\tbackground: transparent;\n}\n\n/* Additional \"plus\" bar */\nhtml.full-vibrancy ._7mkk._7t1o._7t0j {\n\tdisplay: none;\n}\n/* -- BLOCK END: full-window vibrancy -- */\n"
  },
  {
    "path": "css/workchat.css",
    "content": "/* Main: Hide header */\n#pagelet_bluebar {\n\tdisplay: none;\n}\n\n/* Login: Remove vertical scrollbar */\nbody {\n\toverflow: hidden !important;\n}\n\n/* Login: Remove white top bar */\n._4b21 {\n\tdisplay: none;\n}\n\n/* Login: Remove Facebook links */\n#pageFooter {\n\tdisplay: none;\n}\n\n/* Login: Vertically center login box */\nhtml {\n\theight: 100%;\n}\nbody {\n\theight: 100%;\n}\n.UIPage_LoggedOut ._li {\n\theight: 100%;\n}\n#globalContainer {\n\tdisplay: flex;\n\tflex-direction: column;\n\tjustify-content: center;\n\theight: 100%;\n}\n._5rw2 {\n\tmin-height: 0 !important;\n\tpadding: 0 !important;\n}\n\n/**\n * Dark Mode\n */\n\n/* Login: Background */\nhtml.dark-mode ._5rw0 {\n\tcolor: var(--base-seventy);\n\tbackground-color: transparent !important;\n}\n\n/* Login: Logo */\nhtml.dark-mode .sx_2c5ee7 {\n\tfilter: brightness(4);\n}\n\n/* Login: Slogan */\nhtml.dark-mode ._5zi0 {\n\tfilter: brightness(3);\n}\n\n/* Login: Fix background color in vibrancy mode */\nhtml.vibrancy ._5rw0 {\n\tbackground-color: transparent !important;\n}\nhtml.vibrancy:not(.dark-mode) ._5rw2 {\n\tcolor: #242424 !important;\n}\n\n/* Login: Login button */\nhtml.dark-mode.vibrancy button {\n\tbackground: var(--container-color) !important;\n}\n\n/* Login: Remove login button border */\nhtml.dark-mode button {\n\tborder-color: var(--container-color) !important;\n}\n\n/* Login: Email confirmation screen */\nhtml.dark-mode ._5rwd {\n\tcolor: var(--base-seventy);\n\tbackground: var(--container-color) !important;\n}\n\n/* Contact List: Search results */\nhtml.dark-mode ._4p-s {\n\tbackground: var(--base-ten) !important;\n}\n\nhtml.dark-mode ._aou {\n\tbackground: var(--base) !important;\n}\n\nhtml.dark-mode ._4kf5 {\n\tbackground: var(--base) !important;\n}\n"
  },
  {
    "path": "license",
    "content": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)\n\nPermission 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:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE 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.\n"
  },
  {
    "path": "package.json",
    "content": "{\n\t\"name\": \"caprine\",\n\t\"productName\": \"Caprine\",\n\t\"version\": \"2.61.0\",\n\t\"description\": \"Elegant Facebook Messenger desktop app\",\n\t\"license\": \"MIT\",\n\t\"repository\": \"sindresorhus/caprine\",\n\t\"author\": {\n\t\t\"name\": \"Sindre Sorhus\",\n\t\t\"email\": \"sindresorhus@gmail.com\",\n\t\t\"url\": \"https://sindresorhus.com\"\n\t},\n\t\"main\": \"dist-js\",\n\t\"engines\": {\n\t\t\"node\": \">=16\"\n\t},\n\t\"scripts\": {\n\t\t\"postinstall\": \"patch-package && electron-builder install-app-deps\",\n\t\t\"lint:xo\": \"xo\",\n\t\t\"lint:stylelint\": \"stylelint \\\"css/**/*.css\\\"\",\n\t\t\"lint\": \"npm run lint:xo && npm run lint:stylelint\",\n\t\t\"test:tsc\": \"npm run build\",\n\t\t\"test\": \"npm run test:tsc && npm run lint\",\n\t\t\"start\": \"tsc && electron .\",\n\t\t\"build\": \"tsc\",\n\t\t\"dist:linux\": \"electron-builder --linux\",\n\t\t\"dist:mac\": \"electron-builder --mac\",\n\t\t\"dist:win\": \"electron-builder --win\",\n\t\t\"release\": \"np --no-publish\"\n\t},\n\t\"dependencies\": {\n\t\t\"@electron/remote\": \"^2.1.2\",\n\t\t\"@sindresorhus/do-not-disturb\": \"^1.1.0\",\n\t\t\"electron-better-ipc\": \"^2.0.1\",\n\t\t\"electron-context-menu\": \"^3.6.1\",\n\t\t\"electron-debug\": \"^3.2.0\",\n\t\t\"electron-dl\": \"^3.5.2\",\n\t\t\"electron-localshortcut\": \"^3.2.1\",\n\t\t\"electron-store\": \"^8.1.0\",\n\t\t\"electron-updater\": \"^6.1.8\",\n\t\t\"electron-util\": \"^0.17.2\",\n\t\t\"element-ready\": \"^5.0.0\",\n\t\t\"facebook-locales\": \"^1.0.916\",\n\t\t\"is-online\": \"^9.0.1\",\n\t\t\"json-schema-typed\": \"^8.0.1\",\n\t\t\"lodash\": \"^4.17.21\",\n\t\t\"npm-check-updates\": \"^16.14.15\",\n\t\t\"p-wait-for\": \"^3.2.0\"\n\t},\n\t\"devDependencies\": {\n\t\t\"@sindresorhus/tsconfig\": \"^0.7.0\",\n\t\t\"@types/electron-localshortcut\": \"^3.1.3\",\n\t\t\"@types/facebook-locales\": \"^1.0.2\",\n\t\t\"@types/lodash\": \"^4.14.202\",\n\t\t\"del-cli\": \"^5.1.0\",\n\t\t\"electron\": \"^29.0.1\",\n\t\t\"electron-builder\": \"^24.12.0\",\n\t\t\"husky\": \"^9.0.11\",\n\t\t\"np\": \"^9.2.0\",\n\t\t\"patch-package\": \"^8.0.0\",\n\t\t\"stylelint\": \"^14.10.0\",\n\t\t\"stylelint-config-xo\": \"^0.22.0\",\n\t\t\"typescript\": \"^5.3.3\",\n\t\t\"xo\": \"^0.57.0\"\n\t},\n\t\"xo\": {\n\t\t\"envs\": [\n\t\t\t\"node\",\n\t\t\t\"browser\"\n\t\t],\n\t\t\"rules\": {\n\t\t\t\"@typescript-eslint/ban-ts-comment\": \"off\",\n\t\t\t\"@typescript-eslint/consistent-type-imports\": \"off\",\n\t\t\t\"@typescript-eslint/naming-convention\": \"off\",\n\t\t\t\"@typescript-eslint/no-floating-promises\": \"off\",\n\t\t\t\"@typescript-eslint/no-loop-func\": \"off\",\n\t\t\t\"@typescript-eslint/no-non-null-assertion\": \"off\",\n\t\t\t\"@typescript-eslint/no-require-imports\": \"off\",\n\t\t\t\"@typescript-eslint/no-unsafe-argument\": \"off\",\n\t\t\t\"@typescript-eslint/no-unsafe-assignment\": \"off\",\n\t\t\t\"@typescript-eslint/no-unsafe-call\": \"off\",\n\t\t\t\"@typescript-eslint/no-unsafe-enum-comparison\": \"off\",\n\t\t\t\"@typescript-eslint/no-var-requires\": \"off\",\n\t\t\t\"import/extensions\": \"off\",\n\t\t\t\"import/no-anonymous-default-export\": \"off\",\n\t\t\t\"import/no-cycle\": \"off\",\n\t\t\t\"n/file-extension-in-import\": \"off\",\n\t\t\t\"unicorn/prefer-at\": \"off\",\n\t\t\t\"unicorn/prefer-module\": \"off\",\n\t\t\t\"unicorn/prefer-top-level-await\": \"off\"\n\t\t}\n\t},\n\t\"stylelint\": {\n\t\t\"extends\": \"stylelint-config-xo\",\n\t\t\"rules\": {\n\t\t\t\"declaration-no-important\": null,\n\t\t\t\"no-descending-specificity\": null,\n\t\t\t\"no-duplicate-selectors\": null,\n\t\t\t\"rule-empty-line-before\": null,\n\t\t\t\"selector-class-pattern\": null,\n\t\t\t\"selector-id-pattern\": null,\n\t\t\t\"selector-max-class\": null\n\t\t}\n\t},\n\t\"np\": {\n\t\t\"publish\": false,\n\t\t\"releaseDraft\": false\n\t},\n\t\"build\": {\n\t\t\"files\": [\n\t\t\t\"**/*\",\n\t\t\t\"!media${/*}\"\n\t\t],\n\t\t\"asarUnpack\": [\n\t\t\t\"static/Icon.png\"\n\t\t],\n\t\t\"appId\": \"com.sindresorhus.caprine\",\n\t\t\"mac\": {\n\t\t\t\"category\": \"public.app-category.social-networking\",\n\t\t\t\"icon\": \"build/icon.icns\",\n\t\t\t\"electronUpdaterCompatibility\": \">=4.5.2\",\n\t\t\t\"darkModeSupport\": true,\n\t\t\t\"target\": {\n\t\t\t\t\"target\": \"default\",\n\t\t\t\t\"arch\": [\n\t\t\t\t\t\"x64\",\n\t\t\t\t\t\"arm64\"\n\t\t\t\t]\n\t\t\t},\n\t\t\t\"extendInfo\": {\n\t\t\t\t\"LSUIElement\": 1,\n\t\t\t\t\"NSCameraUsageDescription\": \"Caprine needs access to your camera.\",\n\t\t\t\t\"NSMicrophoneUsageDescription\": \"Caprine needs access to your microphone.\"\n\t\t\t}\n\t\t},\n\t\t\"dmg\": {\n\t\t\t\"iconSize\": 160,\n\t\t\t\"contents\": [\n\t\t\t\t{\n\t\t\t\t\t\"x\": 180,\n\t\t\t\t\t\"y\": 170\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"x\": 480,\n\t\t\t\t\t\"y\": 170,\n\t\t\t\t\t\"type\": \"link\",\n\t\t\t\t\t\"path\": \"/Applications\"\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"linux\": {\n\t\t\t\"target\": [\n\t\t\t\t\"AppImage\",\n\t\t\t\t\"deb\"\n\t\t\t],\n\t\t\t\"icon\": \"build/icons/\",\n\t\t\t\"synopsis\": \"Elegant Facebook Messenger desktop app\",\n\t\t\t\"description\": \"Caprine is an unofficial and privacy focused Facebook Messenger app with many useful features.\",\n\t\t\t\"category\": \"Network;Chat\"\n\t\t},\n\t\t\"snap\": {\n\t\t\t\"plugs\": [\n\t\t\t\t\"default\",\n\t\t\t\t\"camera\",\n\t\t\t\t\"removable-media\"\n\t\t\t],\n\t\t\t\"publish\": [\n\t\t\t\t{\n\t\t\t\t\t\"provider\": \"github\"\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"provider\": \"snapStore\",\n\t\t\t\t\t\"channels\": [\n\t\t\t\t\t\t\"stable\"\n\t\t\t\t\t]\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"win\": {\n\t\t\t\"verifyUpdateCodeSignature\": false,\n\t\t\t\"icon\": \"build/icon.png\"\n\t\t},\n\t\t\"nsis\": {\n\t\t\t\"oneClick\": false,\n\t\t\t\"perMachine\": false,\n\t\t\t\"allowToChangeInstallationDirectory\": true\n\t\t}\n\t},\n\t\"husky\": {\n\t\t\"hooks\": {\n\t\t\t\"pre-push\": \"npm test\"\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "packages/deb/addRepo.sh",
    "content": "#!/bin/bash\n# Made by Lefteris Garyfalakis\n# Last update: 25-08-2022\n\nCOL_NC='\\e[0m' # No Color\nCOL_LIGHT_RED='\\e[1;31m'\nCOL_LIGHT_GREEN='\\e[1;32m'\nCOL_LIGHT_BLUE='\\e[1;94m'\nCOL_LIGHT_YELLOW='\\e[1;93m'\nTICK=\"${COL_NC}[${COL_LIGHT_GREEN}✓${COL_NC}]\"\nCROSS=\"${COL_NC}[${COL_LIGHT_RED}✗${COL_NC}]\"\nINFO=\"${COL_NC}[${COL_LIGHT_YELLOW}i${COL_NC}]\"\nQUESTION=\"${COL_NC}[${COL_LIGHT_BLUE}?${COL_NC}]\"\n\nAPT_SOURCE_PATH=\"/etc/apt/sources.list.d/caprine.list\"\nAPT_SOURCE_CONTENT=\"deb [trusted=yes] https://apt.fury.io/lefterisgar/ * *\"\n\nclear\n\n# Print ASCII logo and branding\nprintf \"       ⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣤⣶⣶⣶⣶⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀\n         ⠀⠀⢀⣴⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⡀⠀⠀⠀⠀\n       ⠀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀\n       ⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀\n       ⠀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⢿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⠀\n       ⢰⣿⣿⣿⣿⣿⣿⣿⣿⠟⠁⠀⠀⠙⠿⠿⠛⠉⣠⣾⣿⣿⣿⣿⣿⡆\n       ⢸⣿⣿⣿⣿⣿⣿⠟⠁⢀⣠⣄⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⡇\n       ⠈⣿⣿⣿⣿⣟⣥⣶⣾⣿⣿⣿⣷⣦⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁\n       ⠀⠹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠏⠀\n       ⠀⠀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠋⠀⠀\n        ⠀⠀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⠁⠀⠀⠀\n        ⠀⠀⠀⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠀⠀⠀⠀⠀⠀\n        ⠀⠀⠀⢸⡿⠛⠋\\n\\n\"\n\nprintf \"      ___               _\n     / __|__ _ _ __ _ _(_)_ _  ___\n    | (__/ _\\` | '_ \\ '_| | ' \\/ -_)\n     \\___\\__,_| .__/_| |_|_||_\\___|\n              |_|\\n\\n\"\n\nprintf \" Elegant Facebook Messenger desktop app\\n\\n\"\n\nprintf \"*** Caprine installation script ***\\n\"\nprintf -- \"-----------------------------------\\n\"\nprintf \"Author      : Lefteris Garyfalakis\\n\"\nprintf \"Last update : 25-08-2022\\n\"\nprintf -- \"-----------------------------------\\n\"\n\nprintf \"%b %bDetecting APT...%b\\\\n\" \"${INFO}\"\n\n# Check if APT is installed\nif hash apt 2>/dev/null; then\n\tprintf \"%b %b$(apt -v)%b\\\\n\" \"${TICK}\" \"${COL_LIGHT_GREEN}\" \"${COL_NC}\"\nelse\n\tprintf \"%b %bAPT was not detected!%b\\\\n\" \"${CROSS}\" \"${COL_LIGHT_RED}\" \"${COL_NC}\"\n\tprintf \"%b %bIs your distribution Debian-based? %b\" \"${QUESTION}\"\n    exit 2\nfi\n\n# Add Caprine's APT repository\nprintf \"%b %bAdding APT repository...%b\" \"${INFO}\"\n\n# Disable globbing\nset -f\n\necho $APT_SOURCE_CONTENT | sudo tee $APT_SOURCE_PATH > /dev/null\n\n# Enable globbing\nset +f\n\nif [[ $(< $APT_SOURCE_PATH) == \"$APT_SOURCE_CONTENT\" ]]; then\n\tprintf \" Done!\\n\"\nelse\n\tprintf \"\\n%b %bError adding APT repository!%b\\\\n\" \"${CROSS}\" \"${COL_LIGHT_RED}\" \"${COL_NC}\"\n\tprintf \"%b %bReason: Content mismatch%b\\\\n\" \"${CROSS}\" \"${COL_LIGHT_RED}\" \"${COL_NC}\"\n\texit 5\nfi\n\n# Update the repositories\nprintf \"%b %bUpdating repositories...%b\" \"${INFO}\"\n\nsudo apt update > /dev/null 2>&1\n\nprintf \" Done!\\n\"\n\n# Ask the user if he wants to install Caprine\nprintf \"%b %bDo you want to install Caprine? (y/n) %b\" \"${QUESTION}\"\nread -n 1 -r < /dev/tty\nprintf \"\\n\"\n\nif [[ $REPLY =~ ^[Yy]$ ]]; then\n\tprintf \"%b %bInstalling Caprine...%b\" \"${INFO}\"\n\n\tsudo apt install -y caprine > /dev/null 2>&1\n\n\tprintf \" Done!\\n\"\nelse\n\tprintf \"%b %bOperation cancelled by the user. Aborting.%b\\\\n\" \"${CROSS}\" \"${COL_LIGHT_RED}\" \"${COL_NC}\"\n\texit 125;\nfi\n"
  },
  {
    "path": "packages/rpm/caprine.desktop",
    "content": "[Desktop Entry]\nType=Application\nName=Caprine\nGenericName=IM Client\nComment=Elegant Facebook Messenger desktop app\nIcon=caprine\nExec=caprine\nKeywords=Messenger;Facebook;Chat;\nCategories=GTK;InstantMessaging;Network;\nStartupNotify=true\n"
  },
  {
    "path": "packages/rpm/caprine.spec",
    "content": "%define debug_package %{nil}\n%global _build_id_links alldebug\n\nName:           caprine\nVersion:        2.61.0\nRelease:        1%{?dist}\nSummary:        Elegant Facebook Messenger desktop app\n\nLicense:        MIT\nURL:            https://github.com/sindresorhus/caprine/\nSource0:        https://github.com/sindresorhus/caprine/archive/refs/tags/v%{version}.tar.gz\nSource1:        %{name}.desktop\n\nExclusiveArch:  x86_64\nBuildRequires:  npm\nBuildRequires:  nodejs >= 20.0.0\n\n%description\nCaprine is an unofficial and privacy-focused Facebook Messenger app with many useful features.\n\n%prep\n%autosetup\n\n%build\nnpm install --silent --no-progress\nnode_modules/.bin/tsc\nnode_modules/.bin/electron-builder --linux dir\n\n%install\ninstall -d %{buildroot}%{_libdir}/%{name}\ncp -r dist/linux-unpacked/* %{buildroot}%{_libdir}/%{name}\n\ninstall -d %{buildroot}%{_bindir}\nln -sf %{_libdir}/%{name}/%{name} %{buildroot}%{_bindir}/%{name}\n\ninstall -Dm644 build/icon.png %{buildroot}%{_datadir}/pixmaps/%{name}.png\n\ninstall -d %{buildroot}%{_datadir}/applications\ninstall -Dm644 %{SOURCE1} %{buildroot}%{_datadir}/applications/%{name}.desktop\n\ninstall -d %{buildroot}%{_datadir}/licenses/%{name}\ninstall -Dm644 license %{buildroot}%{_datadir}/licenses/%{name}\n\n%post\n/usr/bin/update-desktop-database\n/usr/bin/gtk-update-icon-cache\n\n%postun\n/usr/bin/update-desktop-database\n/usr/bin/gtk-update-icon-cache\n\n%files\n%license %{_datadir}/licenses/%{name}/license\n%{_libdir}/%{name}/\n%{_bindir}/%{name}\n%{_datadir}/applications/caprine.desktop\n%{_datadir}/pixmaps/%{name}.png\n\n%changelog\n* Wed Apr  3 2024 dusansimic <dusan.simic1810@gmail.com> - 2.60.1-1\n- Code refactoring\n* Tue Feb 20 2024 dusansimic <dusan.simic1810@gmail.com> - 2.59.3-1\n- Fix blank window\n- Fix try icon\n* Mon Feb 19 2024 dusansimic <dusan.simic1810@gmail.com> - 2.59.2-1\n- Hidden dialog issue Fix\n- Update Messenger for Mac/Windows selectors\n* Wed Oct 11 2023 dusansimic <dusan.simic1810@gmail.com> - 2.59.1-1\n- Release 2.59.1\n* Wed Sep 27 2023 dusansimic <dusan.simic1810@gmail.com> - 2.59.0-1\n- Release 2.59.0\n* Mon Sep 25 2023 dusansimic <dusan.simic1810@gmail.com> - 2.58.3-1\n- Release 2.58.3\n* Mon Sep 25 2023 dusansimic <dusan.simic1810@gmail.com> - 2.58.2-1\n- Release 2.58.2\n* Tue Sep  5 2023 dusansimic <dusan.simic1810@gmail.com> - 2.58.1-1\n- Release 2.58.1\n* Wed Jul 26 2023 dusansimic <dusan.simic1810@gmail.com> - 2.58.0-1\n- Release 2.58.0\n* Sat May  6 2023 dusansimic <dusan.simic1810@gmail.com> - 2.57.4-1\n- Release 2.57.4\n* Sun Apr 30 2023 dusansimic <dusan.simic1810@gmail.com> - 2.57.3-1\n- Release 2.57.3\n* Sat Apr 29 2023 dusansimic <dusan.simic1810@gmail.com> - 2.57.2-1\n- Release 2.57.2\n* Mon Apr 17 2023 dusansimic <dusan.simic1810@gmail.com> - 2.57.1-1\n- Release 2.57.1\n* Wed Nov 16 2022 dusansimic <dusan.simic1810@gmail.com> - 2.57.0-1\n- Release 2.57.0\n* Mon Aug 22 2022 dusansimic <dusan.simic1810@gmail.com> - 2.56.1-1\n- Release 2.56.1\n* Thu Aug 18 2022 dusansimic <dusan.simic1810@gmail.com> - 2.56.0-1\n- Release 2.56.0\n* Thu Jun 16 2022 dusansimic <dusan.simic1810@gmail.com> - 2.55.7-1\n- Release 2.55.7\n* Mon Jun 13 2022 dusansimic <dusan.simic1810@gmail.com> - 2.55.6-1\n- Release 2.55.6\n* Mon May 16 2022 dusansimic <dusan.simic1810@gmail.com> - 2.55.5-1\n- Release 2.55.5\n* Sun Mar 20 2022 dusansimic <dusan.simic1810@gmail.com> - 2.55.3-1\n- Release 2.55.3\n* Thu Dec  9 2021 dusansimic <dusan.simic1810@gmail.com> - 2.55.2-1\n- Release 2.55.2\n* Thu Dec  2 2021 dusansimic <dusan.simic1810@gmail.com> - 2.55.1-1\n- Release 2.55.1\n* Thu Oct 28 2021 dusansimic <dusan.simic1810@gmail.com> - 2.55.0-1\n- Release 2.55.0\n* Fri Aug 13 2021 dusansimic <dusan.simic1810@gmail.com> - 2.54.1-1\n- Release 2.54.1\n* Thu Jul 29 2021 dusansimic <dusan.simic1810@gmail.com> - 2.54.0-1\n- Release 2.54.0\n* Sat May  8 2021 dusansimic <dusan.simic1810@gmail.com> - 2.53.0-1\n- Release 2.53.0\n* Mon Apr 26 2021 dusansimic <dusan.simic1810@gmail.com> - 2.52.4-1\n- Release 2.52.4\n- Removed dependency desktop-file-utils and gtk-update-icon-cache\n* Fri Apr  9 2021 dusansimic <dusan.simic1810@gmail.com> - 2.52.3-1\n- Release 2.52.3\n- Some minor updates to spec file and adding license file to installation\n* Thu Mar 25 2021 dusansimic <dusan.simic1810@gmail.com> - 2.52.2-1\n- Release 2.52.2\n"
  },
  {
    "path": "patches/electron-debug++electron-is-dev+1.2.0.patch",
    "content": "diff --git a/node_modules/electron-debug/node_modules/electron-is-dev/index.js b/node_modules/electron-debug/node_modules/electron-is-dev/index.js\nindex 3b3fbc5..042a8a5 100644\n--- a/node_modules/electron-debug/node_modules/electron-is-dev/index.js\n+++ b/node_modules/electron-debug/node_modules/electron-is-dev/index.js\n@@ -5,7 +5,7 @@ if (typeof electron === 'string') {\n \tthrow new TypeError('Not running in an Electron environment!');\n }\n \n-const app = electron.app || electron.remote.app;\n+const app = electron.app || require('@electron/remote');\n \n const isEnvSet = 'ELECTRON_IS_DEV' in process.env;\n const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;\n"
  },
  {
    "path": "patches/electron-util++electron-is-dev+1.2.0.patch",
    "content": "diff --git a/node_modules/electron-util/node_modules/electron-is-dev/index.js b/node_modules/electron-util/node_modules/electron-is-dev/index.js\nindex 3b3fbc5..042a8a5 100644\n--- a/node_modules/electron-util/node_modules/electron-is-dev/index.js\n+++ b/node_modules/electron-util/node_modules/electron-is-dev/index.js\n@@ -5,7 +5,7 @@ if (typeof electron === 'string') {\n \tthrow new TypeError('Not running in an Electron environment!');\n }\n \n-const app = electron.app || electron.remote.app;\n+const app = electron.app || require('@electron/remote');\n \n const isEnvSet = 'ELECTRON_IS_DEV' in process.env;\n const getFromEnv = parseInt(process.env.ELECTRON_IS_DEV, 10) === 1;\n"
  },
  {
    "path": "readme.md",
    "content": "<div align=\"center\">\n\t<br>\n\t<br>\n\t<a href=\"https://github.com/sindresorhus/caprine\">\n\t\t<img src=\"media/AppIcon-readme.png\" width=\"200\" height=\"200\">\n\t</a>\n\t<h1>Caprine</h1>\n\t<p>\n\t\t<b>Elegant Facebook Messenger desktop app</b>\n\t</p>\n\t<br>\n\t<br>\n\t<p>\n\t\tCaprine is an unofficial and privacy-focused Facebook Messenger app with many useful features.\n\t</p>\n\t<b>\n\t\tCaprine is feature complete. However, we welcome contributions for improvements and bug fixes.\n\t</b>\n\t<br>\n\t\t<a href=\"https://github.com/sindresorhus/caprine\">\n\t\tWebsite\n\t\t</a>\n\t<br>\n\t<a href=\"https://github.com/sindresorhus/caprine/releases/latest\">\n\t\t<img src=\"media/screenshot.png\" width=\"846\">\n\t</a>\n</div>\n\n## Highlights\n\n- [Dark theme](#dark-mode)\n- [Vibrant theme](#vibrancy-macos-only)\\*\n- [Privacy-focused](#privacy)\n- [Keyboard shortcuts](#keyboard-shortcuts)\n- [Menu bar mode](#menu-bar-mode-macos-only-)\\*\n- [Work Chat support](#work-chat-support)\n- [Code blocks](#code-blocks)\n- [Touch Bar support](#touch-bar-support-macos-only)\\*\n- [Custom styles](#custom-styles)\n- Cross-platform\n- Silent auto-updates\n- Custom text size\n- Emoji style setting\n- Respects Do Not Disturb\\*\n\n\\*macOS only\n\n## Install\n\n*macOS 10.12+ (Intel and Apple Silicon), Linux (x64 and arm64), and Windows 10+ (64-bit) are supported.*\n\nDownload the latest version on the [website](https://github.com/sindresorhus/caprine) or below.\n\n### macOS\n\n[**Download**](https://github.com/sindresorhus/caprine/releases/latest) the `.dmg` file.\n\nOr with [Homebrew](https://brew.sh): `$ brew install caprine`\n\n### Linux\n\n<table>\n\t<th>Distribution</th>\n\t<th>Repository</th>\n\t<th>Automatic Updates</th>\n\t<th>Maintainer</th>\n\t<th>How to install</th>\n\t<tr>\n\t\t<td>Arch Linux</td>\n\t\t<td>Community</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Frederik Schwan</td>\n\t\t<td><code>pacman -S caprine</code></td>\n\t</tr>\n\t<tr>\n\t\t<td>Debian / Ubuntu (manually)</td>\n\t\t<td>GitHub</td>\n\t\t<td align=\"center\">❌</td>\n\t\t<td>Official</td>\n\t\t<td>\n\t\t\t<a href=\"https://github.com/sindresorhus/caprine/releases/latest\">Download</a> the .deb file\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>Debian / Ubuntu (deb-get)</td>\n\t\t<td>GitHub</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Official</td>\n\t\t<td>\n\t\t\tFollow the <a href=#installation-using-deb-get>instructions below</a>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>Debian / Ubuntu (APT)</td>\n\t\t<td>Gemfury</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Lefteris Garyfalakis</td>\n\t\t<td>\n\t\t\tFollow the <a href=#apt-repository-gemfury>instructions below</a>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>RHEL / Fedora / openSUSE</td>\n\t\t<td>Copr</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Dušan Simić</td>\n\t\t<td>\n\t\t\tFollow the <a href=#copr>instructions below</a>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>AppImage</td>\n\t\t<td>GitHub</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Official</td>\n\t\t<td>\n\t\t\tFollow the <a href=#appimage>instructions below</a>\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>Flatpak</td>\n\t\t<td>Flathub</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Dušan Simić</td>\n\t\t<td>\n\t\t\tVisit <a href=\"https://flathub.org/apps/details/com.sindresorhus.Caprine\">Flathub</a> and follow the instructions\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>Snap</td>\n\t\t<td>Snapcraft</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Official</td>\n\t\t<td>\n\t\t\tVisit <a href=\"https://snapcraft.io/caprine\">Snapcraft</a> and follow the instructions\n\t\t</td>\n\t</tr>\n</table>\n\n#### Installation using deb-get:\n\n* Download and install [deb-get](https://github.com/wimpysworld/deb-get).\n* Run `deb-get install caprine`.\n\nNote: deb-get is 3rd party software, not to be associated with apt-get.\n\n#### APT repository (Gemfury):\n\nRun the following command to add it:\n\n```sh\nwget -q -O- https://raw.githubusercontent.com/sindresorhus/caprine/main/packages/deb/addRepo.sh | sudo bash\n```\n\nAlternatively (for advanced users):\n```sh\n# Add the repository\necho \"deb [trusted=yes] https://apt.fury.io/lefterisgar/ * *\" > \\\n/etc/apt/sources.list.d/caprine.list\n\n# Update the package indexes\nsudo apt update\n\n# Install Caprine\nsudo apt install caprine\n```\n\n\n#### Copr:\n\nFor Fedora / RHEL:\n\n```sh\nsudo dnf copr enable dusansimic/caprine\nsudo dnf install caprine\n```\n\nFor openSUSE:\n- Create a new file in `/etc/zypp/repos.d/caprine.repo`.\n- Copy the contents of [this file](https://copr.fedorainfracloud.org/coprs/dusansimic/caprine/repo/opensuse-tumbleweed/dusansimic-caprine-opensuse-tumbleweed.repo) and paste them into the file you just created.\n\nAlternatively use the following one-liner:\n```sh\ncurl -s https://copr.fedorainfracloud.org/coprs/dusansimic/caprine/repo/opensuse-tumbleweed/dusansimic-caprine-opensuse-tumbleweed.repo | sudo tee /etc/zypp/repos.d/caprine.repo\n```\n\n#### AppImage:\n\n[Download](https://github.com/sindresorhus/caprine/releases/latest) the `.AppImage` file.\n\nMake it [executable](https://discourse.appimage.org/t/how-to-run-an-appimage/80):\n\n```sh\nchmod +x Caprine-2.xx.x.AppImage\n```\n\nThen run it!\n\n#### About immutable Linux distributions:\n[Fedora Silverblue](https://silverblue.fedoraproject.org), [Fedora Kinoite](https://kinoite.fedoraproject.org), [EndlessOS](https://endlessos.com), [CarbonOS](https://carbon.sh) and other immutable distributions only support Flatpak and/or AppImage.*\n\n*Note: On some distributions Flatpak must be [pre-configured manually](https://flatpak.org/setup).*\n\n### Windows\n\n<table>\n\t<th>Method</th>\n\t<th>Repository</th>\n\t<th>Automatic Updates</th>\n\t<th>Maintainer</th>\n\t<th>How to install</th>\n\t<tr>\n\t\t<td>Manually</td>\n\t\t<td>GitHub</td>\n\t\t<td align=\"center\">❌</td>\n\t\t<td>Official</td>\n\t\t<td>\n\t\t\t<a href=\"https://github.com/sindresorhus/caprine/releases/latest\">Download</a> the .exe file\n\t\t</td>\n\t</tr>\n\t<tr>\n\t\t<td>Chocolatey</td>\n\t\t<td>Community</td>\n\t\t<td align=\"center\">✔️</td>\n\t\t<td>Michael Quevillon</td>\n\t\t<td><code>choco install caprine</code></td>\n\t</tr>\n</table>\n\n*For taskbar notification badges to work on Windows 10, you'll need to [enable them in Taskbar Settings](https://www.tenforums.com/tutorials/48186-taskbar-buttons-hide-show-badges-windows-10-a.html).*\n\n## Features\n\n### Dark mode\n\nYou can toggle dark mode in the `View` menu or with <kbd>Command</kbd> <kbd>d</kbd> / <kbd>Control</kbd> <kbd>d</kbd>.\n\n<img src=\"media/screenshot-dark.png\" width=\"846\">\n\n### Hide Names and Avatars\n\nYou can prevent others from looking at who you're chatting with by enabling the “Hide Names and Avatars” feature in the “View” menu or with <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>n</kbd>.\n\n### Vibrancy *(macOS only)*\n\nOn *macOS*, you can toggle the window vibrancy effect in the `View` menu.\n\n<img src=\"media/screenshot-vibrancy.jpg\" width=\"1165\">\n\n### Privacy\n\n<img src=\"media/screenshot-block-typing-indicator.png\" width=\"626\">\n\nYou can choose to prevent people from knowing when you have seen a message and when you are currently typing. These settings are available under the `Caprine`/`File` menu.\n\n### Mute desktop notifications *(macOS only)*\n\nYou can quickly disable receiving notifications from the `Caprine`/`File` menu or the Dock on macOS.\n\n### Hide notification message preview\n\n<div align=\"center\"><img src=\"media/screenshot-hide-notification-message-location.png\" width=\"300\"></div>\n\n<div align=\"center\"><img src=\"media/screenshot-hide-notification-message-before.png\" width=\"400\"></div>\n\n<div align=\"center\"><img src=\"media/screenshot-hide-notification-message-after.png\" width=\"400\"></div>\n\nYou can toggle the `Show Message Preview in Notification` setting in the `Caprine`/`File` menu.\n\n### Prevents link tracking\n\nLinks that you click on will not be tracked by Facebook.\n\n### Jump to conversation hotkey\n\nYou can switch conversations similar to how you switch browser tabs: <kbd>Command/Control</kbd> <kbd>n</kbd> (where `n` is `1` through `9`).\n\n### Compact mode\n\nThe interface adapts when resized to a small size.\n\n<div align=\"center\"><img src=\"media/screenshot-compact.png\" width=\"512\"></div>\n\n### Desktop notifications\n\nDesktop notifications can be turned on in `Preferences`.\n\n<div align=\"center\"><img src=\"media/screenshot-notification.png\" width=\"358\"></div>\n\n### Always on Top\n\nYou can toggle whether Caprine stays on top of other windows in the `Window`/`View` menu or with <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>t</kbd>.\n\n### Work Chat support\n\nSupport for Work Chat: Messenger for [Workplace](https://www.facebook.com/workplace). You can switch to it in the `Caprine`/`File` menu.\n\n<div align=\"center\"><img src=\"media/screenshot-work-chat.png\" width=\"788\"></div>\n\n### Code blocks\n\nYou can send code blocks by using [Markdown syntax](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#code).\n\n<div align=\"center\"><img src=\"media/screenshot-codeblocks-dark.png\" width=\"784\"></div>\n<div align=\"center\"><img src=\"media/screenshot-codeblocks-light.png\" width=\"784\"></div>\n\n### Background behavior\n\nWhen closing the window, the app will by default continue running in the background, in the dock on macOS and the tray on Linux/Windows. Right-click the dock/tray icon and choose `Quit` to completely quit the app. On macOS, click the dock icon to show the window. On Linux, right-click the tray icon and choose `Toggle` to toggle the window. On Windows, click the tray icon to toggle the window.\n\nNote that you can change the behavior of Caprine so that the app closes when the window is closed. For this, you'll need to go to the settings and click on `Quit on Window Close`.\n\n### Quick access to conversations from the Dock menu *(macOS only)*\n\n<img src=\"media/screenshot-dock-menu.png\" width=\"319\" height=\"404\">\n\n### Touch Bar support *(macOS only)*\n\n<img src=\"media/screenshot-touchbar.png\" width=\"1085\">\n\n### Custom languages for spell-check *(Not for macOS)*\n\nUsers can select supported languages from `Conversation` → `Spell Checker Language`.\n\nmacOS detects the language automatically.\n\n### Custom styles\n\nAdvanced users can modify the colors/styles of Caprine. Click the menu item `Caprine`/`File` → `Caprine Settings` → `Advanced` → `Custom Styles` and a CSS file will open up in your default editor.\n\n### Menu Bar Mode *(macOS only)* <img src=\"media/screenshot-menu-bar-mode.png\" width=\"20\">\n\n<img src=\"media/screenshot-menu-bar-menu.png\" width=\"140\" align=\"right\">\n\nYou can enable `Show Menu Bar Icon` in the `Caprine Preferences` menu to have a Caprine icon in the menu bar. The icon will indicate when you have unread notifications and you can click it to toggle the Caprine window. You can also toggle the Caprine window with the global shortcut <kbd>Command</kbd> <kbd>Shift</kbd> <kbd>y</kbd>.\n\nYou can also remove Caprine from the Dock and task switcher by clicking `Hide Dock Icon` menu item from the menu bar icon. There will then no longer be any menus for the window, but you can access those from the `Menu` item in the menu bar icon menu.\n\n### Keyboard shortcuts\n\nDescription            | Keys\n-----------------------| -----------------------\nNew conversation       | <kbd>Command/Control</kbd> <kbd>n</kbd>\nSearch conversations   | <kbd>Command/Control</kbd> <kbd>k</kbd>\nToggle \"Dark mode\"     | <kbd>Command/Control</kbd> <kbd>d</kbd>\nHide Names and Avatars | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>n</kbd>\nNext conversation      | <kbd>Command/Control</kbd> <kbd>]</kbd> or <kbd>Control</kbd> <kbd>Tab</kbd>\nPrevious conversation  | <kbd>Command/Control</kbd> <kbd>[</kbd> or <kbd>Control</kbd> <kbd>Shift</kbd> <kbd>Tab</kbd>\nJump to conversation   | <kbd>Command/Control</kbd> <kbd>1</kbd>…<kbd>9</kbd>\nInsert GIF             | <kbd>Command/Control</kbd> <kbd>g</kbd>\nInsert sticker         | <kbd>Command/Control</kbd> <kbd>s</kbd>\nInsert emoji           | <kbd>Command/Control</kbd> <kbd>e</kbd>\nAttach files           | <kbd>Command/Control</kbd> <kbd>t</kbd>\nFocus text input       | <kbd>Command/Control</kbd> <kbd>i</kbd>\nSearch in conversation | <kbd>Command/Control</kbd> <kbd>f</kbd>\nMute conversation      | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>m</kbd>\nHide conversation      | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>h</kbd>\nDelete conversation    | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>d</kbd>\nToggle \"Always on Top\" | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>t</kbd>\nToggle window menu     | <kbd>Alt</kbd> *(Windows/Linux only)*\nToggle main window     | <kbd>Command</kbd> <kbd>Shift</kbd> <kbd>y</kbd> *(macOS only)*\nToggle sidebar         | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>s</kbd>\nSwitch to Messenger    | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>1</kbd>\nSwitch to Workchat     | <kbd>Command/Control</kbd> <kbd>Shift</kbd> <kbd>2</kbd>\nPreferences            | <kbd>Command/Control</kbd> <kbd>,</kbd>\n\n###### Tip\n\nOn macOS, you can [change these in the System Preferences](https://www.intego.com/mac-security-blog/how-to-make-custom-keyboard-shortcuts-for-any-macos-menu-items-and-to-launch-your-favorite-apps/) and you can even add your own keyboard shortcuts for menu items without a predefined keyboard shortcut.\n\n## FAQ\n\n#### Can I contribute localizations?\n\nThe main app interface is already localized by Facebook. The app menus are not localized, and we're not interested in localizing those.\n\n---\n\n## Dev\n\nBuilt with [Electron](https://electronjs.org).\n\n### Run\n\n```sh\nnpm install && npm start\n```\n\n### Build\n\nSee the [`electron-builder` docs](https://www.electron.build/multi-platform-build).\n\n### Publish\n\n```sh\nnpm run release\n```\n\nThen edit the automatically created GitHub Releases draft and publish.\n\n## Maintainers\n\n- [Dušan Simić](https://github.com/dusansimic)\n- [Lefteris Garyfalakis](https://github.com/lefterisgar)\n- [Michael Quevillon](https://github.com/mquevill)\n- [Nikolas Spiridakis](https://github.com/1nikolas)\n\n**Former**\n\n- [Jarek Radosz](https://github.com/CvX)\n\n## Links\n\n- [Product Hunt post](https://www.producthunt.com/posts/caprine-2)\n\n## Press\n\n- [The Essential Windows Apps for 2018 - Lifehacker](https://lifehacker.com/lifehacker-pack-for-windows-our-list-of-the-essential-1828117805)\n- [Caprine review: Customize Facebook Messenger on Windows 10 - Windows Central](https://www.windowscentral.com/caprine-review-customizing-facebook-messenger-windows-10)\n\n## Disclaimer\n\nCaprine is a third-party app and is not affiliated with Facebook.\n"
  },
  {
    "path": "source/autoplay.ts",
    "content": "import {ipcRenderer as ipc} from 'electron-better-ipc';\nimport selectors from './browser/selectors';\n\nconst conversationId = 'conversationWindow';\nconst disabledVideoId = 'disabled_autoplay';\n\nexport async function toggleVideoAutoplay(): Promise<void> {\n\tconst autoplayVideos = await ipc.callMain<undefined, boolean>('get-config-autoplayVideos');\n\tif (autoplayVideos) {\n\t\t// Stop the observers\n\t\tconversationDivObserver.disconnect();\n\t\tvideoObserver.disconnect();\n\n\t\t// Revert previous changes\n\t\tenableVideoAutoplay();\n\t} else {\n\t\t// Start the observer\n\t\tstartConversationWindowObserver();\n\n\t\t// Trigger once manually before observers kick in\n\t\tdisableVideoAutoplay(getVideos());\n\t}\n}\n\n// Hold reference to videos the user has started playing\n// Enables us to check if the video is autoplaying, for example, when changing conversation\nconst playedVideos: HTMLVideoElement[] = [];\n\nfunction disableVideoAutoplay(videos: NodeListOf<HTMLVideoElement>): void {\n\tfor (const video of videos) {\n\t\t// Don't disable currently playing videos\n\t\tif (playedVideos.includes(video)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst firstParent = video.parentElement!;\n\n\t\t// Video parent element which has a snapshot of the video as a background image\n\t\tconst parentWithBackground = video.parentElement!.parentElement!.parentElement!;\n\n\t\t// Hold reference to the background parent so we can revert our changes\n\t\tconst parentWithBackgroundParent = parentWithBackground.parentElement!;\n\n\t\t// Reference to the original play icon on top of the video\n\t\tconst playIcon = video.nextElementSibling!.nextElementSibling! as HTMLElement;\n\t\t// If the video is playing, the icon is hidden\n\t\tplayIcon.classList.remove('hidden_elem');\n\n\t\t// Set the `id` so we can easily trigger a click-event when reverting changes\n\t\tplayIcon.setAttribute('id', disabledVideoId);\n\n\t\tconst {\n\t\t\tstyle: {width, height},\n\t\t} = firstParent;\n\n\t\tconst style = parentWithBackground.style || window.getComputedStyle(parentWithBackground);\n\t\tconst backgroundImageSource = style.backgroundImage.slice(4, -1).replaceAll(/\"/, '');\n\n\t\t// Create the image to replace the video as a placeholder\n\t\tconst image = document.createElement('img');\n\t\timage.setAttribute('src', backgroundImageSource);\n\t\timage.classList.add('disabledAutoPlayImgTopRadius');\n\n\t\t// If it's a video without a source title bar at the bottom,\n\t\t// round the bottom part of the video\n\t\tif (parentWithBackgroundParent.childElementCount === 1) {\n\t\t\timage.classList.add('disabledAutoPlayImgBottomRadius');\n\t\t}\n\n\t\timage.setAttribute('height', height);\n\t\timage.setAttribute('width', width);\n\n\t\t// Create a separate instance of the play icon\n\t\t// Clone the existing icon to get the original events\n\t\t// Without creating a new icon, Messenger auto-hides the icon when scrolled to the video\n\t\tconst copiedPlayIcon = playIcon.cloneNode(true) as HTMLElement;\n\n\t\t// Remove the image and the new play icon and append the original divs\n\t\t// We can enable autoplay again by triggering this event\n\t\tcopiedPlayIcon.addEventListener('play', () => {\n\t\t\timage.remove();\n\t\t\tcopiedPlayIcon.remove();\n\t\t\tparentWithBackgroundParent.prepend(parentWithBackground);\n\t\t});\n\n\t\t// Separate handler for `click` so we know if it was the user who played the video\n\t\tcopiedPlayIcon.addEventListener('click', () => {\n\t\t\tplayedVideos.push(video);\n\t\t\tconst event = new Event('play');\n\t\t\tcopiedPlayIcon.dispatchEvent(event);\n\t\t\t// Sometimes the video doesn't start playing even though we trigger the click event\n\t\t\t// As a workaround, check if the video didn't start playing and manually trigger\n\t\t\t// the click event\n\t\t\tsetTimeout(() => {\n\t\t\t\tif (video.paused) {\n\t\t\t\t\tplayIcon.click();\n\t\t\t\t}\n\t\t\t}, 50);\n\t\t});\n\n\t\tparentWithBackgroundParent.prepend(image);\n\t\tparentWithBackgroundParent.prepend(copiedPlayIcon);\n\t\tparentWithBackground.remove();\n\t}\n}\n\n// If we previously disabled autoplay on videos,\n// trigger the `copiedPlayIcon` click event to revert changes\nfunction enableVideoAutoplay(): void {\n\tconst playIcons = document.querySelectorAll(`#${disabledVideoId}`);\n\tfor (const icon of playIcons) {\n\t\tconst event = new Event('play');\n\t\ticon.dispatchEvent(event);\n\t}\n}\n\nfunction getVideos(): NodeListOf<HTMLVideoElement> {\n\treturn document.querySelectorAll('video');\n}\n\nfunction startConversationWindowObserver(): void {\n\tconversationDivObserver.observe(document.documentElement, {\n\t\tchildList: true,\n\t\tsubtree: true,\n\t});\n}\n\nfunction startVideoObserver(element: Element): void {\n\tvideoObserver.observe(element, {\n\t\tchildList: true,\n\t\tsubtree: true,\n\t});\n}\n\n// A way to hold reference to conversation part of the document\n// Used to refresh `videoObserver` after the conversation reference is lost\nlet conversationWindow: Element;\nconst conversationDivObserver = new MutationObserver(_ => {\n\tlet conversation = document.querySelector(`#${conversationId}`);\n\n\t// Fetch it using `querySelector` if no luck with the `conversationId`\n\tconversation ||= document.querySelector(selectors.conversationSelector);\n\n\t// If we have a new reference\n\tif (conversation && conversationWindow !== conversation) {\n\t\t// Add `conversationId` so we know when we've lost the reference to\n\t\t// the `conversationWindow` and we can restart the video observer\n\t\tconversation.id = conversationId;\n\t\tconversationWindow = conversation;\n\t\tstartVideoObserver(conversationWindow);\n\t}\n});\n\n// Reference to mutation observer\n// Only active if the user has set option to disable video autoplay\nconst videoObserver = new MutationObserver(_ => {\n\t// Select by tag instead of iterating over mutations which is more performant\n\tconst videos = getVideos();\n\t// If videos were added disable autoplay\n\tif (videos.length > 0) {\n\t\tdisableVideoAutoplay(videos);\n\t}\n});\n"
  },
  {
    "path": "source/browser/conversation-list.ts",
    "content": "import {ipcRenderer as ipc} from 'electron-better-ipc';\nimport elementReady from 'element-ready';\nimport {isNull} from 'lodash';\nimport selectors from './selectors';\n\nconst icon = {\n\tread: 'data-caprine-icon',\n\tunread: 'data-caprine-icon-unread',\n};\n\nconst padding = {\n\ttop: 3,\n\tright: 0,\n\tbottom: 3,\n\tleft: 0,\n};\n\nfunction drawIcon(size: number, img?: HTMLImageElement): HTMLCanvasElement {\n\tconst canvas = document.createElement('canvas');\n\n\tif (img) {\n\t\tcanvas.width = size + padding.left + padding.right;\n\t\tcanvas.height = size + padding.top + padding.bottom;\n\n\t\tconst context = canvas.getContext('2d')!;\n\t\tcontext.beginPath();\n\t\tcontext.arc((size / 2) + padding.left, (size / 2) + padding.top, (size / 2), 0, Math.PI * 2, true);\n\t\tcontext.closePath();\n\t\tcontext.clip();\n\n\t\tcontext.drawImage(img, padding.left, padding.top, size, size);\n\t} else {\n\t\tcanvas.width = 0;\n\t\tcanvas.height = 0;\n\t}\n\n\treturn canvas;\n}\n\n// Return canvas with rounded image\nasync function urlToCanvas(url: string, size: number): Promise<HTMLCanvasElement> {\n\treturn new Promise(resolve => {\n\t\tconst img = new Image();\n\n\t\timg.setAttribute('crossorigin', 'anonymous');\n\n\t\timg.addEventListener('load', () => {\n\t\t\tresolve(drawIcon(size, img));\n\t\t});\n\n\t\timg.addEventListener('error', () => {\n\t\t\tconsole.error('Image not found', url);\n\t\t\tresolve(drawIcon(size));\n\t\t});\n\n\t\timg.src = url;\n\t});\n}\n\nasync function createIcons(element: HTMLElement, url: string): Promise<void> {\n\tconst canvas = await urlToCanvas(url, 50);\n\n\telement.setAttribute(icon.read, canvas.toDataURL());\n\n\tconst markerSize = 8;\n\tconst context = canvas.getContext('2d')!;\n\n\tcontext.fillStyle = '#f42020';\n\tcontext.beginPath();\n\tcontext.ellipse(canvas.width - markerSize, markerSize, markerSize, markerSize, 0, 0, 2 * Math.PI);\n\tcontext.closePath();\n\tcontext.fill();\n\n\telement.setAttribute(icon.unread, canvas.toDataURL());\n}\n\nasync function discoverIcons(element: HTMLElement): Promise<void> {\n\tif (element) {\n\t\treturn createIcons(element, element.getAttribute('src')!);\n\t}\n\n\tconsole.warn('Could not discover profile picture. Falling back to default image.');\n\n\t// Fall back to messenger favicon\n\tconst messengerIcon = document.querySelector('link[rel~=\"icon\"]');\n\n\tif (messengerIcon) {\n\t\treturn createIcons(element, messengerIcon.getAttribute('href')!);\n\t}\n\n\t// Fall back to facebook favicon\n\treturn createIcons(element, 'https://facebook.com/favicon.ico');\n}\n\nasync function getIcon(element: HTMLElement, unread: boolean): Promise<string> {\n\tif (element === null) {\n\t\treturn icon.read;\n\t}\n\n\tif (!element.getAttribute(icon.read)) {\n\t\tawait discoverIcons(element);\n\t}\n\n\treturn element.getAttribute(unread ? icon.unread : icon.read)!;\n}\n\nasync function getLabel(element: HTMLElement): Promise<string> {\n\tif (isNull(element)) {\n\t\treturn '';\n\t}\n\n\tconst emojis: HTMLElement[] = [];\n\tif (element !== null) {\n\t\tfor (const elementCurrent of element.children) {\n\t\t\temojis.push(elementCurrent as HTMLElement);\n\t\t}\n\t}\n\n\tfor (const emoji of emojis) {\n\t\temoji.outerHTML = emoji.querySelector('img')?.getAttribute('alt') ?? '';\n\t}\n\n\treturn element.textContent ?? '';\n}\n\nasync function createConversationNewDesign(element: HTMLElement): Promise<Conversation> {\n\tconst conversation: Partial<Conversation> = {};\n\t// TODO: Exclude muted conversations\n\t/*\n\tconst muted = Boolean(element.querySelector(selectors.muteIconNewDesign));\n\t*/\n\n\tconversation.selected = Boolean(element.querySelector('[role=row] [role=link] > div:only-child'));\n\tconversation.unread = Boolean(element.querySelector('[aria-label=\"Mark as Read\"]'));\n\n\tconst unparsedLabel = element.querySelector<HTMLElement>('.a8c37x1j.ni8dbmo4.stjgntxs.l9j0dhe7 > span > span')!;\n\tconversation.label = await getLabel(unparsedLabel);\n\n\tconst iconElement = element.querySelector<HTMLElement>('img')!;\n\tconversation.icon = await getIcon(iconElement, conversation.unread);\n\n\treturn conversation as Conversation;\n}\n\nasync function createConversationList(): Promise<Conversation[]> {\n\tconst conversationListSelector = selectors.conversationList;\n\n\tconst list = await elementReady(conversationListSelector, {\n\t\tstopOnDomReady: false,\n\t});\n\n\tif (!list) {\n\t\tconsole.error('Could not find conversation list', conversationListSelector);\n\t\treturn [];\n\t}\n\n\tconst elements: HTMLElement[] = [...list.children] as HTMLElement[];\n\n\t// Remove last element from childer list\n\telements.splice(-1, 1);\n\n\tconst conversations: Conversation[] = await Promise.all(elements.map(async element => createConversationNewDesign(element)));\n\n\treturn conversations;\n}\n\nexport async function sendConversationList(): Promise<void> {\n\tconst conversationsToRender: Conversation[] = await createConversationList();\n\tipc.callMain('conversations', conversationsToRender);\n}\n\nfunction generateStringFromNode(element: Element): string | undefined {\n\tconst cloneElement = element.cloneNode(true) as Element;\n\tlet emojiString;\n\n\tconst images = cloneElement.querySelectorAll('img');\n\tfor (const image of images) {\n\t\temojiString = image.alt;\n\t\t// Replace facebook's thumbs up with emoji\n\t\tif (emojiString === '(Y)' || emojiString === '(y)') {\n\t\t\temojiString = '👍';\n\t\t}\n\n\t\timage.parentElement?.replaceWith(document.createTextNode(emojiString));\n\t}\n\n\treturn cloneElement.textContent ?? undefined;\n}\n\nfunction countUnread(mutationsList: MutationRecord[]): void {\n\tconst alreadyChecked: string[] = [];\n\n\tconst unreadMutations = mutationsList.filter(mutation =>\n\t\t// When a conversations \"becomes unread\".\n\t\t(\n\t\t\tmutation.type === 'childList'\n\t\t\t&& mutation.addedNodes.length > 0\n\t\t\t&& ((mutation.addedNodes[0] as Element).className === selectors.conversationSidebarUnreadDot)\n\t\t)\n\t\t// When text is received\n\t\t|| (\n\t\t\tmutation.type === 'characterData'\n\t\t\t// Make sure the text corresponds to a conversation\n\t\t\t&& mutation.target.parentElement?.parentElement?.parentElement?.className === selectors.conversationSidebarTextParent\n\t\t)\n\t\t// When an emoji is received, node(s) are added\n\t\t|| (\n\t\t\tmutation.type === 'childList'\n\t\t\t// Make sure the mutation corresponds to a conversation\n\t\t\t&& mutation.target.parentElement?.parentElement?.className === selectors.conversationSidebarTextParent\n\t\t)\n\t\t// Emoji change\n\t\t|| (\n\t\t\tmutation.type === 'attributes'\n\t\t\t&& mutation.target.parentElement?.parentElement?.parentElement?.parentElement?.className === selectors.conversationSidebarTextParent\n\t\t));\n\n\t// Check latest mutation first\n\tfor (const mutation of unreadMutations.reverse()) {\n\t\tconst current = (mutation.target.parentElement as Element).closest(selectors.conversationSidebarSelector)!;\n\n\t\tconst href = current.closest('[role=\"link\"]')?.getAttribute('href');\n\n\t\tif (!href) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// It is possible to have multiple mutations for the same conversation, but we only want one notification.\n\t\t// So if the current conversation has already been checked, continue.\n\t\t// Additionally if the conversation is not unread, then also continue.\n\t\tif (alreadyChecked.includes(href) || !current.querySelector('.' + selectors.conversationSidebarUnreadDot.replaceAll(/ /, '.'))) {\n\t\t\tcontinue;\n\t\t}\n\n\t\talreadyChecked.push(href);\n\n\t\t// Get the image data URI from the parent of the author/text\n\t\tconst imgUrl = current.querySelector('img')?.dataset.caprineIcon;\n\t\tconst textOptions = current.querySelectorAll(selectors.conversationSidebarTextSelector);\n\t\t// Get the author and text of the new message\n\t\tconst titleTextNode = textOptions[0];\n\t\tconst bodyTextNode = textOptions[1];\n\n\t\tconst titleText = generateStringFromNode(titleTextNode);\n\t\tconst bodyText = generateStringFromNode(bodyTextNode);\n\n\t\tif (!bodyText || !titleText || !imgUrl) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Send a notification\n\t\tipc.callMain('notification', {\n\t\t\tid: 0,\n\t\t\ttitle: titleText,\n\t\t\tbody: bodyText,\n\t\t\ticon: imgUrl,\n\t\t\tsilent: false,\n\t\t});\n\t}\n}\n\nasync function updateTrayIcon(): Promise<void> {\n\tlet messageCount = 0;\n\n\tawait elementReady(selectors.chatsIcon, {stopOnDomReady: false});\n\n\t// Count unread messages in Chats, Marketplace, etc.\n\tfor (const element of document.querySelectorAll<HTMLElement>(selectors.chatsIcon)) {\n\t\t// Extract messageNumber from ariaLabel\n\t\tconst messageNumber = element?.ariaLabel?.match(/\\d+/g);\n\n\t\tif (messageNumber) {\n\t\t\tmessageCount += Number.parseInt(messageNumber[0], 10);\n\t\t}\n\t}\n\n\tipc.callMain('update-tray-icon', messageCount);\n}\n\nwindow.addEventListener('load', async () => {\n\tconst sidebar = await elementReady('[role=navigation]:has([role=grid])', {stopOnDomReady: false});\n\tconst leftSidebar = await elementReady(`${selectors.leftSidebar}:has(${selectors.chatsIcon})`, {stopOnDomReady: false});\n\n\tif (sidebar) {\n\t\tconst conversationListObserver = new MutationObserver(async () => sendConversationList());\n\t\tconst conversationCountObserver = new MutationObserver(countUnread);\n\n\t\tconversationListObserver.observe(sidebar, {\n\t\t\tsubtree: true,\n\t\t\tchildList: true,\n\t\t\tattributes: true,\n\t\t\tattributeFilter: ['class'],\n\t\t});\n\n\t\tconversationCountObserver.observe(sidebar, {\n\t\t\tcharacterData: true,\n\t\t\tsubtree: true,\n\t\t\tchildList: true,\n\t\t\tattributes: true,\n\t\t\tattributeFilter: ['src', 'alt'],\n\t\t});\n\t}\n\n\tif (leftSidebar) {\n\t\tconst chatsIconObserver = new MutationObserver(async () => updateTrayIcon());\n\n\t\tchatsIconObserver.observe(leftSidebar, {\n\t\t\tsubtree: true,\n\t\t\tchildList: true,\n\t\t\tattributes: true,\n\t\t\tattributeFilter: ['aria-label'],\n\t\t});\n\t}\n});\n"
  },
  {
    "path": "source/browser/selectors.ts",
    "content": "export default {\n\tleftSidebar: '[role=\"navigation\"][class=\"x9f619 x1n2onr6 x1ja2u2z x78zum5 xdt5ytf x2lah0s x193iq5w xeuugli\"] > div > div', // ! Tray icon dependency\n\tchatsIcon: '[class=\"x9f619 x1n2onr6 x1ja2u2z x78zum5 xdt5ytf x2lah0s x193iq5w xdj266r\"] a', // ! Tray icon dependency\n\tconversationList: '[role=navigation] [role=grid] [class=\"x1n2onr6\"]',\n\tconversationSelector: '[role=main] [role=grid]',\n\tconversationSidebarUnreadDot: 'x1i10hfl x1qjc9v5 xjbqb8w xjqpnuy xa49m3k xqeqjp1 x2hbi6w x13fuv20 xu3j5b3 x1q0q8m5 x26u7qi x972fbf xcfux6l x1qhh985 xm0m39n x9f619 x1ypdohk xdl72j9 x2lah0s xe8uvvx xdj266r x11i5rnm xat24cr x1mh8g0r x2lwn1j xeuugli xexx8yu x4uap5 x18d9i69 xkhd6sd x1n2onr6 x16tdsg8 x1hl2dhg xggy1nq x1ja2u2z x1t137rt x1o1ewxj x3x9cwd x1e5q0jg x13rtm0m x1q0g3np x87ps6o x1lku1pv x78zum5 x1a2a7pz',\n\tconversationSidebarTextParent: 'html-span xdj266r x11i5rnm xat24cr x1mh8g0r xexx8yu x18d9i69 xkhd6sd x1hl2dhg x16tdsg8 x1vvkbs x6s0dn4 x9f619 x78zum5 x193iq5w xeuugli xg83lxy', // Parent element of the conversation text element (needed for notifications)\n\tconversationSidebarTextSelector: '[class=\"x1lliihq x193iq5w x6ikm8r x10wlt62 xlyipyv xuxw1ft\"]', // Generic selector for the text contents of all conversations\n\tconversationSidebarSelector: '[class=\"x9f619 x1n2onr6 x1ja2u2z x78zum5 x2lah0s x1qughib x6s0dn4 xozqiw3 x1q0g3np\"]', // Selector for the top level element of a single conversation (children contain text content of the conversation and conversation image)\n\tnotificationCheckbox: '._374b:nth-of-type(4) ._4ng2 input',\n\trightSidebarMenu: '.x6s0dn4.x3nfvp2.x1fgtraw.xl56j7k.x1n2onr6.xgd8bvy',\n\trightSidebarButtons: '.x9f619.x1ja2u2z.x78zum5.x2lah0s.x1n2onr6.xl56j7k.x1qjc9v5.xozqiw3.x1q0g3np.xn6708d.x1ye3gou.x1cnzs8.xdj266r.x11i5rnm.xat24cr.x1mh8g0r > div [role=button]',\n\tmuteIconNewDesign: 'path[d=\"M29.676 7.746c.353-.352.44-.92.15-1.324a1 1 0 00-1.524-.129L6.293 28.29a1 1 0 00.129 1.523c.404.29.972.204 1.324-.148l3.082-3.08A2.002 2.002 0 0112.242 26h15.244c.848 0 1.57-.695 1.527-1.541-.084-1.643-1.87-1.145-2.2-3.515l-1.073-8.157-.002-.01a1.976 1.976 0 01.562-1.656l3.376-3.375zm-9.165 20.252H15.51c-.313 0-.565.275-.506.575.274 1.38 1.516 2.422 3.007 2.422 1.49 0 2.731-1.042 3.005-2.422.06-.3-.193-.575-.505-.575zm-10.064-6.719L22.713 9.02a.997.997 0 00-.124-1.51 7.792 7.792 0 00-12.308 5.279l-1.04 7.897c-.089.672.726 1.074 1.206.594z\"]',\n\t// ! Very fragile selector (most likely cause of hidden dialog issue)\n\tclosePreferencesButton: 'div[role=dialog] > div > div > div:nth-child(2) > [role=button]',\n\tuserMenu: '.qi72231t.o9w3sbdw.nu7423ey.tav9wjvu.flwp5yud.tghlliq5.gkg15gwv.s9ok87oh.s9ljgwtm.lxqftegz.bf1zulr9.frfouenu.bonavkto.djs4p424.r7bn319e.bdao358l.fsf7x5fv.tgm57n0e.jez8cy9q.s5oniofx.m8h3af8h.l7ghb35v.kjdc1dyq.kmwttqpk.dnr7xe2t.aeinzg81.srn514ro.oxkhqvkx.rl78xhln.nch0832m.om3e55n1.cr00lzj9.rn8ck1ys.s3jn8y49.g4tp4svg.o9erhkwx.dzqi5evh.hupbnkgi.hvb2xoa8.fxk3tzhb.jl2a5g8c.f14ij5to.l3ldwz01.icdlwmnq > .aglvbi8b.om3e55n1.i8zpp7h3.g4tp4svg',\n\tuserMenuNewSidebar: '[role=navigation]  > div >  div:nth-child(2) > div > div > div:nth-child(1) [role=button]',\n\tviewsMenu: '.x9f619.x1n2onr6.x1ja2u2z.x78zum5.xdt5ytf.x2lah0s.x193iq5w.xdj266r',\n\tselectedConversation: '[role=navigation] [role=grid] [role=row] [role=gridcell] [role=link][aria-current=page]',\n\t// ! Very fragile selector (most likely cause of hidden dialog issue)\n\tpreferencesSelector: '.x1n2onr6.x1ja2u2z.x1afcbsf.x78zum5.xdt5ytf.x1a2a7pz.x6ikm8r.x10wlt62.x71s49j.x1jx94hy.x1g2kw80.xxadwq3.x16n5opg.x3hh19s.xl7ujzl.x1kl8bxo.xhkep3z.xb3b7hn.xwhkkir.x1n7qst7.x17omtbh:has(.x1l90r2v.x1swvt13.x1pi30zi)',\n\t// TODO: Fix this selector for new design\n\tmessengerSoundsSelector: '._374d ._6bkz',\n\tconversationMenuSelectorNewDesign: '[role=menu]',\n};\n"
  },
  {
    "path": "source/browser-call.ts",
    "content": "import elementReady from 'element-ready';\n\n(async () => {\n\tconst startCallButton = (await elementReady<HTMLElement>('._3quh._30yy._2t_', {\n\t\tstopOnDomReady: false,\n\t}))!;\n\n\tstartCallButton.click();\n})();\n"
  },
  {
    "path": "source/browser.ts",
    "content": "import process from 'node:process';\nimport {ipcRenderer as ipc} from 'electron-better-ipc';\nimport {is} from 'electron-util';\nimport elementReady from 'element-ready';\nimport {nativeTheme} from '@electron/remote';\nimport selectors from './browser/selectors';\nimport {toggleVideoAutoplay} from './autoplay';\nimport {sendConversationList} from './browser/conversation-list';\nimport {IToggleSounds} from './types';\n\nasync function withMenu(\n\tmenuButtonElement: HTMLElement,\n\tcallback: () => Promise<void> | void,\n): Promise<void> {\n\tconst {classList} = document.documentElement;\n\n\t// Prevent the dropdown menu from displaying\n\tclassList.add('hide-dropdowns');\n\n\t// Click the menu button\n\tmenuButtonElement.click();\n\n\t// Wait for the menu to close before removing the 'hide-dropdowns' class\n\tawait elementReady('.x78zum5.xdt5ytf.x1n2onr6.xat3117.xxzkxad > div:nth-child(2) > div', {stopOnDomReady: false});\n\tconst menuLayer = document.querySelector('.x78zum5.xdt5ytf.x1n2onr6.xat3117.xxzkxad > div:nth-child(2) > div');\n\n\tif (menuLayer) {\n\t\tconst observer = new MutationObserver(() => {\n\t\t\tif (!menuLayer.hasChildNodes()) {\n\t\t\t\tclassList.remove('hide-dropdowns');\n\t\t\t\tobserver.disconnect();\n\t\t\t}\n\t\t});\n\t\tobserver.observe(menuLayer, {childList: true});\n\t} else {\n\t\t// Fallback in case .uiContextualLayerPositioner is missing\n\t\tclassList.remove('hide-dropdowns');\n\t}\n\n\tawait callback();\n}\n\nasync function isNewSidebar(): Promise<boolean> {\n\t// TODO: stopOnDomReady might not be needed\n\tawait elementReady(selectors.leftSidebar, {stopOnDomReady: false});\n\n\tconst sidebars = document.querySelectorAll<HTMLElement>(selectors.leftSidebar);\n\n\treturn sidebars.length === 2;\n}\n\nasync function withSettingsMenu(callback: () => Promise<void> | void): Promise<void> {\n\t// Wait for navigation pane buttons to show up\n\tconst settingsMenu = await elementReady(selectors.userMenuNewSidebar, {stopOnDomReady: false});\n\n\tawait withMenu(settingsMenu as HTMLElement, callback);\n}\n\nasync function selectMenuItem(itemNumber: number): Promise<void> {\n\tlet selector;\n\n\t// Wait for menu to show up\n\tawait elementReady(selectors.conversationMenuSelectorNewDesign, {stopOnDomReady: false});\n\n\tconst items = document.querySelectorAll<HTMLElement>(\n\t\t`${selectors.conversationMenuSelectorNewDesign} [role=menuitem]`,\n\t);\n\n\t// Negative items will select from the end\n\tif (itemNumber < 0) {\n\t\tselector = -itemNumber <= items.length ? items[items.length + itemNumber] : null;\n\t} else {\n\t\tselector = itemNumber <= items.length ? items[itemNumber - 1] : null;\n\t}\n\n\tif (selector) {\n\t\tselector.click();\n\t}\n}\n\nasync function selectOtherListViews(itemNumber: number): Promise<void> {\n\t// In case one of other views is shown\n\tclickBackButton();\n\n\tconst newSidebar = await isNewSidebar();\n\n\tif (newSidebar) {\n\t\tconst items = document.querySelectorAll<HTMLElement>(\n\t\t\t`${selectors.viewsMenu} span > a`,\n\t\t);\n\n\t\tconst selector = itemNumber <= items.length ? items[itemNumber - 1] : null;\n\n\t\tif (selector) {\n\t\t\tselector.click();\n\t\t}\n\t} else {\n\t\tawait withSettingsMenu(() => {\n\t\t\tselectMenuItem(itemNumber);\n\t\t});\n\t}\n}\n\nfunction clickBackButton(): void {\n\tconst backButton = document.querySelector<HTMLElement>('._30yy._2oc9');\n\n\tif (backButton) {\n\t\tbackButton.click();\n\t}\n}\n\nipc.answerMain('show-preferences', async () => {\n\tif (isPreferencesOpen()) {\n\t\treturn;\n\t}\n\n\tawait openPreferences();\n});\n\nipc.answerMain('new-conversation', async () => {\n\tdocument.querySelector<HTMLElement>('[href=\"/new/\"]')!.click();\n});\n\nipc.answerMain('new-room', async () => {\n\tdocument.querySelector<HTMLElement>('.x16n37ib .x1i10hfl.x6umtig.x1b1mbwd.xaqea5y.xav7gou.x1ypdohk.xe8uvvx.xdj266r.x11i5rnm.xat24cr.x1mh8g0r.x16tdsg8.x1hl2dhg.xggy1nq.x87ps6o.x1lku1pv.x1a2a7pz.x6s0dn4.x14yjl9h.xudhj91.x18nykt9.xww2gxu.x972fbf.xcfux6l.x1qhh985.xm0m39n.x9f619.x78zum5.xl56j7k.xexx8yu.x4uap5.x18d9i69.xkhd6sd.x1n2onr6.xc9qbxq.x14qfxbe.x1qhmfi1')!.click();\n});\n\nipc.answerMain('log-out', async () => {\n\tconst useWorkChat = await ipc.callMain<undefined, boolean>('get-config-useWorkChat');\n\tif (useWorkChat) {\n\t\tdocument.querySelector<HTMLElement>('._5lxs._3qct._p')!.click();\n\n\t\t// Menu creation is slow\n\t\tsetTimeout(() => {\n\t\t\tconst nodes = document.querySelectorAll<HTMLElement>(\n\t\t\t\t'._54nq._9jo._558b._2n_z li:last-child a',\n\t\t\t);\n\n\t\t\tnodes[nodes.length - 1].click();\n\t\t}, 250);\n\t} else {\n\t\tawait withSettingsMenu(() => {\n\t\t\tselectMenuItem(-1);\n\t\t});\n\t}\n});\n\nipc.answerMain('find', () => {\n\tdocument.querySelector<HTMLElement>('[type=\"search\"]')!.focus();\n});\n\nasync function openSearchInConversation() {\n\tconst mainView = document.querySelector('.x9f619.x1ja2u2z.x78zum5.x1n2onr6.x1r8uery.x1iyjqo2.xs83m0k.xeuugli.x1qughib.x1qjc9v5.xozqiw3.x1q0g3np.xexx8yu.x85a59c')!;\n\tconst rightSidebarIsClosed = Boolean(mainView.querySelector<HTMLElement>(':scope > div:only-child'));\n\n\tif (rightSidebarIsClosed) {\n\t\tdocument.querySelector<HTMLElement>(selectors.rightSidebarMenu)?.click();\n\t}\n\n\tawait elementReady(selectors.rightSidebarButtons, {stopOnDomReady: false});\n\tconst buttonList = document.querySelectorAll<HTMLElement>(selectors.rightSidebarButtons);\n\n\t// Search in conversation is the last button\n\tbuttonList[buttonList.length - 1].click();\n}\n\nipc.answerMain('search', () => {\n\topenSearchInConversation();\n});\n\nipc.answerMain('insert-gif', () => {\n\tdocument.querySelector<HTMLElement>('.x1n2onr6.x1iyjqo2.xw2csxc > div:nth-child(3) > span > div')!.click();\n});\n\nipc.answerMain('insert-emoji', async () => {\n\tdocument.querySelector<HTMLElement>('.x1n2onr6.x1iyjqo2.xw2csxc > div:nth-child(5) > span > div')!.click();\n});\n\nipc.answerMain('insert-sticker', () => {\n\tdocument.querySelector<HTMLElement>('.x1n2onr6.x1iyjqo2.xw2csxc > div:nth-child(2) > span > div')!.click();\n});\n\nipc.answerMain('attach-files', () => {\n\tdocument.querySelector<HTMLElement>('.x1n2onr6.x1iyjqo2.xw2csxc > div:nth-child(1) > span > div')!.click();\n});\n\nipc.answerMain('focus-text-input', () => {\n\tdocument.querySelector<HTMLElement>('[role=textbox][contenteditable=true]')!.focus();\n});\n\nipc.answerMain('next-conversation', nextConversation);\n\nipc.answerMain('previous-conversation', previousConversation);\n\nipc.answerMain('mute-conversation', async () => {\n\tawait openMuteModal();\n});\n\nipc.answerMain('delete-conversation', async () => {\n\tconst index = selectedConversationIndex();\n\n\tif (index !== -1) {\n\t\tawait deleteSelectedConversation();\n\n\t\tconst key = index + 1;\n\t\tawait jumpToConversation(key);\n\t}\n});\n\nipc.answerMain('archive-conversation', async () => {\n\tconst index = selectedConversationIndex();\n\n\tif (index !== -1) {\n\t\tawait archiveSelectedConversation();\n\n\t\tconst key = index + 1;\n\t\tawait jumpToConversation(key);\n\t}\n});\n\nasync function openHiddenPreferences(): Promise<boolean> {\n\tif (!isPreferencesOpen()) {\n\t\tdocument.documentElement.classList.add('hide-preferences-window');\n\n\t\tawait openPreferences();\n\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\nasync function toggleSounds({checked}: IToggleSounds): Promise<void> {\n\tconst shouldClosePreferences = await openHiddenPreferences();\n\n\tconst soundsCheckbox = document.querySelector<HTMLInputElement>(`${selectors.preferencesSelector} ${selectors.messengerSoundsSelector}`)!;\n\tif (checked === undefined || checked !== soundsCheckbox.checked) {\n\t\tsoundsCheckbox.click();\n\t}\n\n\tif (shouldClosePreferences) {\n\t\tawait closePreferences();\n\t}\n}\n\nipc.answerMain('toggle-sounds', toggleSounds);\n\nipc.answerMain('toggle-mute-notifications', async () => {\n\tconst shouldClosePreferences = await openHiddenPreferences();\n\n\tconst notificationCheckbox = document.querySelector<HTMLInputElement>(\n\t\tselectors.notificationCheckbox,\n\t)!;\n\n\tif (shouldClosePreferences) {\n\t\tawait closePreferences();\n\t}\n\n\t// TODO: Fix notifications\n\tif (notificationCheckbox === null) {\n\t\treturn false;\n\t}\n\n\treturn !notificationCheckbox.checked;\n});\n\nipc.answerMain('toggle-message-buttons', async () => {\n\tconst showMessageButtons = await ipc.callMain<undefined, boolean>('get-config-showMessageButtons');\n\tdocument.body.classList.toggle('show-message-buttons', !showMessageButtons);\n});\n\nipc.answerMain('show-chats-view', async () => {\n\tawait selectOtherListViews(1);\n});\n\nipc.answerMain('show-marketplace-view', async () => {\n\tawait selectOtherListViews(2);\n});\n\nipc.answerMain('show-requests-view', async () => {\n\tawait selectOtherListViews(3);\n});\n\nipc.answerMain('show-archive-view', async () => {\n\tawait selectOtherListViews(4);\n});\n\nipc.answerMain('toggle-video-autoplay', () => {\n\ttoggleVideoAutoplay();\n});\n\nipc.answerMain('reload', () => {\n\tlocation.reload();\n});\n\nasync function setTheme(): Promise<void> {\n\ttype ThemeSource = typeof nativeTheme.themeSource;\n\tconst theme = await ipc.callMain<undefined, ThemeSource>('get-config-theme');\n\tnativeTheme.themeSource = theme;\n\tsetThemeElement(document.documentElement);\n\tupdateVibrancy();\n}\n\nfunction setThemeElement(element: HTMLElement): void {\n\tconst useDarkColors = Boolean(nativeTheme.shouldUseDarkColors);\n\telement.classList.toggle('dark-mode', useDarkColors);\n\telement.classList.toggle('light-mode', !useDarkColors);\n\telement.classList.toggle('__fb-dark-mode', useDarkColors);\n\telement.classList.toggle('__fb-light-mode', !useDarkColors);\n\tremoveThemeClasses(useDarkColors);\n}\n\nfunction removeThemeClasses(useDarkColors: boolean): void {\n\t// TODO: Workaround for Facebooks buggy frontend\n\t// The ui sometimes hardcodes ligth mode classes in the ui. This removes them so the class\n\t// in the root element would be used.\n\tconst className = useDarkColors ? '__fb-light-mode' : '__fb-dark-mode';\n\tfor (const element of document.querySelectorAll(`.${className}`)) {\n\t\telement.classList.remove(className);\n\t}\n}\n\nasync function observeTheme(): Promise<void> {\n\t/* Main document's class list */\n\tconst observer = new MutationObserver((records: MutationRecord[]) => {\n\t\t// Find records that had class attribute changed\n\t\tconst classRecords = records.filter(record => record.type === 'attributes' && record.attributeName === 'class');\n\t\t// Check if dark mode classes exists\n\t\tconst isDark = classRecords.some(record => {\n\t\t\tconst {classList} = (record.target as HTMLElement);\n\t\t\treturn classList.contains('dark-mode') && classList.contains('__fb-dark-mode');\n\t\t});\n\t\t// If config and class list don't match, update class list\n\t\tif (nativeTheme.shouldUseDarkColors !== isDark) {\n\t\t\tsetTheme();\n\t\t}\n\t});\n\n\tobserver.observe(document.documentElement, {attributes: true, attributeFilter: ['class']});\n\n\t/* Added nodes (dialogs, etc.) */\n\tconst observerNew = new MutationObserver((records: MutationRecord[]) => {\n\t\tconst nodeRecords = records.filter(record => record.addedNodes.length > 0);\n\t\tfor (const nodeRecord of nodeRecords) {\n\t\t\tfor (const newNode of nodeRecord.addedNodes) {\n\t\t\t\tconst {classList} = (newNode as HTMLElement);\n\t\t\t\tconst isLight = classList.contains('light-mode') || classList.contains('__fb-light-mode');\n\t\t\t\tif (nativeTheme.shouldUseDarkColors === isLight) {\n\t\t\t\t\tsetThemeElement(newNode as HTMLElement);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t});\n\n\t/* Observe only elements where new nodes may need dark mode */\n\tconst menuElements = await elementReady('.j83agx80.cbu4d94t.l9j0dhe7.jgljxmt5.be9z9djy > div:nth-of-type(2) > div', {stopOnDomReady: false});\n\tif (menuElements) {\n\t\tobserverNew.observe(menuElements, {childList: true});\n\t}\n\n\tconst modalElements = await elementReady(selectors.preferencesSelector, {stopOnDomReady: false});\n\tif (modalElements) {\n\t\tobserverNew.observe(modalElements, {childList: true});\n\t}\n}\n\nasync function setPrivateMode(): Promise<void> {\n\tconst privateMode = await ipc.callMain<undefined, boolean>('get-config-privateMode');\n\tdocument.documentElement.classList.toggle('private-mode', privateMode);\n\n\tif (is.macos) {\n\t\tsendConversationList();\n\t}\n}\n\nasync function updateVibrancy(): Promise<void> {\n\tconst {classList} = document.documentElement;\n\n\tclassList.remove('sidebar-vibrancy', 'full-vibrancy');\n\n\tconst vibrancy = await ipc.callMain<undefined, 'sidebar' | 'none' | 'full'>('get-config-vibrancy');\n\n\tswitch (vibrancy) {\n\t\tcase 'sidebar': {\n\t\t\tclassList.add('sidebar-vibrancy');\n\t\t\tbreak;\n\t\t}\n\n\t\tcase 'full': {\n\t\t\tclassList.add('full-vibrancy');\n\t\t\tbreak;\n\t\t}\n\n\t\tdefault:\n\t}\n\n\tipc.callMain('set-vibrancy');\n}\n\nasync function updateSidebar(): Promise<void> {\n\tconst {classList} = document.documentElement;\n\n\tclassList.remove('sidebar-hidden', 'sidebar-force-narrow', 'sidebar-force-wide');\n\n\tconst sidebar = await ipc.callMain<undefined, 'default' | 'hidden' | 'narrow' | 'wide'>('get-config-sidebar');\n\n\tswitch (sidebar) {\n\t\tcase 'hidden': {\n\t\t\tclassList.add('sidebar-hidden');\n\t\t\tbreak;\n\t\t}\n\n\t\tcase 'narrow': {\n\t\t\tclassList.add('sidebar-force-narrow');\n\t\t\tbreak;\n\t\t}\n\n\t\tcase 'wide': {\n\t\t\tclassList.add('sidebar-force-wide');\n\t\t\tbreak;\n\t\t}\n\n\t\tdefault:\n\t}\n}\n\nasync function updateDoNotDisturb(): Promise<void> {\n\t/* TODO: Implement this function\n\tconst shouldClosePreferences = await openHiddenPreferences();\n\n\tif (shouldClosePreferences) {\n\t\tawait closePreferences();\n\t}\n\t*/\n}\n\nfunction renderOverlayIcon(messageCount: number): HTMLCanvasElement {\n\tconst canvas = document.createElement('canvas');\n\tcanvas.height = 128;\n\tcanvas.width = 128;\n\tcanvas.style.letterSpacing = '-5px';\n\n\tconst context = canvas.getContext('2d')!;\n\tcontext.fillStyle = '#f42020';\n\tcontext.beginPath();\n\tcontext.ellipse(64, 64, 64, 64, 0, 0, 2 * Math.PI);\n\tcontext.fill();\n\tcontext.textAlign = 'center';\n\tcontext.fillStyle = 'white';\n\tcontext.font = '90px sans-serif';\n\tcontext.fillText(String(Math.min(99, messageCount)), 64, 96);\n\n\treturn canvas;\n}\n\nipc.answerMain('update-sidebar', () => {\n\tupdateSidebar();\n});\n\nipc.answerMain('set-theme', setTheme);\n\nipc.answerMain('set-private-mode', setPrivateMode);\n\nipc.answerMain('update-vibrancy', () => {\n\tupdateVibrancy();\n});\n\nipc.answerMain('render-overlay-icon', (messageCount: number): {data: string; text: string} => ({\n\tdata: renderOverlayIcon(messageCount).toDataURL(),\n\ttext: String(messageCount),\n}));\n\nipc.answerMain('render-native-emoji', (emoji: string): string => {\n\tconst canvas = document.createElement('canvas');\n\tconst context = canvas.getContext('2d')!;\n\tconst systemFont = is.linux ? 'emoji, system-ui' : 'system-ui';\n\tcanvas.width = 256;\n\tcanvas.height = 256;\n\tcontext.textAlign = 'center';\n\tcontext.textBaseline = 'middle';\n\tif (is.macos) {\n\t\tcontext.font = `256px ${systemFont}`;\n\t\tcontext.fillText(emoji, 128, 154);\n\t} else {\n\t\tcontext.textBaseline = 'bottom';\n\t\tcontext.font = `225px ${systemFont}`;\n\t\tcontext.fillText(emoji, 128, 256);\n\t}\n\n\tconst dataUrl = canvas.toDataURL();\n\treturn dataUrl;\n});\n\nipc.answerMain('zoom-reset', async () => {\n\tawait setZoom(1);\n});\n\nipc.answerMain('zoom-in', async () => {\n\tlet zoomFactor = await ipc.callMain<undefined, number>('get-config-zoomFactor');\n\tzoomFactor += 0.1;\n\n\tif (zoomFactor < 1.6) {\n\t\tawait setZoom(zoomFactor);\n\t}\n});\n\nipc.answerMain('zoom-out', async () => {\n\tlet zoomFactor = await ipc.callMain<undefined, number>('get-config-zoomFactor');\n\tzoomFactor -= 0.1;\n\n\tif (zoomFactor >= 0.8) {\n\t\tawait setZoom(zoomFactor);\n\t}\n});\n\nipc.answerMain('jump-to-conversation', async (key: number) => {\n\tawait jumpToConversation(key);\n});\n\nasync function nextConversation(): Promise<void> {\n\tconst index = selectedConversationIndex(1);\n\n\tif (index !== -1) {\n\t\tawait selectConversation(index);\n\t}\n}\n\nasync function previousConversation(): Promise<void> {\n\tconst index = selectedConversationIndex(-1);\n\n\tif (index !== -1) {\n\t\tawait selectConversation(index);\n\t}\n}\n\nasync function jumpToConversation(key: number): Promise<void> {\n\tconst index = key - 1;\n\tawait selectConversation(index);\n}\n\n// Focus on the conversation with the given index\nasync function selectConversation(index: number): Promise<void> {\n\tconst list = await elementReady(selectors.conversationList, {stopOnDomReady: false});\n\n\tif (!list) {\n\t\tconsole.error('Could not find conversations list', selectors.conversationList);\n\t\treturn;\n\t}\n\n\tconst conversation = list.children[index];\n\n\tif (!conversation) {\n\t\tconsole.error('Could not find conversation', index);\n\t\treturn;\n\t}\n\n\tconversation.querySelector<HTMLLegendElement>('[role=link]')!.click();\n}\n\nfunction selectedConversationIndex(offset = 0): number {\n\tconst selected = document.querySelector<HTMLElement>(selectors.selectedConversation);\n\n\tif (!selected) {\n\t\treturn -1;\n\t}\n\n\tconst newSelected = selected.closest(`${selectors.conversationList} > div`)!;\n\n\tconst list = [...newSelected.parentNode!.children];\n\tconst index = list.indexOf(newSelected) + offset;\n\n\treturn ((index % list.length) + list.length) % list.length;\n}\n\nasync function setZoom(zoomFactor: number): Promise<void> {\n\tconst node = document.querySelector<HTMLElement>('#zoomFactor')!;\n\tnode.textContent = `${selectors.conversationSelector} {zoom: ${zoomFactor} !important}`;\n\tawait ipc.callMain<number, void>('set-config-zoomFactor', zoomFactor);\n}\n\nasync function withConversationMenu(callback: () => void): Promise<void> {\n\t// eslint-disable-next-line @typescript-eslint/ban-types\n\tlet menuButton: HTMLElement | null = null;\n\tconst conversation = document.querySelector<HTMLElement>(selectors.selectedConversation)!.closest(`${selectors.conversationList} > div`);\n\n\tmenuButton = conversation?.querySelector('[aria-label=Menu][role=button]') ?? null;\n\n\tif (menuButton) {\n\t\tawait withMenu(menuButton, callback);\n\t}\n}\n\nasync function openMuteModal(): Promise<void> {\n\tawait withConversationMenu(() => {\n\t\tselectMenuItem(2);\n\t});\n}\n\n/*\nThese functions assume:\n- There is a selected conversation.\n- That the conversation already has its conversation menu open.\n\nIn other words, you should only use this function within a callback that is provided to `withConversationMenu()`, because `withConversationMenu()` makes sure to have the conversation menu open before executing the callback and closes the conversation menu afterwards.\n*/\nfunction isSelectedConversationGroup(): boolean {\n\t// Individual conversations include an entry for \"View Profile\", which is type `a`\n\treturn !document.querySelector<HTMLElement>(`${selectors.conversationMenuSelectorNewDesign} a[role=menuitem]`);\n}\n\nfunction isSelectedConversationMetaAI(): boolean {\n\t// Meta AI menu only has 1 separator of type `hr`\n\treturn !document.querySelector<HTMLElement>(`${selectors.conversationMenuSelectorNewDesign} hr:nth-of-type(2)`);\n}\n\nasync function archiveSelectedConversation(): Promise<void> {\n\tawait withConversationMenu(() => {\n\t\tconst [isGroup, isNotGroup, isMetaAI] = [-4, -3, -2];\n\n\t\tlet archiveMenuIndex;\n\t\tif (isSelectedConversationMetaAI()) {\n\t\t\tarchiveMenuIndex = isMetaAI;\n\t\t} else if (isSelectedConversationGroup()) {\n\t\t\tarchiveMenuIndex = isGroup;\n\t\t} else {\n\t\t\tarchiveMenuIndex = isNotGroup;\n\t\t}\n\n\t\tselectMenuItem(archiveMenuIndex);\n\t});\n}\n\nasync function deleteSelectedConversation(): Promise<void> {\n\tawait withConversationMenu(() => {\n\t\tconst [isGroup, isNotGroup, isMetaAI] = [-3, -2, -1];\n\n\t\tlet deleteMenuIndex;\n\t\tif (isSelectedConversationMetaAI()) {\n\t\t\tdeleteMenuIndex = isMetaAI;\n\t\t} else if (isSelectedConversationGroup()) {\n\t\t\tdeleteMenuIndex = isGroup;\n\t\t} else {\n\t\t\tdeleteMenuIndex = isNotGroup;\n\t\t}\n\n\t\tselectMenuItem(deleteMenuIndex);\n\t});\n}\n\nasync function openPreferences(): Promise<void> {\n\tawait withSettingsMenu(() => {\n\t\tselectMenuItem(1);\n\t});\n\n\tawait elementReady(selectors.preferencesSelector, {stopOnDomReady: false});\n}\n\nfunction isPreferencesOpen(): boolean {\n\treturn Boolean(document.querySelector<HTMLElement>(selectors.preferencesSelector));\n}\n\nasync function closePreferences(): Promise<void> {\n\t// Wait for the preferences window to be closed, then remove the class from the document\n\tconst preferencesOverlayObserver = new MutationObserver(records => {\n\t\tconst removedRecords = records.filter(({removedNodes}) => removedNodes.length > 0 && (removedNodes[0] as HTMLElement).tagName === 'DIV');\n\n\t\t// In case there is a div removed, hide utility class and stop observing\n\t\tif (removedRecords.length > 0) {\n\t\t\tdocument.documentElement.classList.remove('hide-preferences-window');\n\t\t\tpreferencesOverlayObserver.disconnect();\n\t\t}\n\t});\n\n\tconst preferencesOverlay = document.querySelector(selectors.preferencesSelector)!;\n\n\t// Get the parent of preferences, that's not getting deleted\n\tconst preferencesParent = preferencesOverlay.closest('div:not([class])')!;\n\n\tpreferencesOverlayObserver.observe(preferencesParent, {childList: true});\n\n\tconst closeButton = preferencesOverlay.querySelector(selectors.closePreferencesButton)!;\n\t(closeButton as HTMLElement)?.click();\n}\n\nfunction insertionListener(event: AnimationEvent): void {\n\tif (event.animationName === 'nodeInserted' && event.target) {\n\t\tevent.target.dispatchEvent(new Event('mouseover', {bubbles: true}));\n\t}\n}\n\nasync function observeAutoscroll(): Promise<void> {\n\tconst mainElement = await elementReady('._4sp8', {stopOnDomReady: false});\n\tif (!mainElement) {\n\t\treturn;\n\t}\n\n\tconst scrollToBottom = (): void => {\n\t\t// eslint-disable-next-line @typescript-eslint/ban-types\n\t\tconst scrollableElement: HTMLElement | null = document.querySelector('[role=presentation] .scrollable');\n\t\tif (scrollableElement) {\n\t\t\tscrollableElement.scroll({\n\t\t\t\ttop: Number.MAX_SAFE_INTEGER,\n\t\t\t\tbehavior: 'smooth',\n\t\t\t});\n\t\t}\n\t};\n\n\tconst hookMessageObserver = async (): Promise<void> => {\n\t\tconst chatElement = await elementReady(\n\t\t\t'[role=presentation] .scrollable [role = region] > div[id ^= \"js_\"]', {stopOnDomReady: false},\n\t\t);\n\n\t\tif (chatElement) {\n\t\t\t// Scroll to the bottom when opening different conversation\n\t\t\tscrollToBottom();\n\n\t\t\tconst messageObserver = new MutationObserver((record: MutationRecord[]) => {\n\t\t\t\tconst newMessages: MutationRecord[] = record.filter(record =>\n\t\t\t\t\t// The mutation is an addition\n\t\t\t\t\trecord.addedNodes.length > 0\n\t\t\t\t\t\t// ... of a div       (skip the \"seen\" status change)\n\t\t\t\t\t\t&& (record.addedNodes[0] as HTMLElement).tagName === 'DIV'\n\t\t\t\t\t\t// ... on the last child       (skip previous messages added when scrolling up)\n\t\t\t\t\t\t&& chatElement.lastChild!.contains(record.target),\n\t\t\t\t);\n\n\t\t\t\tif (newMessages.length > 0) {\n\t\t\t\t\t// Scroll to the bottom when there are new messages\n\t\t\t\t\tscrollToBottom();\n\t\t\t\t}\n\t\t\t});\n\n\t\t\tmessageObserver.observe(chatElement, {childList: true, subtree: true});\n\t\t}\n\t};\n\n\thookMessageObserver();\n\n\t// Hook it again if conversation changes\n\tconst conversationObserver = new MutationObserver(hookMessageObserver);\n\tconversationObserver.observe(mainElement, {childList: true});\n}\n\nasync function observeThemeBugs(): Promise<void> {\n\tconst rootObserver = new MutationObserver((record: MutationRecord[]) => {\n\t\tconst newNodes: MutationRecord[] = record\n\t\t\t.filter(record => record.addedNodes.length > 0 || record.removedNodes.length > 0);\n\n\t\tif (newNodes) {\n\t\t\tremoveThemeClasses(Boolean(nativeTheme.shouldUseDarkColors));\n\t\t}\n\t});\n\n\trootObserver.observe(document.documentElement, {childList: true, subtree: true});\n}\n\n// Listen for emoji element dom insertion\ndocument.addEventListener('animationstart', insertionListener, false);\n\n// Inject a global style node to maintain custom appearance after conversation change or startup\ndocument.addEventListener('DOMContentLoaded', async () => {\n\tconst style = document.createElement('style');\n\tstyle.id = 'zoomFactor';\n\tdocument.body.append(style);\n\n\t// Set the zoom factor if it was set before quitting\n\tconst zoomFactor = await ipc.callMain<undefined, number>('get-config-zoomFactor');\n\tsetZoom(zoomFactor);\n\n\t// Enable OS specific styles\n\tdocument.documentElement.classList.add(`os-${process.platform}`);\n\n\t// Restore sidebar view state to what is was set before quitting\n\tupdateSidebar();\n\n\t// Activate Dark Mode if it was set before quitting\n\tsetTheme();\n\t// Observe for dark mode changes\n\tobserveTheme();\n\n\t// Activate Private Mode if it was set before quitting\n\tsetPrivateMode();\n\n\t// Configure do not disturb\n\tif (is.macos) {\n\t\tawait updateDoNotDisturb();\n\t}\n\n\t// Prevent flash of white on startup when in dark mode\n\t// TODO: find a CSS-only solution\n\tif (!is.macos && nativeTheme.shouldUseDarkColors) {\n\t\tdocument.documentElement.style.backgroundColor = '#1e1e1e';\n\t}\n\n\t// Disable autoplay if set in settings\n\ttoggleVideoAutoplay();\n\n\t// Hook auto-scroll observer\n\tobserveAutoscroll();\n\n\t// Hook broken dark mode observer\n\tobserveThemeBugs();\n});\n\n// Handle title bar double-click.\nwindow.addEventListener('dblclick', (event: Event) => {\n\tconst target = event.target as HTMLElement;\n\tconst titleBar = target.closest('._36ic._5l-3,._5742,._6-xk,._673w');\n\n\tif (!titleBar) {\n\t\treturn;\n\t}\n\n\tipc.callMain('titlebar-doubleclick');\n}, {\n\tpassive: true,\n});\n\nwindow.addEventListener('load', async () => {\n\tif (location.pathname.startsWith('/login')) {\n\t\tconst keepMeSignedInCheckbox = document.querySelector<HTMLInputElement>('[id^=\"u_0_0\"]')!;\n\t\tconst keepMeSignedInConfig = await ipc.callMain<undefined, boolean>('get-config-keepMeSignedIn');\n\t\tkeepMeSignedInCheckbox.checked = keepMeSignedInConfig;\n\t\tkeepMeSignedInCheckbox.addEventListener('change', async () => {\n\t\t\tconst keepMeSignedIn = await ipc.callMain<undefined, boolean>('get-config-keepMeSignedIn');\n\t\t\tawait ipc.callMain('set-config-keepMeSignedIn', keepMeSignedIn);\n\t\t});\n\t}\n});\n\n// Toggles styles for inactive window\nwindow.addEventListener('blur', () => {\n\tdocument.documentElement.classList.add('is-window-inactive');\n});\nwindow.addEventListener('focus', () => {\n\tdocument.documentElement.classList.remove('is-window-inactive');\n});\n\n// It's not possible to add multiple accelerators\n// so this needs to be done the old-school way\ndocument.addEventListener('keydown', async event => {\n\t// The `!event.altKey` part is a workaround for https://github.com/electron/electron/issues/13895\n\tconst combineKey = is.macos ? event.metaKey : event.ctrlKey && !event.altKey;\n\n\tif (!combineKey) {\n\t\treturn;\n\t}\n\n\tif (event.key === ']') {\n\t\tawait nextConversation();\n\t}\n\n\tif (event.key === '[') {\n\t\tawait previousConversation();\n\t}\n\n\tconst number = Number.parseInt(event.code.slice(-1), 10);\n\n\tif (number >= 1 && number <= 9) {\n\t\tawait jumpToConversation(number);\n\t}\n});\n\n// Pass events sent via `window.postMessage` on to the main process\nwindow.addEventListener('message', async ({data: {type, data}}) => {\n\tif (type === 'notification') {\n\t\tshowNotification(data as NotificationEvent);\n\t}\n\n\tif (type === 'notification-reply') {\n\t\tawait sendReply(data.reply as string);\n\n\t\tif (data.previousConversation) {\n\t\t\tawait selectConversation(data.previousConversation as number);\n\t\t}\n\t}\n});\n\nfunction showNotification({id, title, body, icon, silent}: NotificationEvent): void {\n\tconst image = new Image();\n\timage.crossOrigin = 'anonymous';\n\timage.src = icon;\n\n\timage.addEventListener('load', () => {\n\t\tconst canvas = document.createElement('canvas');\n\t\tconst context = canvas.getContext('2d')!;\n\n\t\tcanvas.width = image.width;\n\t\tcanvas.height = image.height;\n\n\t\tcontext.drawImage(image, 0, 0, image.width, image.height);\n\n\t\tipc.callMain('notification', {\n\t\t\tid,\n\t\t\ttitle,\n\t\t\tbody,\n\t\t\ticon: canvas.toDataURL(),\n\t\t\tsilent,\n\t\t});\n\t});\n}\n\nasync function sendReply(message: string): Promise<void> {\n\tconst inputField = document.querySelector<HTMLElement>('[contenteditable=\"true\"]');\n\tif (!inputField) {\n\t\treturn;\n\t}\n\n\tconst previousMessage = inputField.textContent;\n\n\t// Send message\n\tinputField.focus();\n\tinsertMessageText(message, inputField);\n\n\tconst sendButton = await elementReady<HTMLElement>('._30yy._38lh', {stopOnDomReady: false});\n\tif (!sendButton) {\n\t\tconsole.error('Could not find send button');\n\t\treturn;\n\t}\n\n\tsendButton.click();\n\n\t// Restore (possible) previous message\n\tif (previousMessage) {\n\t\tinsertMessageText(previousMessage, inputField);\n\t}\n}\n\nfunction insertMessageText(text: string, inputField: HTMLElement): void {\n\t// Workaround: insert placeholder value to get execCommand working\n\tif (!inputField.textContent) {\n\t\tconst event = new InputEvent('textInput', {\n\t\t\tbubbles: true,\n\t\t\tcancelable: true,\n\t\t\tdata: '_',\n\t\t\tview: window,\n\t\t});\n\t\tinputField.dispatchEvent(event);\n\t}\n\n\tdocument.execCommand('selectAll', false, undefined);\n\tdocument.execCommand('insertText', false, text);\n}\n\nipc.answerMain('notification-callback', (data: unknown) => {\n\twindow.postMessage({type: 'notification-callback', data}, '*');\n});\n\nipc.answerMain('notification-reply-callback', async (data: any) => {\n\tconst previousConversation = selectedConversationIndex();\n\tdata.previousConversation = previousConversation;\n\twindow.postMessage({type: 'notification-reply-callback', data}, '*');\n});\n"
  },
  {
    "path": "source/config.ts",
    "content": "import Store from 'electron-store';\nimport {is} from 'electron-util';\nimport {EmojiStyle} from './emoji';\n\nexport type StoreType = {\n\ttheme: 'system' | 'light' | 'dark';\n\tprivateMode: boolean;\n\tshowPrivateModePrompt: boolean;\n\tvibrancy: 'none' | 'sidebar' | 'full';\n\tzoomFactor: number;\n\tlastWindowState: {\n\t\tx: number;\n\t\ty: number;\n\t\twidth: number;\n\t\theight: number;\n\t\tisMaximized: boolean;\n\t};\n\tmenuBarMode: boolean;\n\tshowDockIcon: boolean;\n\tshowTrayIcon: boolean;\n\talwaysOnTop: boolean;\n\tshowAlwaysOnTopPrompt: boolean;\n\tbounceDockOnMessage: boolean;\n\tshowUnreadBadge: boolean;\n\tshowMessageButtons: boolean;\n\tlaunchMinimized: boolean;\n\tflashWindowOnMessage: boolean;\n\tnotificationMessagePreview: boolean;\n\tblock: {\n\t\tchatSeen: boolean;\n\t\ttypingIndicator: boolean;\n\t\tdeliveryReceipt: boolean;\n\t};\n\temojiStyle: EmojiStyle;\n\tuseWorkChat: boolean;\n\tsidebar: 'default' | 'hidden' | 'narrow' | 'wide';\n\tautoHideMenuBar: boolean;\n\tautoUpdate: boolean;\n\tnotificationsMuted: boolean;\n\tcallRingtoneMuted: boolean;\n\thardwareAcceleration: boolean;\n\tquitOnWindowClose: boolean;\n\tkeepMeSignedIn: boolean;\n\tautoplayVideos: boolean;\n\tisSpellCheckerEnabled: boolean;\n\tspellCheckerLanguages: string[];\n};\n\nconst schema: Store.Schema<StoreType> = {\n\ttheme: {\n\t\ttype: 'string',\n\t\tenum: ['system', 'light', 'dark'],\n\t\tdefault: 'system',\n\t},\n\tprivateMode: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tshowPrivateModePrompt: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tvibrancy: {\n\t\ttype: 'string',\n\t\tenum: ['none', 'sidebar', 'full'],\n\t\t// TODO: Change the default to 'sidebar' when the vibrancy issue in Electron is fixed.\n\t\t// See https://github.com/electron/electron/issues/10420\n\t\tdefault: 'none',\n\t},\n\tzoomFactor: {\n\t\ttype: 'number',\n\t\tdefault: 1,\n\t},\n\tlastWindowState: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tx: {\n\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\ty: {\n\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\twidth: {\n\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\theight: {\n\t\t\t\ttype: 'number',\n\t\t\t},\n\t\t\tisMaximized: {\n\t\t\t\ttype: 'boolean',\n\t\t\t},\n\t\t},\n\t\tdefault: {\n\t\t\tx: undefined,\n\t\t\ty: undefined,\n\t\t\twidth: 800,\n\t\t\theight: 600,\n\t\t\tisMaximized: false,\n\t\t},\n\t},\n\tmenuBarMode: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tshowDockIcon: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tshowTrayIcon: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\talwaysOnTop: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tshowAlwaysOnTopPrompt: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tbounceDockOnMessage: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tshowUnreadBadge: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tshowMessageButtons: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tlaunchMinimized: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tflashWindowOnMessage: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tnotificationMessagePreview: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tblock: {\n\t\ttype: 'object',\n\t\tproperties: {\n\t\t\tchatSeen: {\n\t\t\t\ttype: 'boolean',\n\t\t\t},\n\t\t\ttypingIndicator: {\n\t\t\t\ttype: 'boolean',\n\t\t\t},\n\t\t\tdeliveryReceipt: {\n\t\t\t\ttype: 'boolean',\n\t\t\t},\n\t\t},\n\t\tdefault: {\n\t\t\tchatSeen: false,\n\t\t\ttypingIndicator: false,\n\t\t\tdeliveryReceipt: false,\n\t\t},\n\t},\n\temojiStyle: {\n\t\ttype: 'string',\n\t\tenum: ['native', 'facebook-3-0', 'messenger-1-0', 'facebook-2-2'],\n\t\tdefault: 'facebook-3-0',\n\t},\n\tuseWorkChat: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tsidebar: {\n\t\ttype: 'string',\n\t\tenum: ['default', 'hidden', 'narrow', 'wide'],\n\t\tdefault: 'default',\n\t},\n\tautoHideMenuBar: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tautoUpdate: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tnotificationsMuted: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tcallRingtoneMuted: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\thardwareAcceleration: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tquitOnWindowClose: {\n\t\ttype: 'boolean',\n\t\tdefault: false,\n\t},\n\tkeepMeSignedIn: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tautoplayVideos: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tisSpellCheckerEnabled: {\n\t\ttype: 'boolean',\n\t\tdefault: true,\n\t},\n\tspellCheckerLanguages: {\n\t\ttype: 'array',\n\t\titems: {\n\t\t\ttype: 'string',\n\t\t},\n\t\tdefault: [],\n\t},\n};\n\nfunction updateVibrancySetting(store: Store<StoreType>): void {\n\tconst vibrancy = store.get('vibrancy');\n\n\tif (!is.macos || !vibrancy) {\n\t\tstore.set('vibrancy', 'none');\n\t// @ts-expect-error\n\t} else if (vibrancy === true) {\n\t\tstore.set('vibrancy', 'full');\n\t// @ts-expect-error\n\t} else if (vibrancy === false) {\n\t\tstore.set('vibrancy', 'sidebar');\n\t}\n}\n\nfunction updateSidebarSetting(store: Store<StoreType>): void {\n\tif (store.get('sidebarHidden')) {\n\t\tstore.set('sidebar', 'hidden');\n\t\t// @ts-expect-error\n\t\tstore.delete('sidebarHidden');\n\t} else if (!store.has('sidebar')) {\n\t\tstore.set('sidebar', 'default');\n\t}\n}\n\nfunction updateThemeSetting(store: Store<StoreType>): void {\n\tconst darkMode = store.get('darkMode');\n\tconst followSystemAppearance = store.get('followSystemAppearance');\n\n\tif (is.macos && followSystemAppearance) {\n\t\tstore.set('theme', 'system');\n\t} else if (darkMode !== undefined) {\n\t\tstore.set('theme', darkMode ? 'dark' : 'light');\n\t} else if (!store.has('theme')) {\n\t\tstore.set('theme', 'system');\n\t}\n\n\tif (darkMode !== undefined) {\n\t\t// @ts-expect-error\n\t\tstore.delete('darkMode');\n\t}\n\n\tif (followSystemAppearance !== undefined) {\n\t\t// @ts-expect-error\n\t\tstore.delete('followSystemAppearance');\n\t}\n}\n\nfunction migrate(store: Store<StoreType>): void {\n\tupdateVibrancySetting(store);\n\tupdateSidebarSetting(store);\n\tupdateThemeSetting(store);\n}\n\nconst store = new Store<StoreType>({schema});\nmigrate(store);\n\nexport default store;\n"
  },
  {
    "path": "source/constants.ts",
    "content": "import * as path from 'node:path';\nimport {fixPathForAsarUnpack} from 'electron-util';\n\nexport const caprineIconPath = fixPathForAsarUnpack(path.join(__dirname, '..', 'static', 'Icon.png'));\n"
  },
  {
    "path": "source/conversation.d.ts",
    "content": "type Conversation = {\n\tlabel: string;\n\tselected: boolean;\n\tunread: boolean;\n\ticon: string;\n};\n"
  },
  {
    "path": "source/do-not-disturb.d.ts",
    "content": "declare module '@sindresorhus/do-not-disturb';\n"
  },
  {
    "path": "source/emoji.ts",
    "content": "import * as path from 'node:path';\nimport {\n\tnativeImage,\n\tNativeImage,\n\tMenuItemConstructorOptions,\n\tCallbackResponse,\n\tMenu,\n} from 'electron';\nimport {is} from 'electron-util';\nimport {memoize} from 'lodash';\nimport {showRestartDialog, getWindow, sendBackgroundAction} from './util';\nimport config from './config';\n\n// The list of emojis that aren't supported by older emoji (facebook-2-2, messenger-1-0)\n// Based on https://emojipedia.org/facebook/3.0/new/\nconst excludedEmoji = new Set([\n\t'f0000', // Facebook's thumbs-up icon as shown on the contacts list\n\t'1f3c3_200d_2640',\n\t'1f3c4_200d_2640',\n\t'1f3ca_200d_2640',\n\t'1f3f4_200d_2620',\n\t'1f468_1f3fb_200d_1f9b0',\n\t'1f468_1f3fb_200d_1f9b1',\n\t'1f468_1f3fb_200d_1f9b2',\n\t'1f468_1f3fb_200d_1f9b3',\n\t'1f468_1f3fc_200d_1f9b0',\n\t'1f468_1f3fc_200d_1f9b1',\n\t'1f468_1f3fc_200d_1f9b2',\n\t'1f468_1f3fc_200d_1f9b3',\n\t'1f468_1f3fd_200d_1f9b0',\n\t'1f468_1f3fd_200d_1f9b1',\n\t'1f468_1f3fd_200d_1f9b2',\n\t'1f468_1f3fd_200d_1f9b3',\n\t'1f468_1f3fe_200d_1f9b0',\n\t'1f468_1f3fe_200d_1f9b1',\n\t'1f468_1f3fe_200d_1f9b2',\n\t'1f468_1f3fe_200d_1f9b3',\n\t'1f468_1f3ff_200d_1f9b0',\n\t'1f468_1f3ff_200d_1f9b1',\n\t'1f468_1f3ff_200d_1f9b2',\n\t'1f468_1f3ff_200d_1f9b3',\n\t'1f468_200d_1f9b0',\n\t'1f468_200d_1f9b1',\n\t'1f468_200d_1f9b2',\n\t'1f468_200d_1f9b3',\n\t'1f468_200d_2764_200d_1f468',\n\t'1f468_200d_2764_200d_1f48b_200d_1f468',\n\t'1f469_1f3fb_200d_1f9b0',\n\t'1f469_1f3fb_200d_1f9b1',\n\t'1f469_1f3fb_200d_1f9b2',\n\t'1f469_1f3fb_200d_1f9b3',\n\t'1f469_1f3fc_200d_1f9b0',\n\t'1f469_1f3fc_200d_1f9b1',\n\t'1f469_1f3fc_200d_1f9b2',\n\t'1f469_1f3fc_200d_1f9b3',\n\t'1f469_1f3fd_200d_1f9b0',\n\t'1f469_1f3fd_200d_1f9b1',\n\t'1f469_1f3fd_200d_1f9b2',\n\t'1f469_1f3fd_200d_1f9b3',\n\t'1f469_1f3fe_200d_1f9b0',\n\t'1f469_1f3fe_200d_1f9b1',\n\t'1f469_1f3fe_200d_1f9b2',\n\t'1f469_1f3fe_200d_1f9b3',\n\t'1f469_1f3ff_200d_1f9b0',\n\t'1f469_1f3ff_200d_1f9b1',\n\t'1f469_1f3ff_200d_1f9b2',\n\t'1f469_1f3ff_200d_1f9b3',\n\t'1f469_200d_1f9b0',\n\t'1f469_200d_1f9b1',\n\t'1f469_200d_1f9b2',\n\t'1f469_200d_1f9b3',\n\t'1f469_200d_2764_200d_1f469',\n\t'1f469_200d_2764_200d_1f48b_200d_1f469',\n\t'1f46e_200d_2640',\n\t'1f46f_200d_2640',\n\t'1f471_200d_2640',\n\t'1f473_200d_2640',\n\t'1f477_200d_2640',\n\t'1f481_200d_2640',\n\t'1f482_200d_2640',\n\t'1f486_200d_2640',\n\t'1f487_200d_2640',\n\t'1f645_200d_2640',\n\t'1f646_200d_2640',\n\t'1f647_200d_2640',\n\t'1f64b_200d_2640',\n\t'1f64d_200d_2640',\n\t'1f64e_200d_2640',\n\t'1f6a3_200d_2640',\n\t'1f6b4_200d_2640',\n\t'1f6b5_200d_2640',\n\t'1f6b6_200d_2640',\n\t'1f6f9',\n\t'1f94d',\n\t'1f94e',\n\t'1f94f',\n\t'1f96c',\n\t'1f96d',\n\t'1f96e',\n\t'1f96f',\n\t'1f970',\n\t'1f973',\n\t'1f974',\n\t'1f975',\n\t'1f976',\n\t'1f97a',\n\t'1f97c',\n\t'1f97d',\n\t'1f97e',\n\t'1f97f',\n\t'1f998',\n\t'1f999',\n\t'1f99a',\n\t'1f99b',\n\t'1f99c',\n\t'1f99d',\n\t'1f99e',\n\t'1f99f',\n\t'1f9a0',\n\t'1f9a1',\n\t'1f9a2',\n\t'1f9b0',\n\t'1f9b1',\n\t'1f9b2',\n\t'1f9b3',\n\t'1f9b4',\n\t'1f9b5_1f3fb',\n\t'1f9b5_1f3fc',\n\t'1f9b5_1f3fd',\n\t'1f9b5_1f3fe',\n\t'1f9b5_1f3ff',\n\t'1f9b5',\n\t'1f9b6_1f3fb',\n\t'1f9b6_1f3fc',\n\t'1f9b6_1f3fd',\n\t'1f9b6_1f3fe',\n\t'1f9b6_1f3ff',\n\t'1f9b6',\n\t'1f9b7',\n\t'1f9b8_1f3fb',\n\t'1f9b8_1f3fb_200d_2640',\n\t'1f9b8_1f3fb_200d_2642',\n\t'1f9b8_1f3fc',\n\t'1f9b8_1f3fc_200d_2640',\n\t'1f9b8_1f3fc_200d_2642',\n\t'1f9b8_1f3fd',\n\t'1f9b8_1f3fd_200d_2640',\n\t'1f9b8_1f3fd_200d_2642',\n\t'1f9b8_1f3fe',\n\t'1f9b8_1f3fe_200d_2640',\n\t'1f9b8_1f3fe_200d_2642',\n\t'1f9b8_1f3ff',\n\t'1f9b8_1f3ff_200d_2640',\n\t'1f9b8_1f3ff_200d_2642',\n\t'1f9b8_200d_2640',\n\t'1f9b8_200d_2642',\n\t'1f9b8',\n\t'1f9b9_1f3fb',\n\t'1f9b9_1f3fb_200d_2640',\n\t'1f9b9_1f3fb_200d_2642',\n\t'1f9b9_1f3fc',\n\t'1f9b9_1f3fc_200d_2640',\n\t'1f9b9_1f3fc_200d_2642',\n\t'1f9b9_1f3fd',\n\t'1f9b9_1f3fd_200d_2640',\n\t'1f9b9_1f3fd_200d_2642',\n\t'1f9b9_1f3fe',\n\t'1f9b9_1f3fe_200d_2640',\n\t'1f9b9_1f3fe_200d_2642',\n\t'1f9b9_1f3ff',\n\t'1f9b9_1f3ff_200d_2640',\n\t'1f9b9_1f3ff_200d_2642',\n\t'1f9b9_200d_2640',\n\t'1f9b9_200d_2642',\n\t'1f9b9',\n\t'1f9c1',\n\t'1f9c2',\n\t'1f9e7',\n\t'1f9e8',\n\t'1f9e9',\n\t'1f9ea',\n\t'1f9eb',\n\t'1f9ec',\n\t'1f9ed',\n\t'1f9ee',\n\t'1f9ef',\n\t'1f9f0',\n\t'1f9f1',\n\t'1f9f2',\n\t'1f9f3',\n\t'1f9f4',\n\t'1f9f5',\n\t'1f9f6',\n\t'1f9f7',\n\t'1f9f8',\n\t'1f9f9',\n\t'1f9fa',\n\t'1f9fb',\n\t'1f9fc',\n\t'1f9fd',\n\t'1f9fe',\n\t'1f9ff',\n\t'265f',\n\t'267e',\n]);\n\nexport enum EmojiStyle {\n\tNative = 'native',\n\tFacebook30 = 'facebook-3-0',\n\tMessenger10 = 'messenger-1-0',\n\tFacebook22 = 'facebook-2-2',\n}\n\nenum EmojiStyleCode {\n\tFacebook30 = 't',\n\tMessenger10 = 'z',\n\tFacebook22 = 'f',\n}\n\nfunction codeForEmojiStyle(style: EmojiStyle): EmojiStyleCode {\n\tswitch (style) {\n\t\tcase 'facebook-2-2': {\n\t\t\treturn EmojiStyleCode.Facebook22;\n\t\t}\n\n\t\tcase 'messenger-1-0': {\n\t\t\treturn EmojiStyleCode.Messenger10;\n\t\t}\n\n\t\tdefault: {\n\t\t\treturn EmojiStyleCode.Facebook30;\n\t\t}\n\t}\n}\n\n/**\nRenders the given emoji in the renderer process and returns a PNG `data:` URL.\n*/\nconst renderEmoji = memoize(async (emoji: string): Promise<string> => sendBackgroundAction<string, string>('render-native-emoji', emoji));\n\n/**\n@param url - A Facebook emoji URL like `https://static.xx.fbcdn.net/images/emoji.php/v9/tae/2/16/1f471_1f3fb_200d_2640.png`.\n*/\nfunction urlToEmoji(url: string): string {\n\tconst codePoints = url\n\t\t.split('/')\n\t\t.pop()!\n\t\t.replace(/\\.png$/, '')\n\t\t.split('_')\n\t\t.map(hexCodePoint => Number.parseInt(hexCodePoint, 16));\n\n\t// F0000 (983040 decimal) is Facebook's thumbs-up icon\n\tif (codePoints.length === 1 && codePoints[0] === 983_040) {\n\t\treturn '👍';\n\t}\n\n\t// Emoji is missing Variation Selector-16 (\\uFE0F):\n\t// \"An invisible codepoint which specifies that the preceding character\n\t// should be displayed with emoji presentation.\n\t// Only required if the preceding character defaults to text presentation.\"\n\treturn String.fromCodePoint(...codePoints) + '\\uFE0F';\n}\n\nconst cachedEmojiMenuIcons = new Map<EmojiStyle, NativeImage>();\n\n/**\n@returns An icon to use for the menu item of this emoji style.\n*/\nasync function getEmojiIcon(style: EmojiStyle): Promise<NativeImage | undefined> {\n\tconst cachedIcon = cachedEmojiMenuIcons.get(style);\n\n\tif (cachedIcon) {\n\t\treturn cachedIcon;\n\t}\n\n\tif (style === 'native') {\n\t\tif (!getWindow()) {\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst dataUrl = await renderEmoji('🙂');\n\t\tconst image = nativeImage.createFromDataURL(dataUrl);\n\t\tconst resizedImage = image.resize({width: 16, height: 16});\n\n\t\tcachedEmojiMenuIcons.set(style, resizedImage);\n\n\t\treturn resizedImage;\n\t}\n\n\tconst image = nativeImage.createFromPath(\n\t\tpath.join(__dirname, '..', 'static', `emoji-${style}.png`),\n\t);\n\n\tcachedEmojiMenuIcons.set(style, image);\n\n\treturn image;\n}\n\n/**\nFor example, when 'emojiStyle' setting is set to 'messenger-1-0' it replaces\nthis URL:  https://static.xx.fbcdn.net/images/emoji.php/v9/t27/2/32/1f600.png\nwith this: https://static.xx.fbcdn.net/images/emoji.php/v9/z27/2/32/1f600.png\n                                                 (see here) ^\n*/\nexport async function process(url: string): Promise<CallbackResponse> {\n\tconst emojiStyle = config.get('emojiStyle');\n\tconst emojiSetCode = codeForEmojiStyle(emojiStyle);\n\n\t// The character code is the filename without the extension.\n\tconst characterCodeEnd = url.lastIndexOf('.png');\n\tconst characterCode = url.slice(url.lastIndexOf('/') + 1, characterCodeEnd);\n\n\tif (emojiStyle === EmojiStyle.Native) {\n\t\tconst emoji = urlToEmoji(url);\n\t\tconst dataUrl = await renderEmoji(emoji);\n\t\treturn {redirectURL: dataUrl};\n\t}\n\n\tif (\n\t\t// Don't replace emoji from Facebook's latest emoji set\n\t\temojiSetCode === 't'\n\t\t// Don't replace the same URL in a loop\n\t\t|| url.includes('#replaced')\n\t\t// Ignore non-png files\n\t\t|| characterCodeEnd === -1\n\t\t// Messenger 1.0 and Facebook 2.2 emoji sets support only emoji up to version 5.0.\n\t\t// Fall back to default style for emoji >= 10.0\n\t\t|| excludedEmoji.has(characterCode)\n\t) {\n\t\treturn {};\n\t}\n\n\tconst emojiSetPrefix = 'emoji.php/v9/';\n\tconst emojiSetIndex = url.indexOf(emojiSetPrefix) + emojiSetPrefix.length;\n\tconst newURL\n\t\t= url.slice(0, emojiSetIndex) + emojiSetCode + url.slice(emojiSetIndex + 1) + '#replaced';\n\n\treturn {redirectURL: newURL};\n}\n\nexport async function generateSubmenu(\n\tupdateMenu: () => Promise<Menu>,\n): Promise<MenuItemConstructorOptions[]> {\n\tconst emojiMenuOption = async (\n\t\tlabel: string,\n\t\tstyle: EmojiStyle,\n\t\tvisibility: boolean,\n\t): Promise<MenuItemConstructorOptions> => ({\n\t\tlabel,\n\t\ttype: 'checkbox',\n\t\tvisible: visibility,\n\t\ticon: await getEmojiIcon(style),\n\t\tchecked: config.get('emojiStyle') === style,\n\t\tasync click() {\n\t\t\tif (config.get('emojiStyle') === style) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconfig.set('emojiStyle', style);\n\n\t\t\tawait updateMenu();\n\t\t\tshowRestartDialog('Caprine needs to be restarted to apply emoji changes.');\n\t\t},\n\t});\n\n\treturn Promise.all([\n\t\temojiMenuOption('System', EmojiStyle.Native, true),\n\t\t{type: 'separator'} as const,\n\t\temojiMenuOption('Facebook 3.0', EmojiStyle.Facebook30, true),\n\t\temojiMenuOption('Messenger 1.0', EmojiStyle.Messenger10, !is.linux || is.development),\n\t\temojiMenuOption('Facebook 2.2', EmojiStyle.Facebook22, true),\n\t]);\n}\n"
  },
  {
    "path": "source/ensure-online.ts",
    "content": "import {app, dialog} from 'electron';\nimport isOnline from 'is-online';\nimport pWaitFor from 'p-wait-for';\n\nfunction showWaitDialog(): void {\n\tconst buttonIndex = dialog.showMessageBoxSync({\n\t\tmessage: 'You appear to be offline. Caprine requires a working internet connection.',\n\t\tdetail: 'Do you want to wait?',\n\t\tbuttons: [\n\t\t\t'Wait',\n\t\t\t'Quit',\n\t\t],\n\t\tdefaultId: 0,\n\t\tcancelId: 1,\n\t});\n\n\tif (buttonIndex === 1) {\n\t\tapp.quit();\n\t}\n}\n\nexport default async (): Promise<void> => {\n\tif (!(await isOnline())) {\n\t\tconst connectivityTimeout = setTimeout(showWaitDialog, 15_000);\n\n\t\tawait pWaitFor(isOnline, {interval: 1000});\n\t\tclearTimeout(connectivityTimeout);\n\t}\n};\n"
  },
  {
    "path": "source/index.ts",
    "content": "import path from 'node:path';\nimport {readFileSync, existsSync} from 'node:fs';\nimport {\n\tapp,\n\tnativeImage,\n\tscreen as electronScreen,\n\tsession,\n\tshell,\n\tBrowserWindow,\n\tMenu,\n\tNotification,\n\tMenuItemConstructorOptions,\n\tsystemPreferences,\n\tnativeTheme,\n} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport {autoUpdater} from 'electron-updater';\nimport electronDl from 'electron-dl';\nimport electronContextMenu from 'electron-context-menu';\nimport electronLocalshortcut from 'electron-localshortcut';\nimport electronDebug from 'electron-debug';\nimport {is, darkMode} from 'electron-util';\nimport {bestFacebookLocaleFor} from 'facebook-locales';\nimport doNotDisturb from '@sindresorhus/do-not-disturb';\nimport updateAppMenu from './menu';\nimport config, {StoreType} from './config';\nimport tray from './tray';\nimport {\n\tsendAction,\n\tsendBackgroundAction,\n\tmessengerDomain,\n\tstripTrackingFromUrl,\n} from './util';\nimport {process as processEmojiUrl} from './emoji';\nimport ensureOnline from './ensure-online';\nimport {setUpMenuBarMode} from './menu-bar-mode';\nimport {caprineIconPath} from './constants';\n\nipc.setMaxListeners(100);\n\nelectronDebug({\n\tisEnabled: true, // TODO: This is only enabled to allow `Command+R` because messenger.com sometimes gets stuck after computer waking up\n\tshowDevTools: false,\n});\n\nelectronDl();\nelectronContextMenu({\n\tshowCopyImageAddress: true,\n\tprepend(defaultActions) {\n\t\t/*\n\t\tTODO: Use menu option or use replacement of options (https://github.com/sindresorhus/electron-context-menu/issues/70)\n\t\tSee explanation for this hacky solution here: https://github.com/sindresorhus/caprine/pull/1169\n\t\t*/\n\t\tdefaultActions.copyLink({\n\t\t\ttransform: stripTrackingFromUrl,\n\t\t});\n\n\t\treturn [];\n\t},\n});\n\napp.setAppUserModelId('com.sindresorhus.caprine');\n\nif (!config.get('hardwareAcceleration')) {\n\tapp.disableHardwareAcceleration();\n}\n\nif (!is.development && config.get('autoUpdate')) {\n\t(async () => {\n\t\tconst FOUR_HOURS = 1000 * 60 * 60 * 4;\n\t\tsetInterval(async () => {\n\t\t\tawait autoUpdater.checkForUpdatesAndNotify();\n\t\t}, FOUR_HOURS);\n\n\t\tawait autoUpdater.checkForUpdatesAndNotify();\n\t})();\n}\n\nlet mainWindow: BrowserWindow;\nlet isQuitting = false;\nlet previousMessageCount = 0;\nlet dockMenu: Menu;\nlet isDNDEnabled = false;\n\nif (!app.requestSingleInstanceLock()) {\n\tapp.quit();\n}\n\napp.on('second-instance', () => {\n\tif (mainWindow) {\n\t\tif (mainWindow.isMinimized()) {\n\t\t\tmainWindow.restore();\n\t\t}\n\n\t\tmainWindow.show();\n\t}\n});\n\n// Preserves the window position when a display is removed and Caprine is moved to a different screen.\napp.on('ready', () => {\n\telectronScreen.on('display-removed', () => {\n\t\tconst [x, y] = mainWindow.getPosition();\n\t\tmainWindow.setPosition(x, y);\n\t});\n});\n\nasync function updateBadge(messageCount: number): Promise<void> {\n\tif (!is.windows) {\n\t\tif (config.get('showUnreadBadge') && !isDNDEnabled) {\n\t\t\tapp.badgeCount = messageCount;\n\t\t}\n\n\t\tif (\n\t\t\tis.macos\n\t\t\t&& !isDNDEnabled\n\t\t\t&& config.get('bounceDockOnMessage')\n\t\t\t&& previousMessageCount !== messageCount\n\t\t) {\n\t\t\tapp.dock.bounce('informational');\n\t\t\tpreviousMessageCount = messageCount;\n\t\t}\n\t}\n\n\tif (!is.macos) {\n\t\tif (config.get('showUnreadBadge')) {\n\t\t\ttray.setBadge(messageCount > 0);\n\t\t}\n\n\t\tif (config.get('flashWindowOnMessage')) {\n\t\t\tmainWindow.flashFrame(messageCount !== 0);\n\t\t}\n\t}\n\n\ttray.update(messageCount);\n\n\tif (is.windows) {\n\t\tif (!config.get('showUnreadBadge') || messageCount === 0) {\n\t\t\tmainWindow.setOverlayIcon(null, '');\n\t\t} else {\n\t\t\t// Delegate drawing of overlay icon to renderer process\n\t\t\tupdateOverlayIcon(await ipc.callRenderer(mainWindow, 'render-overlay-icon', messageCount));\n\t\t}\n\t}\n}\n\nfunction updateOverlayIcon({data, text}: {data: string; text: string}): void {\n\tconst img = nativeImage.createFromDataURL(data);\n\tmainWindow.setOverlayIcon(img, text);\n}\n\ntype BeforeSendHeadersResponse = {\n\tcancel?: boolean;\n\trequestHeaders?: Record<string, string>;\n};\n\ntype OnSendHeadersDetails = {\n\tid: number;\n\turl: string;\n\tmethod: string;\n\twebContentsId?: number;\n\tresourceType: string;\n\treferrer: string;\n\ttimestamp: number;\n\trequestHeaders: Record<string, string>;\n};\n\nfunction enableHiresResources(): void {\n\tconst scaleFactor = Math.max(\n\t\t...electronScreen.getAllDisplays().map(display => display.scaleFactor),\n\t);\n\n\tif (scaleFactor === 1) {\n\t\treturn;\n\t}\n\n\tconst filter = {urls: [`*://*.${messengerDomain}/`]};\n\n\tsession.defaultSession.webRequest.onBeforeSendHeaders(\n\t\tfilter,\n\t\t(details: OnSendHeadersDetails, callback: (response: BeforeSendHeadersResponse) => void) => {\n\t\t\tlet cookie = details.requestHeaders.Cookie;\n\n\t\t\tif (cookie && details.method === 'GET') {\n\t\t\t\tcookie = /(?:; )?dpr=\\d/.test(cookie) ? cookie.replace(/dpr=\\d/, `dpr=${scaleFactor}`) : `${cookie}; dpr=${scaleFactor}`;\n\n\t\t\t\t(details.requestHeaders as any).Cookie = cookie;\n\t\t\t}\n\n\t\t\tcallback({\n\t\t\t\tcancel: false,\n\t\t\t\trequestHeaders: details.requestHeaders,\n\t\t\t});\n\t\t},\n\t);\n}\n\nfunction initRequestsFiltering(): void {\n\tconst filter = {\n\t\turls: [\n\t\t\t`*://*.${messengerDomain}/*typ.php*`, // Type indicator blocker\n\t\t\t`*://*.${messengerDomain}/*change_read_status.php*`, // Seen indicator blocker\n\t\t\t`*://*.${messengerDomain}/*delivery_receipts*`, // Delivery receipts indicator blocker\n\t\t\t`*://*.${messengerDomain}/*unread_threads*`, // Delivery receipts indicator blocker\n\t\t\t'*://*.fbcdn.net/images/emoji.php/v9/*', // Emoji\n\t\t\t'*://*.facebook.com/images/emoji.php/v9/*', // Emoji\n\t\t],\n\t};\n\n\tsession.defaultSession.webRequest.onBeforeRequest(filter, async ({url}, callback) => {\n\t\tif (url.includes('emoji.php')) {\n\t\t\tcallback(await processEmojiUrl(url));\n\t\t} else if (url.includes('typ.php')) {\n\t\t\tcallback({cancel: config.get('block.typingIndicator' as any)});\n\t\t} else if (url.includes('change_read_status.php')) {\n\t\t\tcallback({cancel: config.get('block.chatSeen' as any)});\n\t\t} else if (url.includes('delivery_receipts') || url.includes('unread_threads')) {\n\t\t\tcallback({cancel: config.get('block.deliveryReceipt' as any)});\n\t\t}\n\t});\n\n\tsession.defaultSession.webRequest.onHeadersReceived({\n\t\turls: ['*://static.xx.fbcdn.net/rsrc.php/*'],\n\t}, ({responseHeaders}, callback) => {\n\t\tif (!config.get('callRingtoneMuted') || !responseHeaders) {\n\t\t\tcallback({});\n\t\t\treturn;\n\t\t}\n\n\t\tconst callRingtoneHash = '2NAu/QVqg211BbktgY5GkA==';\n\t\tcallback({\n\t\t\tcancel: responseHeaders['content-md5'][0] === callRingtoneHash,\n\t\t});\n\t});\n}\n\nfunction setUserLocale(): void {\n\tconst userLocale = bestFacebookLocaleFor(app.getLocale().replace('-', '_'));\n\tconst cookie = {\n\t\turl: 'https://www.messenger.com/',\n\t\tname: 'locale',\n\t\tsecure: true,\n\t\tvalue: userLocale,\n\t};\n\n\tsession.defaultSession.cookies.set(cookie);\n}\n\nfunction setNotificationsMute(status: boolean): void {\n\tconst label = 'Mute Notifications';\n\tconst muteMenuItem = Menu.getApplicationMenu()!.getMenuItemById('mute-notifications')!;\n\n\tconfig.set('notificationsMuted', status);\n\tmuteMenuItem.checked = status;\n\n\tif (is.macos) {\n\t\tconst item = dockMenu.items.find(x => x.label === label);\n\t\titem!.checked = status;\n\t}\n}\n\nfunction createMainWindow(): BrowserWindow {\n\tconst lastWindowState = config.get('lastWindowState');\n\n\t// Messenger or Work Chat\n\tconst mainURL = config.get('useWorkChat')\n\t\t? 'https://work.facebook.com/chat'\n\t\t: 'https://www.messenger.com/login/';\n\n\tconst win = new BrowserWindow({\n\t\ttitle: app.name,\n\t\tshow: false,\n\t\tx: lastWindowState.x,\n\t\ty: lastWindowState.y,\n\t\twidth: lastWindowState.width,\n\t\theight: lastWindowState.height,\n\t\ticon: is.linux ? caprineIconPath : undefined,\n\t\tminWidth: 400,\n\t\tminHeight: 200,\n\t\talwaysOnTop: config.get('alwaysOnTop'),\n\t\ttitleBarStyle: 'hiddenInset',\n\t\ttrafficLightPosition: {\n\t\t\tx: 80,\n\t\t\ty: 20,\n\t\t},\n\t\tautoHideMenuBar: config.get('autoHideMenuBar'),\n\t\twebPreferences: {\n\t\t\tpreload: path.join(__dirname, 'browser.js'),\n\t\t\tcontextIsolation: true,\n\t\t\tnodeIntegration: true,\n\t\t\tspellcheck: config.get('isSpellCheckerEnabled'),\n\t\t\tplugins: true,\n\t\t},\n\t});\n\n\trequire('@electron/remote/main').initialize();\n\trequire('@electron/remote/main').enable(win.webContents);\n\n\tsetUserLocale();\n\tinitRequestsFiltering();\n\n\tlet previousDarkMode = darkMode.isEnabled;\n\tdarkMode.onChange(() => {\n\t\tif (darkMode.isEnabled !== previousDarkMode) {\n\t\t\tpreviousDarkMode = darkMode.isEnabled;\n\t\t\twin.webContents.send('set-theme');\n\t\t}\n\t});\n\n\tif (is.macos) {\n\t\twin.setSheetOffset(40);\n\t}\n\n\twin.loadURL(mainURL);\n\n\twin.on('close', event => {\n\t\tif (config.get('quitOnWindowClose')) {\n\t\t\tapp.quit();\n\t\t\treturn;\n\t\t}\n\n\t\t// Workaround for https://github.com/electron/electron/issues/20263\n\t\t// Closing the app window when on full screen leaves a black screen\n\t\t// Exit fullscreen before closing\n\t\tif (is.macos && mainWindow.isFullScreen()) {\n\t\t\tmainWindow.once('leave-full-screen', () => {\n\t\t\t\tmainWindow.hide();\n\t\t\t});\n\t\t\tmainWindow.setFullScreen(false);\n\t\t}\n\n\t\tif (!isQuitting) {\n\t\t\tevent.preventDefault();\n\n\t\t\t// Workaround for https://github.com/electron/electron/issues/10023\n\t\t\twin.blur();\n\t\t\tif (is.macos) {\n\t\t\t\t// On macOS we're using `app.hide()` in order to focus the previous window correctly\n\t\t\t\tapp.hide();\n\t\t\t} else {\n\t\t\t\twin.hide();\n\t\t\t}\n\t\t}\n\t});\n\n\twin.on('focus', () => {\n\t\tif (config.get('flashWindowOnMessage')) {\n\t\t\t// This is a security in the case where messageCount is not reset by page title update\n\t\t\twin.flashFrame(false);\n\t\t}\n\t});\n\n\twin.on('resize', () => {\n\t\tconst {isMaximized} = config.get('lastWindowState');\n\t\tconfig.set('lastWindowState', {...win.getNormalBounds(), isMaximized});\n\t});\n\n\twin.on('maximize', () => {\n\t\tconfig.set('lastWindowState.isMaximized', true);\n\t});\n\n\twin.on('unmaximize', () => {\n\t\tconfig.set('lastWindowState.isMaximized', false);\n\t});\n\n\treturn win;\n}\n\n(async () => {\n\tawait Promise.all([ensureOnline(), app.whenReady()]);\n\tawait updateAppMenu();\n\tmainWindow = createMainWindow();\n\n\t// Workaround for https://github.com/electron/electron/issues/5256\n\telectronLocalshortcut.register(mainWindow, 'CommandOrControl+=', () => {\n\t\tsendAction('zoom-in');\n\t});\n\n\t// Start in menu bar mode if enabled, otherwise start normally\n\tsetUpMenuBarMode(mainWindow);\n\n\tif (is.macos) {\n\t\tconst firstItem: MenuItemConstructorOptions = {\n\t\t\tlabel: 'Mute Notifications',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('notificationsMuted'),\n\t\t\tasync click() {\n\t\t\t\tsetNotificationsMute(await ipc.callRenderer(mainWindow, 'toggle-mute-notifications'));\n\t\t\t},\n\t\t};\n\n\t\tdockMenu = Menu.buildFromTemplate([firstItem]);\n\t\tapp.dock.setMenu(dockMenu);\n\n\t\t// Dock icon is hidden initially on macOS\n\t\tif (config.get('showDockIcon')) {\n\t\t\tapp.dock.show();\n\t\t}\n\n\t\tipc.once('conversations', () => {\n\t\t\t// Messenger sorts the conversations by unread state.\n\t\t\t// We select the first conversation from the list.\n\t\t\tsendAction('jump-to-conversation', 1);\n\t\t});\n\n\t\tipc.answerRenderer('conversations', (conversations: Conversation[]) => {\n\t\t\tif (conversations.length === 0) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst items = conversations.map(({label, icon}, index) => ({\n\t\t\t\tlabel: `${label}`,\n\t\t\t\ticon: nativeImage.createFromDataURL(icon),\n\t\t\t\tclick() {\n\t\t\t\t\tmainWindow.show();\n\t\t\t\t\tsendAction('jump-to-conversation', index + 1);\n\t\t\t\t},\n\t\t\t}));\n\n\t\t\tapp.dock.setMenu(Menu.buildFromTemplate([firstItem, {type: 'separator'}, ...items]));\n\t\t});\n\t}\n\n\t// Update badge on conversations change\n\tipc.answerRenderer('update-tray-icon', async (messageCount: number) => {\n\t\tupdateBadge(messageCount);\n\t});\n\n\tenableHiresResources();\n\n\tconst {webContents} = mainWindow;\n\n\twebContents.on('dom-ready', async () => {\n\t\t// Set window title to Caprine\n\t\tmainWindow.setTitle(app.name);\n\n\t\tawait updateAppMenu();\n\n\t\tconst files = ['browser.css', 'dark-mode.css', 'vibrancy.css', 'code-blocks.css', 'autoplay.css', 'scrollbar.css'];\n\n\t\tconst cssPath = path.join(__dirname, '..', 'css');\n\n\t\tfor (const file of files) {\n\t\t\tif (existsSync(path.join(cssPath, file))) {\n\t\t\t\twebContents.insertCSS(readFileSync(path.join(cssPath, file), 'utf8'));\n\t\t\t}\n\t\t}\n\n\t\tif (config.get('useWorkChat') && existsSync(path.join(cssPath, 'workchat.css'))) {\n\t\t\twebContents.insertCSS(\n\t\t\t\treadFileSync(path.join(cssPath, 'workchat.css'), 'utf8'),\n\t\t\t);\n\t\t}\n\n\t\tif (existsSync(path.join(app.getPath('userData'), 'custom.css'))) {\n\t\t\twebContents.insertCSS(readFileSync(path.join(app.getPath('userData'), 'custom.css'), 'utf8'));\n\t\t}\n\n\t\tif (config.get('launchMinimized') || app.getLoginItemSettings().wasOpenedAsHidden) {\n\t\t\tmainWindow.hide();\n\t\t\ttray.create(mainWindow);\n\t\t} else {\n\t\t\tif (config.get('lastWindowState').isMaximized) {\n\t\t\t\tmainWindow.maximize();\n\t\t\t}\n\n\t\t\tmainWindow.show();\n\t\t}\n\n\t\tif (is.macos) {\n\t\t\t// TODO: 'update-dnd-mode' is not called\n\t\t\tipc.answerRenderer('update-dnd-mode', async (initialSoundsValue: boolean) => {\n\t\t\t\tdoNotDisturb.on('change', (doNotDisturb: boolean) => {\n\t\t\t\t\tisDNDEnabled = doNotDisturb;\n\t\t\t\t\tipc.callRenderer(mainWindow, 'toggle-sounds', {checked: isDNDEnabled ? false : initialSoundsValue});\n\t\t\t\t});\n\n\t\t\t\tisDNDEnabled = await doNotDisturb.isEnabled();\n\n\t\t\t\treturn isDNDEnabled ? false : initialSoundsValue;\n\t\t\t});\n\t\t}\n\n\t\t// TODO: Re-enable this when muting notifications is fixed\n\t\t// setNotificationsMute(await ipc.callRenderer(mainWindow, 'toggle-mute-notifications', {\n\t\t// \tdefaultStatus: config.get('notificationsMuted'),\n\t\t// }));\n\n\t\tipc.callRenderer(mainWindow, 'toggle-message-buttons', config.get('showMessageButtons'));\n\n\t\tawait webContents.executeJavaScript(\n\t\t\treadFileSync(path.join(__dirname, 'notifications-isolated.js'), 'utf8'),\n\t\t);\n\n\t\tif (is.macos) {\n\t\t\tawait import('./touch-bar');\n\t\t}\n\t});\n\n\twebContents.setWindowOpenHandler(details => {\n\t\tif (details.disposition === 'foreground-tab' || details.disposition === 'background-tab') {\n\t\t\tconst url = stripTrackingFromUrl(details.url);\n\t\t\tshell.openExternal(url);\n\t\t\treturn {action: 'deny'};\n\t\t}\n\n\t\tif (details.disposition === 'new-window') {\n\t\t\tif (details.url === 'about:blank' || details.url === 'about:blank#blocked') {\n\t\t\t\tif (details.frameName !== 'about:blank') {\n\t\t\t\t\t// Voice/video call popup\n\t\t\t\t\treturn {\n\t\t\t\t\t\taction: 'allow',\n\t\t\t\t\t\toverrideBrowserWindowOptions: {\n\t\t\t\t\t\t\tshow: true,\n\t\t\t\t\t\t\ttitleBarStyle: 'default',\n\t\t\t\t\t\t\twebPreferences: {\n\t\t\t\t\t\t\t\tnodeIntegration: false,\n\t\t\t\t\t\t\t\tpreload: path.join(__dirname, 'browser-call.js'),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst url = stripTrackingFromUrl(details.url);\n\t\t\t\tshell.openExternal(url);\n\t\t\t}\n\n\t\t\treturn {action: 'deny'};\n\t\t}\n\n\t\treturn {action: 'allow'};\n\t});\n\n\twebContents.on('will-navigate', async (event, url) => {\n\t\tconst isMessengerDotCom = (url: string): boolean => {\n\t\t\tconst {hostname} = new URL(url);\n\t\t\treturn hostname.endsWith('.messenger.com');\n\t\t};\n\n\t\tconst isTwoFactorAuth = (url: string): boolean => {\n\t\t\tconst twoFactorAuthURL = 'https://www.facebook.com/checkpoint';\n\t\t\treturn url.startsWith(twoFactorAuthURL);\n\t\t};\n\n\t\tconst isWorkChat = (url: string): boolean => {\n\t\t\tconst {hostname, pathname} = new URL(url);\n\n\t\t\tif (hostname === 'work.facebook.com' || hostname === 'work.workplace.com') {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t// Example: https://company-name.facebook.com/login or\n\t\t\t\t//   \t\thttps://company-name.workplace.com/login\n\t\t\t\t(hostname.endsWith('.facebook.com') || hostname.endsWith('.workplace.com'))\n\t\t\t\t&& (pathname.startsWith('/login') || pathname.startsWith('/chat'))\n\t\t\t) {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\tif (hostname === 'login.microsoftonline.com') {\n\t\t\t\treturn true;\n\t\t\t}\n\n\t\t\treturn false;\n\t\t};\n\n\t\tif (isMessengerDotCom(url) || isTwoFactorAuth(url) || isWorkChat(url)) {\n\t\t\treturn;\n\t\t}\n\n\t\tevent.preventDefault();\n\t\tawait shell.openExternal(url);\n\t});\n})();\n\nif (is.macos) {\n\tipc.answerRenderer('set-vibrancy', () => {\n\t\tmainWindow.setBackgroundColor('#80FFFFFF'); // Transparent, workaround for vibrancy issue.\n\t\tmainWindow.setVibrancy('sidebar');\n\t});\n}\n\nfunction toggleMaximized(): void {\n\tif (mainWindow.isMaximized()) {\n\t\tmainWindow.unmaximize();\n\t} else {\n\t\tmainWindow.maximize();\n\t}\n}\n\nipc.answerRenderer('titlebar-doubleclick', () => {\n\tif (is.macos) {\n\t\tconst doubleClickAction = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');\n\n\t\tif (doubleClickAction === 'Minimize') {\n\t\t\tmainWindow.minimize();\n\t\t} else if (doubleClickAction === 'Maximize') {\n\t\t\ttoggleMaximized();\n\t\t}\n\t} else {\n\t\ttoggleMaximized();\n\t}\n});\n\napp.on('activate', () => {\n\tif (mainWindow) {\n\t\tmainWindow.show();\n\t}\n});\n\napp.on('before-quit', () => {\n\tisQuitting = true;\n\n\t// Checking whether the window exists to work around an Electron race issue:\n\t// https://github.com/sindresorhus/caprine/issues/809\n\tif (mainWindow) {\n\t\tconst {isMaximized} = config.get('lastWindowState');\n\t\tconfig.set('lastWindowState', {...mainWindow.getNormalBounds(), isMaximized});\n\t}\n});\n\nconst notifications = new Map();\n\nipc.answerRenderer(\n\t'notification',\n\t({id, title, body, icon, silent}: {id: number; title: string; body: string; icon: string; silent: boolean}) => {\n\t\t// Don't send notifications when the window is focused\n\t\tif (mainWindow.isFocused()) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst notification = new Notification({\n\t\t\ttitle,\n\t\t\tbody: config.get('notificationMessagePreview') ? body : 'You have a new message',\n\t\t\thasReply: true,\n\t\t\ticon: nativeImage.createFromDataURL(icon),\n\t\t\tsilent,\n\t\t});\n\n\t\tnotifications.set(id, notification);\n\n\t\tnotification.on('click', () => {\n\t\t\tsendAction('notification-callback', {callbackName: 'onclick', id});\n\n\t\t\tnotifications.delete(id);\n\t\t});\n\n\t\tnotification.on('reply', (_event, reply: string) => {\n\t\t\t// We use onclick event used by messenger to go to the right convo\n\t\t\tsendBackgroundAction('notification-reply-callback', {callbackName: 'onclick', id, reply});\n\n\t\t\tnotifications.delete(id);\n\t\t});\n\n\t\tnotification.on('close', () => {\n\t\t\tsendBackgroundAction('notification-callback', {callbackName: 'onclose', id});\n\t\t\tnotifications.delete(id);\n\t\t});\n\n\t\tnotification.show();\n\t},\n);\n\ntype ThemeSource = typeof nativeTheme.themeSource;\n\nipc.answerRenderer<undefined, StoreType['useWorkChat']>('get-config-useWorkChat', async () => config.get('useWorkChat'));\nipc.answerRenderer<undefined, StoreType['showMessageButtons']>('get-config-showMessageButtons', async () => config.get('showMessageButtons'));\nipc.answerRenderer<undefined, ThemeSource>('get-config-theme', async () => config.get('theme'));\nipc.answerRenderer<undefined, StoreType['privateMode']>('get-config-privateMode', async () => config.get('privateMode'));\nipc.answerRenderer<undefined, StoreType['vibrancy']>('get-config-vibrancy', async () => config.get('vibrancy'));\nipc.answerRenderer<undefined, StoreType['sidebar']>('get-config-sidebar', async () => config.get('sidebar'));\nipc.answerRenderer<undefined, StoreType['zoomFactor']>('get-config-zoomFactor', async () => config.get('zoomFactor'));\nipc.answerRenderer<StoreType['zoomFactor'], void>('set-config-zoomFactor', async zoomFactor => {\n\tconfig.set('zoomFactor', zoomFactor);\n});\nipc.answerRenderer<undefined, StoreType['keepMeSignedIn']>('get-config-keepMeSignedIn', async () => config.get('keepMeSignedIn'));\nipc.answerRenderer<StoreType['keepMeSignedIn'], void>('set-config-keepMeSignedIn', async keepMeSignedIn => {\n\tconfig.set('keepMeSignedIn', keepMeSignedIn);\n});\nipc.answerRenderer<undefined, StoreType['autoplayVideos']>('get-config-autoplayVideos', async () => config.get('autoplayVideos'));\nipc.answerRenderer<undefined, StoreType['emojiStyle']>('get-config-emojiStyle', async () => config.get('emojiStyle'));\nipc.answerRenderer<StoreType['emojiStyle'], void>('set-config-emojiStyle', async emojiStyle => {\n\tconfig.set('emojiStyle', emojiStyle);\n});\n"
  },
  {
    "path": "source/menu-bar-mode.ts",
    "content": "import {\n\tapp,\n\tglobalShortcut,\n\tBrowserWindow,\n\tMenu,\n} from 'electron';\nimport {is} from 'electron-util';\nimport config from './config';\nimport tray from './tray';\n\nconst menuBarShortcut = 'Command+Shift+y';\n\nexport function toggleMenuBarMode(window: BrowserWindow): void {\n\tconst isEnabled = config.get('menuBarMode');\n\tconst menuItem = Menu.getApplicationMenu()!.getMenuItemById('menuBarMode')!;\n\n\tmenuItem.checked = isEnabled;\n\n\twindow.setVisibleOnAllWorkspaces(isEnabled);\n\n\tif (isEnabled) {\n\t\tglobalShortcut.register(menuBarShortcut, () => {\n\t\t\tif (window.isVisible()) {\n\t\t\t\twindow.hide();\n\t\t\t} else {\n\t\t\t\twindow.show();\n\t\t\t}\n\t\t});\n\n\t\ttray.create(window);\n\t} else {\n\t\tglobalShortcut.unregister(menuBarShortcut);\n\n\t\ttray.destroy();\n\t\tapp.dock.show();\n\t\twindow.show();\n\t}\n}\n\nexport function setUpMenuBarMode(window: BrowserWindow): void {\n\tif (is.macos) {\n\t\ttoggleMenuBarMode(window);\n\t} else if (config.get('showTrayIcon') && !config.get('quitOnWindowClose')) {\n\t\ttray.create(window);\n\t}\n}\n"
  },
  {
    "path": "source/menu.ts",
    "content": "import * as path from 'node:path';\nimport {existsSync, writeFileSync} from 'node:fs';\nimport {\n\tapp,\n\tshell,\n\tMenu,\n\tMenuItemConstructorOptions,\n\tdialog,\n} from 'electron';\nimport {\n\tis,\n\tappMenu,\n\topenUrlMenuItem,\n\taboutMenuItem,\n\topenNewGitHubIssue,\n\tdebugInfo,\n} from 'electron-util';\nimport config from './config';\nimport getSpellCheckerLanguages from './spell-checker';\nimport {\n\tsendAction,\n\tshowRestartDialog,\n\tgetWindow,\n\ttoggleTrayIcon,\n\ttoggleLaunchMinimized,\n} from './util';\nimport {generateSubmenu as generateEmojiSubmenu} from './emoji';\nimport {toggleMenuBarMode} from './menu-bar-mode';\nimport {caprineIconPath} from './constants';\n\nexport default async function updateMenu(): Promise<Menu> {\n\tconst newConversationItem: MenuItemConstructorOptions = {\n\t\tlabel: 'New Conversation',\n\t\taccelerator: 'CommandOrControl+N',\n\t\tclick() {\n\t\t\tsendAction('new-conversation');\n\t\t},\n\t};\n\n\tconst newRoomItem: MenuItemConstructorOptions = {\n\t\tlabel: 'New Room',\n\t\taccelerator: 'CommandOrControl+O',\n\t\tclick() {\n\t\t\tsendAction('new-room');\n\t\t},\n\t};\n\n\tconst switchItems: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Switch to Work Chat…',\n\t\t\taccelerator: 'CommandOrControl+Shift+2',\n\t\t\tvisible: !config.get('useWorkChat'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('useWorkChat', true);\n\t\t\t\tapp.relaunch();\n\t\t\t\tapp.quit();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Switch to Messenger…',\n\t\t\taccelerator: 'CommandOrControl+Shift+1',\n\t\t\tvisible: config.get('useWorkChat'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('useWorkChat', false);\n\t\t\t\tapp.relaunch();\n\t\t\t\tapp.quit();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Log Out',\n\t\t\tclick() {\n\t\t\t\tsendAction('log-out');\n\t\t\t},\n\t\t},\n\t];\n\n\tconst vibrancySubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'No Vibrancy',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('vibrancy') === 'none',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('vibrancy', 'none');\n\t\t\t\tsendAction('update-vibrancy');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Sidebar-only Vibrancy',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('vibrancy') === 'sidebar',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('vibrancy', 'sidebar');\n\t\t\t\tsendAction('update-vibrancy');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Full-window Vibrancy',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('vibrancy') === 'full',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('vibrancy', 'full');\n\t\t\t\tsendAction('update-vibrancy');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t];\n\n\tconst themeSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Follow System Appearance',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('theme') === 'system',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('theme', 'system');\n\t\t\t\tsendAction('set-theme');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Light Mode',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('theme') === 'light',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('theme', 'light');\n\t\t\t\tsendAction('set-theme');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Dark Mode',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('theme') === 'dark',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('theme', 'dark');\n\t\t\t\tsendAction('set-theme');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t];\n\n\tconst sidebarSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Adaptive Sidebar',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('sidebar') === 'default',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('sidebar', 'default');\n\t\t\t\tsendAction('update-sidebar');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Hide Sidebar',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('sidebar') === 'hidden',\n\t\t\taccelerator: 'CommandOrControl+Shift+S',\n\t\t\tasync click() {\n\t\t\t\t// Toggle between default and hidden\n\t\t\t\tconfig.set('sidebar', config.get('sidebar') === 'hidden' ? 'default' : 'hidden');\n\t\t\t\tsendAction('update-sidebar');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Narrow Sidebar',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('sidebar') === 'narrow',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('sidebar', 'narrow');\n\t\t\t\tsendAction('update-sidebar');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Wide Sidebar',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('sidebar') === 'wide',\n\t\t\tasync click() {\n\t\t\t\tconfig.set('sidebar', 'wide');\n\t\t\t\tsendAction('update-sidebar');\n\t\t\t\tawait updateMenu();\n\t\t\t},\n\t\t},\n\t];\n\n\tconst privacySubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Block Seen Indicator',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('block.chatSeen' as any),\n\t\t\tclick(menuItem) {\n\t\t\t\tconfig.set('block.chatSeen' as any, menuItem.checked);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Block Typing Indicator',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('block.typingIndicator' as any),\n\t\t\tclick(menuItem) {\n\t\t\t\tconfig.set('block.typingIndicator' as any, menuItem.checked);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Block Delivery Receipts',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('block.deliveryReceipt' as any),\n\t\t\tclick(menuItem) {\n\t\t\t\tconfig.set('block.deliveryReceipt' as any, menuItem.checked);\n\t\t\t},\n\t\t},\n\t];\n\n\tconst advancedSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Custom Styles',\n\t\t\tclick() {\n\t\t\t\tconst filePath = path.join(app.getPath('userData'), 'custom.css');\n\t\t\t\tconst defaultCustomStyle = `/*\nThis is the custom styles file where you can add anything you want.\nThe styles here will be injected into Caprine and will override default styles.\nIf you want to disable styles but keep the config, just comment the lines that you don't want to be used.\n\nPress Command/Ctrl+R in Caprine to see your changes.\n*/\n`;\n\n\t\t\t\tif (!existsSync(filePath)) {\n\t\t\t\t\twriteFileSync(filePath, defaultCustomStyle, 'utf8');\n\t\t\t\t}\n\n\t\t\t\tshell.openPath(filePath);\n\t\t\t},\n\t\t},\n\t];\n\n\tconst preferencesSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\t/* TODO: Fix privacy features */\n\t\t\t/* If you want to help, see #1688 */\n\t\t\tlabel: 'Privacy',\n\t\t\tvisible: is.development,\n\t\t\tsubmenu: privacySubmenu,\n\t\t},\n\t\t{\n\t\t\tlabel: 'Emoji Style',\n\t\t\tsubmenu: await generateEmojiSubmenu(updateMenu),\n\t\t},\n\t\t{\n\t\t\tlabel: 'Bounce Dock on Message',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.macos,\n\t\t\tchecked: config.get('bounceDockOnMessage'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('bounceDockOnMessage', !config.get('bounceDockOnMessage'));\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Fix ability to disable autoplay */\n\t\t\t/* GitHub issue: #1845 */\n\t\t\tlabel: 'Autoplay Videos',\n\t\t\tid: 'video-autoplay',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('autoplayVideos'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('autoplayVideos', !config.get('autoplayVideos'));\n\t\t\t\tsendAction('toggle-video-autoplay');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Fix notifications */\n\t\t\tlabel: 'Show Message Preview in Notifications',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('notificationMessagePreview'),\n\t\t\tclick(menuItem) {\n\t\t\t\tconfig.set('notificationMessagePreview', menuItem.checked);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Fix notifications */\n\t\t\tlabel: 'Mute Notifications',\n\t\t\tid: 'mute-notifications',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('notificationsMuted'),\n\t\t\tclick() {\n\t\t\t\tsendAction('toggle-mute-notifications');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Mute Call Ringtone',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('callRingtoneMuted'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('callRingtoneMuted', !config.get('callRingtoneMuted'));\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Fix notification badge */\n\t\t\tlabel: 'Show Unread Badge',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('showUnreadBadge'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('showUnreadBadge', !config.get('showUnreadBadge'));\n\t\t\t\tsendAction('reload');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Spell Checker',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('isSpellCheckerEnabled'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('isSpellCheckerEnabled', !config.get('isSpellCheckerEnabled'));\n\t\t\t\tshowRestartDialog('Caprine needs to be restarted to enable or disable the spell checker.');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Hardware Acceleration',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('hardwareAcceleration'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('hardwareAcceleration', !config.get('hardwareAcceleration'));\n\t\t\t\tshowRestartDialog('Caprine needs to be restarted to change hardware acceleration.');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Menu Bar Icon',\n\t\t\tid: 'menuBarMode',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.macos,\n\t\t\tchecked: config.get('menuBarMode'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('menuBarMode', !config.get('menuBarMode'));\n\t\t\t\ttoggleMenuBarMode(getWindow());\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Always on Top',\n\t\t\tid: 'always-on-top',\n\t\t\ttype: 'checkbox',\n\t\t\taccelerator: 'CommandOrControl+Shift+T',\n\t\t\tchecked: config.get('alwaysOnTop'),\n\t\t\tasync click(menuItem, focusedWindow, event) {\n\t\t\t\tif (!config.get('alwaysOnTop') && config.get('showAlwaysOnTopPrompt') && event.shiftKey) {\n\t\t\t\t\tconst result = await dialog.showMessageBox(focusedWindow!, {\n\t\t\t\t\t\tmessage: 'Are you sure you want the window to stay on top of other windows?',\n\t\t\t\t\t\tdetail: 'This was triggered by Command/Control+Shift+T.',\n\t\t\t\t\t\tbuttons: [\n\t\t\t\t\t\t\t'Display on Top',\n\t\t\t\t\t\t\t'Don\\'t Display on Top',\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdefaultId: 0,\n\t\t\t\t\t\tcancelId: 1,\n\t\t\t\t\t\tcheckboxLabel: 'Don\\'t ask me again',\n\t\t\t\t\t});\n\n\t\t\t\t\tconfig.set('showAlwaysOnTopPrompt', !result.checkboxChecked);\n\n\t\t\t\t\tif (result.response === 0) {\n\t\t\t\t\t\tconfig.set('alwaysOnTop', !config.get('alwaysOnTop'));\n\t\t\t\t\t\tfocusedWindow?.setAlwaysOnTop(menuItem.checked);\n\t\t\t\t\t} else if (result.response === 1) {\n\t\t\t\t\t\tmenuItem.checked = false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconfig.set('alwaysOnTop', !config.get('alwaysOnTop'));\n\t\t\t\t\tfocusedWindow?.setAlwaysOnTop(menuItem.checked);\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Add support for Linux */\n\t\t\tlabel: 'Launch at Login',\n\t\t\tvisible: !is.linux,\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: app.getLoginItemSettings().openAtLogin,\n\t\t\tclick(menuItem) {\n\t\t\t\tapp.setLoginItemSettings({\n\t\t\t\t\topenAtLogin: menuItem.checked,\n\t\t\t\t\topenAsHidden: menuItem.checked,\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Auto Hide Menu Bar',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: !is.macos,\n\t\t\tchecked: config.get('autoHideMenuBar'),\n\t\t\tclick(menuItem, focusedWindow) {\n\t\t\t\tconfig.set('autoHideMenuBar', menuItem.checked);\n\t\t\t\tfocusedWindow?.setAutoHideMenuBar(menuItem.checked);\n\t\t\t\tfocusedWindow?.setMenuBarVisibility(!menuItem.checked);\n\n\t\t\t\tif (menuItem.checked) {\n\t\t\t\t\tdialog.showMessageBox({\n\t\t\t\t\t\ttype: 'info',\n\t\t\t\t\t\tmessage: 'Press the Alt key to toggle the menu bar.',\n\t\t\t\t\t\tbuttons: ['OK'],\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Automatic Updates',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('autoUpdate'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('autoUpdate', !config.get('autoUpdate'));\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t/* TODO: Fix notifications */\n\t\t\tlabel: 'Flash Window on Message',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: is.development,\n\t\t\tchecked: config.get('flashWindowOnMessage'),\n\t\t\tclick(menuItem) {\n\t\t\t\tconfig.set('flashWindowOnMessage', menuItem.checked);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tid: 'showTrayIcon',\n\t\t\tlabel: 'Show Tray Icon',\n\t\t\ttype: 'checkbox',\n\t\t\tenabled: !is.macos && !config.get('launchMinimized'),\n\t\t\tchecked: config.get('showTrayIcon'),\n\t\t\tclick() {\n\t\t\t\ttoggleTrayIcon();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Launch Minimized',\n\t\t\ttype: 'checkbox',\n\t\t\tvisible: !is.macos,\n\t\t\tchecked: config.get('launchMinimized'),\n\t\t\tclick() {\n\t\t\t\ttoggleLaunchMinimized(menu);\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Quit on Window Close',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('quitOnWindowClose'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('quitOnWindowClose', !config.get('quitOnWindowClose'));\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Advanced',\n\t\t\tsubmenu: advancedSubmenu,\n\t\t},\n\t];\n\n\tconst viewSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Reset Text Size',\n\t\t\taccelerator: 'CommandOrControl+0',\n\t\t\tclick() {\n\t\t\t\tsendAction('zoom-reset');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Increase Text Size',\n\t\t\taccelerator: 'CommandOrControl+Plus',\n\t\t\tclick() {\n\t\t\t\tsendAction('zoom-in');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Decrease Text Size',\n\t\t\taccelerator: 'CommandOrControl+-',\n\t\t\tclick() {\n\t\t\t\tsendAction('zoom-out');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Theme',\n\t\t\tsubmenu: themeSubmenu,\n\t\t},\n\t\t{\n\t\t\tlabel: 'Vibrancy',\n\t\t\tvisible: is.macos,\n\t\t\tsubmenu: vibrancySubmenu,\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Hide Names and Avatars',\n\t\t\tid: 'privateMode',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('privateMode'),\n\t\t\taccelerator: 'CommandOrControl+Shift+N',\n\t\t\tasync click(menuItem, _browserWindow, event) {\n\t\t\t\tif (!config.get('privateMode') && config.get('showPrivateModePrompt') && event.shiftKey) {\n\t\t\t\t\tconst result = await dialog.showMessageBox(_browserWindow!, {\n\t\t\t\t\t\tmessage: 'Are you sure you want to hide names and avatars?',\n\t\t\t\t\t\tdetail: 'This was triggered by Command/Control+Shift+N.',\n\t\t\t\t\t\tbuttons: [\n\t\t\t\t\t\t\t'Hide',\n\t\t\t\t\t\t\t'Don\\'t Hide',\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdefaultId: 0,\n\t\t\t\t\t\tcancelId: 1,\n\t\t\t\t\t\tcheckboxLabel: 'Don\\'t ask me again',\n\t\t\t\t\t});\n\n\t\t\t\t\tconfig.set('showPrivateModePrompt', !result.checkboxChecked);\n\n\t\t\t\t\tif (result.response === 0) {\n\t\t\t\t\t\tconfig.set('privateMode', !config.get('privateMode'));\n\t\t\t\t\t\tsendAction('set-private-mode');\n\t\t\t\t\t} else if (result.response === 1) {\n\t\t\t\t\t\tmenuItem.checked = false;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tconfig.set('privateMode', !config.get('privateMode'));\n\t\t\t\t\tsendAction('set-private-mode');\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Sidebar',\n\t\t\tsubmenu: sidebarSubmenu,\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Message Buttons',\n\t\t\ttype: 'checkbox',\n\t\t\tchecked: config.get('showMessageButtons'),\n\t\t\tclick() {\n\t\t\t\tconfig.set('showMessageButtons', !config.get('showMessageButtons'));\n\t\t\t\tsendAction('toggle-message-buttons');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Main Chats',\n\t\t\tclick() {\n\t\t\t\tsendAction('show-chats-view');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Marketplace Chats',\n\t\t\tclick() {\n\t\t\t\tsendAction('show-marketplace-view');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Message Requests',\n\t\t\tclick() {\n\t\t\t\tsendAction('show-requests-view');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show Archived Chats',\n\t\t\tclick() {\n\t\t\t\tsendAction('show-archive-view');\n\t\t\t},\n\t\t},\n\t];\n\n\tconst spellCheckerSubmenu: MenuItemConstructorOptions[] = getSpellCheckerLanguages();\n\n\tconst conversationSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Mute Conversation',\n\t\t\taccelerator: 'CommandOrControl+Shift+M',\n\t\t\tclick() {\n\t\t\t\tsendAction('mute-conversation');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Archive Conversation',\n\t\t\taccelerator: 'CommandOrControl+Shift+H',\n\t\t\tclick() {\n\t\t\t\tsendAction('archive-conversation');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Delete Conversation',\n\t\t\taccelerator: 'CommandOrControl+Shift+D',\n\t\t\tclick() {\n\t\t\t\tsendAction('delete-conversation');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Select Next Conversation',\n\t\t\taccelerator: 'Control+Tab',\n\t\t\tclick() {\n\t\t\t\tsendAction('next-conversation');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Select Previous Conversation',\n\t\t\taccelerator: 'Control+Shift+Tab',\n\t\t\tclick() {\n\t\t\t\tsendAction('previous-conversation');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Find Conversation',\n\t\t\taccelerator: 'CommandOrControl+K',\n\t\t\tclick() {\n\t\t\t\tsendAction('find');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Search in Conversation',\n\t\t\taccelerator: 'CommandOrControl+F',\n\t\t\tclick() {\n\t\t\t\tsendAction('search');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Insert GIF',\n\t\t\taccelerator: 'CommandOrControl+G',\n\t\t\tclick() {\n\t\t\t\tsendAction('insert-gif');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Insert Sticker',\n\t\t\taccelerator: 'CommandOrControl+S',\n\t\t\tclick() {\n\t\t\t\tsendAction('insert-sticker');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Insert Emoji',\n\t\t\taccelerator: 'CommandOrControl+E',\n\t\t\tclick() {\n\t\t\t\tsendAction('insert-emoji');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Attach Files',\n\t\t\taccelerator: 'CommandOrControl+T',\n\t\t\tclick() {\n\t\t\t\tsendAction('attach-files');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Focus Text Input',\n\t\t\taccelerator: 'CommandOrControl+I',\n\t\t\tclick() {\n\t\t\t\tsendAction('focus-text-input');\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Spell Checker Language',\n\t\t\tvisible: !is.macos && config.get('isSpellCheckerEnabled'),\n\t\t\tsubmenu: spellCheckerSubmenu,\n\t\t},\n\t];\n\n\tconst helpSubmenu: MenuItemConstructorOptions[] = [\n\t\topenUrlMenuItem({\n\t\t\tlabel: 'Website',\n\t\t\turl: 'https://github.com/sindresorhus/caprine',\n\t\t}),\n\t\topenUrlMenuItem({\n\t\t\tlabel: 'Source Code',\n\t\t\turl: 'https://github.com/sindresorhus/caprine',\n\t\t}),\n\t\topenUrlMenuItem({\n\t\t\tlabel: 'Donate…',\n\t\t\turl: 'https://github.com/sindresorhus/caprine?sponsor=1',\n\t\t}),\n\t\t{\n\t\t\tlabel: 'Report an Issue…',\n\t\t\tclick() {\n\t\t\t\tconst body = `\n<!-- Please succinctly describe your issue and steps to reproduce it. -->\n\n\n---\n\n${debugInfo()}`;\n\n\t\t\t\topenNewGitHubIssue({\n\t\t\t\t\tuser: 'sindresorhus',\n\t\t\t\t\trepo: 'caprine',\n\t\t\t\t\tbody,\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t];\n\n\tif (!is.macos) {\n\t\thelpSubmenu.push(\n\t\t\t{\n\t\t\t\ttype: 'separator',\n\t\t\t},\n\t\t\taboutMenuItem({\n\t\t\t\ticon: caprineIconPath,\n\t\t\t\tcopyright: 'Created by Sindre Sorhus',\n\t\t\t\ttext: 'Maintainers:\\nDušan Simić\\nLefteris Garyfalakis\\nMichael Quevillon\\nNikolas Spiridakis',\n\t\t\t\twebsite: 'https://github.com/sindresorhus/caprine',\n\t\t\t}),\n\t\t);\n\t}\n\n\tconst debugSubmenu: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\tlabel: 'Show Settings',\n\t\t\tclick() {\n\t\t\t\tconfig.openInEditor();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Show App Data',\n\t\t\tclick() {\n\t\t\t\tshell.openPath(app.getPath('userData'));\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\ttype: 'separator',\n\t\t},\n\t\t{\n\t\t\tlabel: 'Delete Settings',\n\t\t\tclick() {\n\t\t\t\tconfig.clear();\n\t\t\t\tapp.relaunch();\n\t\t\t\tapp.quit();\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: 'Delete App Data',\n\t\t\tclick() {\n\t\t\t\tshell.trashItem(app.getPath('userData'));\n\t\t\t\tapp.relaunch();\n\t\t\t\tapp.quit();\n\t\t\t},\n\t\t},\n\t];\n\n\tconst macosTemplate: MenuItemConstructorOptions[] = [\n\t\tappMenu([\n\t\t\t{\n\t\t\t\tlabel: 'Caprine Preferences',\n\t\t\t\tsubmenu: preferencesSubmenu,\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Messenger Preferences…',\n\t\t\t\taccelerator: 'Command+,',\n\t\t\t\tclick() {\n\t\t\t\t\tsendAction('show-preferences');\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\ttype: 'separator',\n\t\t\t},\n\t\t\t...switchItems,\n\t\t\t{\n\t\t\t\ttype: 'separator',\n\t\t\t},\n\t\t\t{\n\t\t\t\tlabel: 'Relaunch Caprine',\n\t\t\t\tclick() {\n\t\t\t\t\tapp.relaunch();\n\t\t\t\t\tapp.quit();\n\t\t\t\t},\n\t\t\t},\n\t\t]),\n\t\t{\n\t\t\trole: 'fileMenu',\n\t\t\tsubmenu: [\n\t\t\t\tnewConversationItem,\n\t\t\t\tnewRoomItem,\n\t\t\t\t{\n\t\t\t\t\ttype: 'separator',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\trole: 'close',\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\trole: 'editMenu',\n\t\t},\n\t\t{\n\t\t\trole: 'viewMenu',\n\t\t\tsubmenu: viewSubmenu,\n\t\t},\n\t\t{\n\t\t\tlabel: 'Conversation',\n\t\t\tsubmenu: conversationSubmenu,\n\t\t},\n\t\t{\n\t\t\trole: 'windowMenu',\n\t\t},\n\t\t{\n\t\t\trole: 'help',\n\t\t\tsubmenu: helpSubmenu,\n\t\t},\n\t];\n\n\tconst linuxWindowsTemplate: MenuItemConstructorOptions[] = [\n\t\t{\n\t\t\trole: 'fileMenu',\n\t\t\tsubmenu: [\n\t\t\t\tnewConversationItem,\n\t\t\t\tnewRoomItem,\n\t\t\t\t{\n\t\t\t\t\ttype: 'separator',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Caprine Settings',\n\t\t\t\t\tsubmenu: preferencesSubmenu,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Messenger Settings',\n\t\t\t\t\taccelerator: 'Control+,',\n\t\t\t\t\tclick() {\n\t\t\t\t\t\tsendAction('show-preferences');\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'separator',\n\t\t\t\t},\n\t\t\t\t...switchItems,\n\t\t\t\t{\n\t\t\t\t\ttype: 'separator',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Relaunch Caprine',\n\t\t\t\t\tclick() {\n\t\t\t\t\t\tapp.relaunch();\n\t\t\t\t\t\tapp.quit();\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\trole: 'quit',\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\trole: 'editMenu',\n\t\t},\n\t\t{\n\t\t\trole: 'viewMenu',\n\t\t\tsubmenu: viewSubmenu,\n\t\t},\n\t\t{\n\t\t\tlabel: 'Conversation',\n\t\t\tsubmenu: conversationSubmenu,\n\t\t},\n\t\t{\n\t\t\trole: 'help',\n\t\t\tsubmenu: helpSubmenu,\n\t\t},\n\t];\n\n\tconst template = is.macos ? macosTemplate : linuxWindowsTemplate;\n\n\tif (is.development) {\n\t\ttemplate.push({\n\t\t\tlabel: 'Debug',\n\t\t\tsubmenu: debugSubmenu,\n\t\t});\n\t}\n\n\tconst menu = Menu.buildFromTemplate(template);\n\tMenu.setApplicationMenu(menu);\n\n\treturn menu;\n}\n"
  },
  {
    "path": "source/notification-event.d.ts",
    "content": "type NotificationEvent = {\n\tid: number;\n\ttitle: string;\n\tbody: string;\n\ticon: string;\n\tsilent: boolean;\n};\n"
  },
  {
    "path": "source/notifications-isolated.ts",
    "content": "((window, notification) => {\n\tconst notifications = new Map<number, Notification>();\n\n\t// Handle events sent from the browser process\n\twindow.addEventListener('message', ({data: {type, data}}) => {\n\t\tif (type === 'notification-callback') {\n\t\t\tconst {callbackName, id}: NotificationCallback = data;\n\t\t\tconst notification = notifications.get(id);\n\n\t\t\tif (!notification) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (notification[callbackName]) {\n\t\t\t\tnotification[callbackName]();\n\t\t\t}\n\n\t\t\tif (callbackName === 'onclose') {\n\t\t\t\tnotifications.delete(id);\n\t\t\t}\n\t\t}\n\n\t\tif (type === 'notification-reply-callback') {\n\t\t\tconst {callbackName, id, previousConversation, reply}: NotificationReplyCallback = data;\n\t\t\tconst notification = notifications.get(id);\n\n\t\t\tif (!notification) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tif (notification[callbackName]) {\n\t\t\t\tnotification[callbackName]();\n\t\t\t}\n\n\t\t\tnotifications.delete(id);\n\t\t\twindow.postMessage({type: 'notification-reply', data: {previousConversation, reply}}, '*');\n\t\t}\n\t});\n\n\tlet counter = 1;\n\n\tconst augmentedNotification = Object.assign(\n\t\tclass {\n\t\t\tprivate readonly _id: number;\n\n\t\t\tconstructor(title: string, options: NotificationOptions) {\n\t\t\t\t// According to https://github.com/sindresorhus/caprine/pull/637, the Notification\n\t\t\t\t// constructor can be called with non-string title and body.\n\t\t\t\tlet {body} = options;\n\t\t\t\tconst bodyProperties = (body as any).props;\n\t\t\t\tbody = bodyProperties ? bodyProperties.content[0] : options.body;\n\n\t\t\t\tconst titleProperties = (title as any).props;\n\t\t\t\ttitle = titleProperties ? titleProperties.content[0] : title;\n\n\t\t\t\tthis._id = counter++;\n\n\t\t\t\tnotifications.set(this._id, this as any);\n\n\t\t\t\twindow.postMessage(\n\t\t\t\t\t{\n\t\t\t\t\t\ttype: 'notification',\n\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\ttitle,\n\t\t\t\t\t\t\tid: this._id,\n\t\t\t\t\t\t\t...options,\n\t\t\t\t\t\t\tbody,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t'*',\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// No-op, but Messenger expects this method to be present\n\t\t\tclose(): void {} // eslint-disable-line @typescript-eslint/no-empty-function\n\t\t},\n\t\tnotification,\n\t);\n\n\tObject.assign(window, {notification: augmentedNotification});\n})(window, Notification);\n"
  },
  {
    "path": "source/notifications.d.ts",
    "content": "type NotificationCallback = {\n\tcallbackName: keyof Notification;\n\tid: number;\n};\n\ntype NotificationReplyCallback = NotificationCallback & {\n\tpreviousConversation: number;\n\treply: string;\n};\n"
  },
  {
    "path": "source/spell-checker.ts",
    "content": "import {session, MenuItemConstructorOptions} from 'electron';\nimport config from './config';\n\nconst languageToCode = new Map<string, string>([\n\t// All languages available in Electron's spellchecker\n\t['af', 'Afrikaans'],\n\t['bg', 'Bulgarian'],\n\t['ca', 'Catalan'],\n\t['cs', 'Czech'],\n\t['cy', 'Welsh'],\n\t['da', 'Danish '],\n\t['de', 'German'],\n\t['el', 'Greek'],\n\t['en', 'English'],\n\t['en-AU', 'English (Australia)'],\n\t['en-CA', 'English (Canada)'],\n\t['en-GB', 'English (United Kingdom)'],\n\t['en-US', 'English (United States)'],\n\t['es', 'Spanish'],\n\t['es-ES', 'Spanish'],\n\t['es-419', 'Spanish (Central and South America)'],\n\t['es-AR', 'Spanish (Argentina)'],\n\t['es-MX', 'Spanish (Mexico)'],\n\t['es-US', 'Spanish (United States)'],\n\t['et', 'Estonian'],\n\t['fa', 'Persian'],\n\t['fo', 'Faroese'],\n\t['fr', 'French'],\n\t['he', 'Hebrew'],\n\t['hi', 'Hindi'],\n\t['hr', 'Croatian'],\n\t['hu', 'Hungarian'],\n\t['hy', 'Armenian'],\n\t['id', 'Indonesian'],\n\t['it', 'Italian'],\n\t['ko', 'Korean'],\n\t['lt', 'Lithuanian'],\n\t['lv', 'Latvian'],\n\t['nb', 'Norwegian'],\n\t['nl', 'Dutch'],\n\t['pl', 'Polish'],\n\t['pt', 'Portuguese'],\n\t['pt-BR', 'Portuguese (Brazil)'],\n\t['pt-PT', 'Portuguese'],\n\t['ro', 'Moldovan'],\n\t['ru', 'Russian'],\n\t['sh', 'Serbo-Croatian'],\n\t['sk', 'Slovak'],\n\t['sl', 'Slovenian'],\n\t['sq', 'Albanian'],\n\t['sr', 'Serbian'],\n\t['sv', 'Swedish'],\n\t['ta', 'Tamil'],\n\t['tg', 'Tajik'],\n\t['tr', 'Turkish'],\n\t['uk', 'Ukrainian'],\n\t['vi', 'Vietnamese'],\n]);\n\nfunction getSpellCheckerLanguages(): MenuItemConstructorOptions[] {\n\tconst availableLanguages = session.defaultSession.availableSpellCheckerLanguages;\n\tconst languageItem: MenuItemConstructorOptions[] = [];\n\tlet languagesChecked = config.get('spellCheckerLanguages');\n\n\tfor (const language of languagesChecked) {\n\t\tif (!availableLanguages.includes(language)) {\n\t\t\t// Remove it since it's not in the spell checker dictionary.\n\t\t\tlanguagesChecked = languagesChecked.filter(currentLang => currentLang !== language);\n\t\t\tconfig.set('spellCheckerLanguages', languagesChecked);\n\t\t}\n\t}\n\n\tfor (const language of availableLanguages) {\n\t\tlanguageItem.push(\n\t\t\t{\n\t\t\t\tlabel: languageToCode.get(language) ?? languageToCode.get(language.split('-')[0]) ?? language,\n\t\t\t\ttype: 'checkbox',\n\t\t\t\tchecked: languagesChecked.includes(language),\n\t\t\t\tclick() {\n\t\t\t\t\tconst index = languagesChecked.indexOf(language);\n\t\t\t\t\tif (index > -1) {\n\t\t\t\t\t\t// Remove language\n\t\t\t\t\t\tlanguagesChecked.splice(index, 1);\n\t\t\t\t\t\tconfig.set('spellCheckerLanguages', languagesChecked);\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Add language\n\t\t\t\t\t\tlanguagesChecked = [...languagesChecked, language];\n\t\t\t\t\t\tconfig.set('spellCheckerLanguages', languagesChecked);\n\t\t\t\t\t}\n\n\t\t\t\t\tsession.defaultSession.setSpellCheckerLanguages(languagesChecked);\n\t\t\t\t},\n\t\t\t},\n\t\t);\n\t}\n\n\tif (languageItem.length === 1) {\n\t\treturn [\n\t\t\t{\n\t\t\t\tlabel: 'System Default',\n\t\t\t\ttype: 'checkbox',\n\t\t\t\tchecked: true,\n\t\t\t\tenabled: false,\n\t\t\t},\n\t\t];\n\t}\n\n\treturn languageItem;\n}\n\nexport default getSpellCheckerLanguages;\n"
  },
  {
    "path": "source/touch-bar.ts",
    "content": "import {TouchBar, nativeImage} from 'electron';\nimport {ipcMain as ipc} from 'electron-better-ipc';\nimport config from './config';\nimport {sendAction, getWindow} from './util';\nimport {caprineIconPath} from './constants';\n\nconst {TouchBarButton} = TouchBar;\nconst MAX_VISIBLE_LENGTH = 25;\nconst privateModeTouchBarLabel: Electron.TouchBarButton = new TouchBarButton({\n\tlabel: 'Private mode enabled',\n\ticon: nativeImage.createFromPath(caprineIconPath),\n\ticonPosition: 'left',\n});\n\nfunction setTouchBar(items: Electron.TouchBarButton[]): void {\n\tconst touchBar = new TouchBar({items});\n\tconst win = getWindow();\n\twin.setTouchBar(touchBar);\n}\n\nfunction createLabel(label: string): string {\n\tif (label.length > MAX_VISIBLE_LENGTH) {\n\t\t// If the label is too long, we'll render a truncated one with \"…\" appended\n\t\treturn `${label.slice(0, MAX_VISIBLE_LENGTH)}…`;\n\t}\n\n\treturn label;\n}\n\nfunction createTouchBarButton({label, selected, icon}: Conversation, index: number): Electron.TouchBarButton {\n\treturn new TouchBarButton({\n\t\tlabel: createLabel(label),\n\t\tbackgroundColor: selected ? '#0084ff' : undefined,\n\t\ticon: nativeImage.createFromDataURL(icon),\n\t\ticonPosition: 'left',\n\t\tclick() {\n\t\t\tsendAction('jump-to-conversation', index + 1);\n\t\t},\n\t});\n}\n\nipc.answerRenderer('conversations', (conversations: Conversation[]) => {\n\tif (config.get('privateMode')) {\n\t\tsetTouchBar([privateModeTouchBarLabel]);\n\t} else {\n\t\tsetTouchBar(conversations.map((conversation, index) => createTouchBarButton(conversation, index)));\n\t}\n});\n"
  },
  {
    "path": "source/tray.ts",
    "content": "import * as path from 'node:path';\nimport {\n\tapp,\n\tMenu,\n\tTray,\n\tBrowserWindow,\n\tMenuItemConstructorOptions,\n} from 'electron';\nimport {is} from 'electron-util';\nimport config from './config';\nimport {toggleMenuBarMode} from './menu-bar-mode';\n\nlet tray: Tray | undefined;\nlet previousMessageCount = 0;\n\nlet contextMenu: Menu;\n\nexport default {\n\tcreate(win: BrowserWindow) {\n\t\tif (tray) {\n\t\t\treturn;\n\t\t}\n\n\t\tfunction toggleWindow(): void {\n\t\t\tif (win.isVisible()) {\n\t\t\t\twin.hide();\n\t\t\t} else {\n\t\t\t\tif (config.get('lastWindowState').isMaximized) {\n\t\t\t\t\twin.maximize();\n\t\t\t\t\twin.focus();\n\t\t\t\t} else {\n\t\t\t\t\twin.show();\n\t\t\t\t}\n\n\t\t\t\t// Workaround for https://github.com/electron/electron/issues/20858\n\t\t\t\t// `setAlwaysOnTop` stops working after hiding the window on KDE Plasma.\n\t\t\t\tconst alwaysOnTopMenuItem = Menu.getApplicationMenu()!.getMenuItemById('always-on-top')!;\n\t\t\t\twin.setAlwaysOnTop(alwaysOnTopMenuItem.checked);\n\t\t\t}\n\t\t}\n\n\t\tconst macosMenuItems: MenuItemConstructorOptions[] = is.macos\n\t\t\t? [\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Disable Menu Bar Mode',\n\t\t\t\t\tclick() {\n\t\t\t\t\t\tconfig.set('menuBarMode', false);\n\t\t\t\t\t\ttoggleMenuBarMode(win);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tlabel: 'Show Dock Icon',\n\t\t\t\t\ttype: 'checkbox',\n\t\t\t\t\tchecked: config.get('showDockIcon'),\n\t\t\t\t\tclick(menuItem) {\n\t\t\t\t\t\tconfig.set('showDockIcon', menuItem.checked);\n\n\t\t\t\t\t\tif (menuItem.checked) {\n\t\t\t\t\t\t\tapp.dock.show();\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tapp.dock.hide();\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst dockMenuItem = contextMenu.getMenuItemById('dockMenu')!;\n\t\t\t\t\t\tdockMenuItem.visible = !menuItem.checked;\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\ttype: 'separator',\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tid: 'dockMenu',\n\t\t\t\t\tlabel: 'Menu',\n\t\t\t\t\tvisible: !config.get('showDockIcon'),\n\t\t\t\t\tsubmenu: Menu.getApplicationMenu()!,\n\t\t\t\t},\n\t\t\t] : [];\n\n\t\tcontextMenu = Menu.buildFromTemplate([\n\t\t\t{\n\t\t\t\tlabel: 'Toggle',\n\t\t\t\tvisible: !is.macos,\n\t\t\t\tclick() {\n\t\t\t\t\ttoggleWindow();\n\t\t\t\t},\n\t\t\t},\n\t\t\t...macosMenuItems,\n\t\t\t{\n\t\t\t\ttype: 'separator',\n\t\t\t},\n\t\t\t{\n\t\t\t\trole: 'quit',\n\t\t\t},\n\t\t]);\n\n\t\ttray = new Tray(getIconPath(false));\n\n\t\ttray.setContextMenu(contextMenu);\n\n\t\tupdateToolTip(0);\n\n\t\tconst trayClickHandler = (): void => {\n\t\t\tif (!win.isFullScreen()) {\n\t\t\t\ttoggleWindow();\n\t\t\t}\n\t\t};\n\n\t\ttray.on('click', trayClickHandler);\n\t\ttray.on('double-click', trayClickHandler);\n\t\ttray.on('right-click', () => {\n\t\t\ttray?.popUpContextMenu(contextMenu);\n\t\t});\n\t},\n\n\tdestroy() {\n\t\t// Workaround for https://github.com/electron/electron/issues/14036\n\t\tsetTimeout(() => {\n\t\t\ttray?.destroy();\n\t\t\ttray = undefined;\n\t\t}, 500);\n\t},\n\n\tupdate(messageCount: number) {\n\t\tif (!tray || previousMessageCount === messageCount) {\n\t\t\treturn;\n\t\t}\n\n\t\tpreviousMessageCount = messageCount;\n\t\ttray.setImage(getIconPath(messageCount > 0));\n\t\tupdateToolTip(messageCount);\n\t},\n\n\tsetBadge(shouldDisplayUnread: boolean) {\n\t\tif (is.macos || !tray) {\n\t\t\treturn;\n\t\t}\n\n\t\ttray.setImage(getIconPath(shouldDisplayUnread));\n\t},\n};\n\nfunction updateToolTip(counter: number): void {\n\tif (!tray) {\n\t\treturn;\n\t}\n\n\tlet tooltip = app.name;\n\n\tif (counter > 0) {\n\t\ttooltip += `- ${counter} unread ${counter === 1 ? 'message' : 'messages'}`;\n\t}\n\n\ttray.setToolTip(tooltip);\n}\n\nfunction getIconPath(hasUnreadMessages: boolean): string {\n\tconst icon = is.macos\n\t\t? getMacOSIconName(hasUnreadMessages)\n\t\t: getNonMacOSIconName(hasUnreadMessages);\n\n\treturn path.join(__dirname, '..', `static/${icon}`);\n}\n\nfunction getNonMacOSIconName(hasUnreadMessages: boolean): string {\n\treturn hasUnreadMessages ? 'IconTrayUnread.png' : 'IconTray.png';\n}\n\nfunction getMacOSIconName(hasUnreadMessages: boolean): string {\n\treturn hasUnreadMessages ? 'IconMenuBarUnreadTemplate.png' : 'IconMenuBarTemplate.png';\n}\n"
  },
  {
    "path": "source/types.ts",
    "content": "export type IToggleSounds = {\n\tchecked: boolean;\n};\n\nexport type IToggleMuteNotifications = {\n\tdefaultStatus: boolean;\n};\n"
  },
  {
    "path": "source/util.ts",
    "content": "import {\n\tapp,\n\tBrowserWindow,\n\tdialog,\n\tMenu,\n} from 'electron';\nimport {ipcMain} from 'electron-better-ipc';\nimport {is} from 'electron-util';\nimport config from './config';\nimport tray from './tray';\n\nexport function getWindow(): BrowserWindow {\n\tconst [win] = BrowserWindow.getAllWindows();\n\treturn win;\n}\n\nexport function sendAction<T>(action: string, arguments_?: T): void {\n\tconst win = getWindow();\n\n\tif (is.macos) {\n\t\twin.restore();\n\t}\n\n\tipcMain.callRenderer(win, action, arguments_);\n}\n\nexport async function sendBackgroundAction<T, ReturnValue>(action: string, arguments_?: T): Promise<ReturnValue> {\n\treturn ipcMain.callRenderer<T, ReturnValue>(getWindow(), action, arguments_);\n}\n\nexport function showRestartDialog(message: string): void {\n\tconst buttonIndex = dialog.showMessageBoxSync(\n\t\tgetWindow(),\n\t\t{\n\t\t\tmessage,\n\t\t\tdetail: 'Do you want to restart the app now?',\n\t\t\tbuttons: [\n\t\t\t\t'Restart',\n\t\t\t\t'Ignore',\n\t\t\t],\n\t\t\tdefaultId: 0,\n\t\t\tcancelId: 1,\n\t\t},\n\t);\n\n\tif (buttonIndex === 0) {\n\t\tapp.relaunch();\n\t\tapp.quit();\n\t}\n}\n\nexport const messengerDomain = config.get('useWorkChat') ? 'facebook.com' : 'messenger.com';\n\nexport function stripTrackingFromUrl(url: string): string {\n\tconst trackingUrlPrefix = `https://l.${messengerDomain}/l.php`;\n\tif (url.startsWith(trackingUrlPrefix)) {\n\t\turl = new URL(url).searchParams.get('u')!;\n\t}\n\n\treturn url;\n}\n\nexport const toggleTrayIcon = (): void => {\n\tconst showTrayIconState = config.get('showTrayIcon');\n\tconfig.set('showTrayIcon', !showTrayIconState);\n\n\tif (showTrayIconState) {\n\t\ttray.destroy();\n\t} else {\n\t\ttray.create(getWindow());\n\t}\n};\n\nexport const toggleLaunchMinimized = (menu: Menu): void => {\n\tconfig.set('launchMinimized', !config.get('launchMinimized'));\n\tconst showTrayIconItem = menu.getMenuItemById('showTrayIcon')!;\n\n\tif (config.get('launchMinimized')) {\n\t\tif (!config.get('showTrayIcon')) {\n\t\t\ttoggleTrayIcon();\n\t\t}\n\n\t\tdisableMenuItem(showTrayIconItem, true);\n\n\t\tdialog.showMessageBox({\n\t\t\ttype: 'info',\n\t\t\tmessage: 'The “Show Tray Icon” setting is force-enabled while the “Launch Minimized” setting is enabled.',\n\t\t\tbuttons: ['OK'],\n\t\t});\n\t} else {\n\t\tshowTrayIconItem.enabled = true;\n\t}\n};\n\nconst disableMenuItem = (menuItem: Electron.MenuItem, checked: boolean): void => {\n\tmenuItem.enabled = false;\n\tmenuItem.checked = checked;\n};\n"
  },
  {
    "path": "static/readme.md",
    "content": "# Notes\n\n## Convert `IconTrayUnread` to black & white\n\nIn Photoshop, click `Image → Adjustments → Black & White`, choose the `Default` preset, and then change `Blues` to `-10%`.\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n\t\"extends\": \"@sindresorhus/tsconfig\",\n\t\"compilerOptions\": {\n\t\t\"outDir\": \"dist-js\",\n\t\t\"target\": \"ES2022\",\n\t\t\"module\": \"commonjs\",\n\t\t\"esModuleInterop\": true,\n\t\t\"lib\": [\n\t\t\t\"esnext\",\n\t\t\t\"dom\",\n\t\t\t\"dom.iterable\"\n\t\t]\n\t}\n}"
  }
]