electron#BrowserView TypeScript Examples

The following examples show how to use electron#BrowserView. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: getFromRenderer.ts    From TidGi-Desktop with Mozilla Public License 2.0 6 votes vote down vote up
/**
 * Get data from a BrowserView
 * @param channel
 * @param viewToGetData
 */
export default async function getFromRenderer<T>(channel: Channels, viewToGetData: BrowserView | BrowserWindow): Promise<T> {
  // prevent several ipc happened together, and later return too early so first get the result that is for later one
  const ipcToken = uuid();
  viewToGetData.webContents.send(channel, { ipcToken });
  return await new Promise((resolve) => {
    ipcMain.once(`${channel}-${ipcToken}`, (_event, data: T) => resolve(data));
  });
}
Example #2
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 6 votes vote down vote up
public forEachView(functionToRun: (view: BrowserView, workspaceID: string, windowName: WindowNames) => unknown): void {
    Object.keys(this.views).forEach((id) => {
      const workspaceOwnedViews = this.views[id];
      if (workspaceOwnedViews !== undefined) {
        (Object.keys(workspaceOwnedViews) as WindowNames[]).forEach((name) => {
          const view = this.getView(id, name);
          if (view !== undefined) {
            functionToRun(view, id, name);
          }
        });
      }
    });
  }
Example #3
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 6 votes vote down vote up
public async getActiveBrowserView(): Promise<BrowserView | undefined> {
    const workspace = await this.workspaceService.getActiveWorkspace();
    const isMenubarOpen = await this.windowService.isMenubarOpen();
    if (workspace !== undefined) {
      if (isMenubarOpen) {
        return this.getView(workspace.id, WindowNames.menuBar);
      } else {
        return this.getView(workspace.id, WindowNames.main);
      }
    }
  }
Example #4
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 6 votes vote down vote up
public async setWikiLanguage(view: BrowserView, workspaceID: string, tiddlywikiLanguageName: string): Promise<void> {
    const twLanguageUpdateTimeout = 15_000;
    const retryTime = 2000;
    return await new Promise<void>((resolve, reject) => {
      const onRetryOrDo = (): void => {
        view.webContents.send(WikiChannel.setTiddlerText, '$:/language', tiddlywikiLanguageName, workspaceID);
      };
      const intervalHandle = setInterval(onRetryOrDo, retryTime);
      const onTimeout = (): void => {
        ipcMain.removeListener(WikiChannel.setTiddlerTextDone + workspaceID, onDone);
        clearInterval(intervalHandle);
        const errorMessage = `setWikiLanguage("${tiddlywikiLanguageName}"), language "${tiddlywikiLanguageName}" in workspaceID ${workspaceID} is too slow to update after ${twLanguageUpdateTimeout}ms.`;
        logger.error(errorMessage);
        reject(new Error(errorMessage));
      };
      const timeoutHandle = setTimeout(onTimeout, twLanguageUpdateTimeout);
      const onDone = (): void => {
        clearTimeout(timeoutHandle);
        clearInterval(intervalHandle);
        resolve();
      };
      ipcMain.once(WikiChannel.setTiddlerTextDone + workspaceID, onDone);
      onRetryOrDo();
    });
  }
Example #5
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
public async getActiveBrowserViews(): Promise<Array<BrowserView | undefined>> {
    const workspace = await this.workspaceService.getActiveWorkspace();
    if (workspace !== undefined) {
      return [this.getView(workspace.id, WindowNames.main), this.getView(workspace.id, WindowNames.menuBar)];
    }
    return [];
  }
Example #6
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
/**
   * Record<workspaceID, Record<windowName, BrowserView>>
   *
   * Each workspace can have several windows to render its view (main window and menu bar)
   */
  private views: Record<string, Record<WindowNames, BrowserView> | undefined> = {};
Example #7
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
public getView = (workspaceID: string, windowName: WindowNames): BrowserView | undefined => this.views[workspaceID]?.[windowName];
Example #8
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
public getAllViewOfWorkspace = (workspaceID: string): BrowserView[] => Object.values(this.views[workspaceID] ?? {});
Example #9
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 5 votes vote down vote up
public setView = (workspaceID: string, windowName: WindowNames, newView: BrowserView): void => {
    const workspaceOwnedViews = this.views[workspaceID];
    if (workspaceOwnedViews === undefined) {
      this.views[workspaceID] = { [windowName]: newView } as Record<WindowNames, BrowserView>;
    } else {
      workspaceOwnedViews[windowName] = newView;
    }
  };
Example #10
Source File: chrome-browserAction-spec.ts    From electron-browser-shell with GNU General Public License v3.0 4 votes vote down vote up
describe('chrome.browserAction', () => {
  const server = useServer()

  const defaultAnchorRect = {
    x: 0,
    y: 0,
    width: 16,
    height: 16,
  }

  const activateExtension = async (
    partition: string,
    webContents: WebContents,
    extension: Extension,
    tabId: number = -1
  ) => {
    const details = {
      eventType: 'click',
      extensionId: extension.id,
      tabId,
      anchorRect: defaultAnchorRect,
    }

    const js = `browserAction.activate('${partition}', ${JSON.stringify(details)})`
    await webContents.executeJavaScript(js)
  }

  describe('messaging', () => {
    const browser = useExtensionBrowser({
      url: server.getUrl,
      extensionName: 'chrome-browserAction-click',
    })

    it('supports cross-session communication', async () => {
      const otherSession = session.fromPartition(`persist:crx-${uuid()}`)
      otherSession.setPreloads(browser.session.getPreloads())

      const view = new BrowserView({
        webPreferences: { session: otherSession, nodeIntegration: false, contextIsolation: true },
      })
      await view.webContents.loadURL(server.getUrl())
      browser.window.addBrowserView(view)
      await activateExtension(browser.partition, view.webContents, browser.extension)
    })

    it('can request action for specific tab', async () => {
      const tab = browser.window.webContents
      await activateExtension(browser.partition, tab, browser.extension, tab.id)
    })

    it('throws for unknown tab', async () => {
      const tab = browser.window.webContents
      const unknownTabId = 99999
      let caught = false
      try {
        await activateExtension(browser.partition, tab, browser.extension, unknownTabId)
      } catch {
        caught = true
      }
      expect(caught).to.be.true
    })
  })

  describe('onClicked', () => {
    const browser = useExtensionBrowser({
      url: server.getUrl,
      extensionName: 'chrome-browserAction-click',
    })

    it('fires listeners when activated', async () => {
      const tabPromise = emittedOnce(ipcMain, 'success')
      await activateExtension(browser.partition, browser.window.webContents, browser.extension)
      const [_, tabDetails] = await tabPromise
      expect(tabDetails).to.be.an('object')
      expect(tabDetails.id).to.equal(browser.window.webContents.id)
    })
  })

  describe('popup', () => {
    const browser = useExtensionBrowser({
      url: server.getUrl,
      extensionName: 'chrome-browserAction-popup',
    })

    it('opens when the browser action is clicked', async () => {
      const popupPromise = emittedOnce(browser.extensions, 'browser-action-popup-created')
      await activateExtension(browser.partition, browser.window.webContents, browser.extension)
      const [popup] = await popupPromise
      expect(popup.extensionId).to.equal(browser.extension.id)
    })

    it('opens when BrowserView is the active tab', async () => {
      const view = new BrowserView({
        webPreferences: {
          session: browser.session,
          nodeIntegration: false,
          contextIsolation: true,
        },
      })
      await view.webContents.loadURL(server.getUrl())
      browser.window.addBrowserView(view)
      browser.extensions.addTab(view.webContents, browser.window)
      browser.extensions.selectTab(view.webContents)

      const popupPromise = emittedOnce(browser.extensions, 'browser-action-popup-created')
      await activateExtension(browser.partition, browser.window.webContents, browser.extension)
      const [popup] = await popupPromise
      expect(popup.extensionId).to.equal(browser.extension.id)
    })
  })

  describe('details', () => {
    const browser = useExtensionBrowser({
      url: server.getUrl,
      extensionName: 'rpc',
    })

    const props = [
      { method: 'BadgeBackgroundColor', detail: 'color', value: '#cacaca' },
      { method: 'BadgeText', detail: 'text' },
      { method: 'Popup', detail: 'popup' },
      { method: 'Title', detail: 'title' },
    ]

    for (const { method, detail, value } of props) {
      it(`sets and gets '${detail}'`, async () => {
        const newValue = value || uuid()
        await browser.crx.exec(`browserAction.set${method}`, { [detail]: newValue })
        const result = await browser.crx.exec(`browserAction.get${method}`)
        expect(result).to.equal(newValue)
      })

      it(`restores initial values for '${detail}'`, async () => {
        const newValue = value || uuid()
        const initial = await browser.crx.exec(`browserAction.get${method}`)
        await browser.crx.exec(`browserAction.set${method}`, { [detail]: newValue })
        await browser.crx.exec(`browserAction.set${method}`, { [detail]: null })
        const result = await browser.crx.exec(`browserAction.get${method}`)
        expect(result).to.equal(initial)
      })
    }

    it('uses custom popup when opening browser action', async () => {
      const popupUuid = uuid()
      const popupPath = `popup.html?${popupUuid}`
      await browser.crx.exec('browserAction.setPopup', { popup: popupPath })
      const popupPromise = emittedOnce(browser.extensions, 'browser-action-popup-created')
      await activateExtension(browser.partition, browser.window.webContents, browser.extension)
      const [popup] = await popupPromise
      await popup.whenReady()
      expect(popup.browserWindow.webContents.getURL()).to.equal(
        `chrome-extension://${browser.extension.id}/${popupPath}`
      )
    })
  })

  describe('<browser-action-list> element', () => {
    const basePath = path.join(__dirname, 'fixtures/browser-action-list')

    const browser = useExtensionBrowser({
      extensionName: 'chrome-browserAction-popup',
    })

    it('lists actions', async () => {
      await browser.webContents.loadFile(path.join(basePath, 'default.html'))

      const extensionIds = await browser.webContents.executeJavaScript(
        `(${() => {
          const list = document.querySelector('browser-action-list')!
          const actions = list.shadowRoot!.querySelectorAll('.action')
          const ids = Array.from(actions).map((elem) => elem.id)
          return ids
        }})();`
      )

      expect(extensionIds).to.deep.equal([browser.extension.id])
    })

    it('lists actions in remote partition', async () => {
      const remoteWindow = createCrxRemoteWindow()
      const remoteTab = remoteWindow.webContents

      await remoteTab.loadURL(server.getUrl())

      // Add <browser-action-list> for remote partition.
      await remoteTab.executeJavaScript(
        `(${(partition: string) => {
          const list = document.createElement('browser-action-list')
          list.setAttribute('partition', partition)
          document.body.appendChild(list)
        }})('${browser.partition}');`
      )

      const extensionIds = await remoteTab.executeJavaScript(
        `(${() => {
          const list = document.querySelector('browser-action-list')!
          const actions = list.shadowRoot!.querySelectorAll('.action')
          const ids = Array.from(actions).map((elem) => elem.id)
          return ids
        }})();`
      )

      expect(extensionIds).to.deep.equal([browser.extension.id])
    })
  })
})
Example #11
Source File: index.ts    From TidGi-Desktop with Mozilla Public License 2.0 4 votes vote down vote up
public async addView(workspace: IWorkspace, windowName: WindowNames): Promise<void> {
    // we assume each window will only have one view, so get view by window name + workspace
    const existedView = this.getView(workspace.id, windowName);
    const browserWindow = this.windowService.get(windowName);
    if (existedView !== undefined) {
      logger.warn(`BrowserViewService.addView: ${workspace.id} 's view already exists`);
      return;
    }
    if (browserWindow === undefined) {
      logger.warn(`BrowserViewService.addView: ${workspace.id} 's browser window is not ready`);
      return;
    }
    // create a new BrowserView
    const { rememberLastPageVisited, shareWorkspaceBrowsingData, spellcheck, spellcheckLanguages } = await this.preferenceService.getPreferences();
    // configure session, proxy & ad blocker
    const partitionId = shareWorkspaceBrowsingData ? 'persist:shared' : `persist:${workspace.id}`;
    // prepare configs for start a BrowserView that loads wiki's web content
    // session
    const sessionOfView = session.fromPartition(partitionId);
    // spellchecker
    if (spellcheck && process.platform !== 'darwin') {
      sessionOfView.setSpellCheckerLanguages(spellcheckLanguages);
    }
    const browserViewMetaData: IBrowserViewMetaData = { workspaceID: workspace.id };
    const sharedWebPreferences: WebPreferences = {
      devTools: true,
      spellcheck,
      nodeIntegration: false,
      contextIsolation: true,
      // allow loading pictures from the localhost network, you may want to setup img host services in your local network, set this to true will cause CORS
      // TODO: make this a setting in security preference
      webSecurity: false,
      allowRunningInsecureContent: true,
      session: sessionOfView,
      preload: MAIN_WINDOW_PRELOAD_WEBPACK_ENTRY,
      additionalArguments: [
        `${MetaDataChannel.browserViewMetaData}${WindowNames.view}`,
        `${MetaDataChannel.browserViewMetaData}${encodeURIComponent(JSON.stringify(browserViewMetaData))}`,
      ],
    };
    const view = new BrowserView({
      webPreferences: sharedWebPreferences,
    });
    // background needs to explicitly set
    // if not, by default, the background of BrowserView is transparent
    // which would break the CSS of certain websites
    // even with dark mode, all major browsers
    // always use #FFF as default page background
    // https://github.com/atomery/webcatalog/issues/723
    // https://github.com/electron/electron/issues/16212
    view.setBackgroundColor('#fafafa');

    // Handle audio & notification preferences
    if (this.shouldMuteAudio !== undefined) {
      view.webContents.audioMuted = this.shouldMuteAudio;
    }
    this.setView(workspace.id, windowName, view);
    if (workspace.active) {
      browserWindow.setBrowserView(view);
      const contentSize = browserWindow.getContentSize();
      view.setBounds(await getViewBounds(contentSize as [number, number]));
      view.setAutoResize({
        width: true,
        height: true,
      });
    }
    // fix some case that local ip can't be load
    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
    const urlToReplace = (rememberLastPageVisited && workspace.lastUrl) || workspace.homeUrl;
    const portReplacedUrl = replaceUrlPortWithSettingPort(urlToReplace, workspace.port);
    const hostReplacedUrl = await getLocalHostUrlWithActualIP(portReplacedUrl);
    logger.debug(`Load initialUrl: ${hostReplacedUrl} for windowName ${windowName} for workspace ${workspace.name}`, {
      urlToReplace,
      replacedUrl: portReplacedUrl,
    });
    /**
     * Try catch loadUrl, other wise it will throw unhandled promise rejection Error: ERR_CONNECTION_REFUSED (-102) loading 'http://localhost:5212/
     * We will set `didFailLoadErrorMessage`, it will set didFailLoadErrorMessage, and we throw actuarial error after that
     */
    const loadInitialUrlWithCatch = async (): Promise<void> => {
      try {
        logger.debug(
          `loadInitialUrlWithCatch(): view.webContents: ${String(view.webContents)} ${hostReplacedUrl} for windowName ${windowName} for workspace ${
            workspace.name
          }`,
          { stack: new Error('debug error, not a real error').stack },
        );
        if (await this.workspaceService.workspaceDidFailLoad(workspace.id)) {
          return;
        }
        // will set again in view.webContents.on('did-start-loading'), but that one sometimes is too late to block services that wait for `isLoading`
        await this.workspaceService.updateMetaData(workspace.id, {
          // eslint-disable-next-line unicorn/no-null
          didFailLoadErrorMessage: null,
          isLoading: true,
        });
        await view.webContents.loadURL(hostReplacedUrl);
        logger.debug('loadInitialUrlWithCatch() await loadURL() done');
        const unregisterContextMenu = await this.menuService.initContextMenuForWindowWebContents(view.webContents);
        view.webContents.on('destroyed', () => {
          unregisterContextMenu();
        });
      } catch (error) {
        logger.warn(new ViewLoadUrlError(hostReplacedUrl, `${(error as Error).message} ${(error as Error).stack ?? ''}`));
      }
    };
    setupViewEventHandlers(view, browserWindow, {
      shouldPauseNotifications: this.shouldPauseNotifications,
      workspace,
      sharedWebPreferences,
      loadInitialUrlWithCatch,
    });
    await loadInitialUrlWithCatch();
  }
Example #12
Source File: setupViewEventHandlers.ts    From TidGi-Desktop with Mozilla Public License 2.0 4 votes vote down vote up
/**
 * Bind workspace related event handler to view.webContent
 */
export default function setupViewEventHandlers(
  view: BrowserView,
  browserWindow: BrowserWindow,
  { workspace, sharedWebPreferences, loadInitialUrlWithCatch }: IViewContext,
): void {
  // metadata and state about current BrowserView
  const viewMeta: IViewMeta = {
    forceNewWindow: false,
  };

  const workspaceService = container.get<IWorkspaceService>(serviceIdentifier.Workspace);
  const workspaceViewService = container.get<IWorkspaceViewService>(serviceIdentifier.WorkspaceView);
  const windowService = container.get<IWindowService>(serviceIdentifier.Window);
  const preferenceService = container.get<IPreferenceService>(serviceIdentifier.Preference);

  view.webContents.on('did-start-loading', async () => {
    const workspaceObject = await workspaceService.get(workspace.id);
    // this event might be triggered
    // even after the workspace obj and BrowserView
    // are destroyed. See https://github.com/atomery/webcatalog/issues/836
    if (workspaceObject === undefined) {
      return;
    }
    if (workspaceObject.active && (await workspaceService.workspaceDidFailLoad(workspace.id)) && browserWindow !== undefined && !browserWindow.isDestroyed()) {
      // fix https://github.com/webcatalog/singlebox-legacy/issues/228
      const contentSize = browserWindow.getContentSize();
      view.setBounds(await getViewBounds(contentSize as [number, number]));
    }
    await workspaceService.updateMetaData(workspace.id, {
      // eslint-disable-next-line unicorn/no-null
      didFailLoadErrorMessage: null,
      isLoading: true,
    });
  });
  view.webContents.on('did-navigate-in-page', async () => {
    await workspaceViewService.updateLastUrl(workspace.id, view);
  });

  const throttledDidFinishedLoad = throttle(async () => {
    // if have error, don't realignActiveWorkspace, which will hide the error message
    if (await workspaceService.workspaceDidFailLoad(workspace.id)) {
      return;
    }
    logger.debug(`throttledDidFinishedLoad() workspace.id: ${workspace.id}, now workspaceViewService.realignActiveWorkspace() then set isLoading to false`);
    // focus on initial load
    // https://github.com/atomery/webcatalog/issues/398
    if (workspace.active && !browserWindow.isDestroyed() && browserWindow.isFocused() && !view.webContents.isFocused()) {
      view.webContents.focus();
    }
    // fix https://github.com/atomery/webcatalog/issues/870
    await workspaceViewService.realignActiveWorkspace();
    // update isLoading to false when load succeed
    await workspaceService.updateMetaData(workspace.id, {
      isLoading: false,
    });
  }, 2000);
  view.webContents.on('did-finish-load', () => {
    logger.debug('did-finish-load called');
    void throttledDidFinishedLoad();
  });
  view.webContents.on('did-stop-loading', () => {
    logger.debug('did-stop-loading called');
    void throttledDidFinishedLoad();
  });
  view.webContents.on('dom-ready', () => {
    logger.debug('dom-ready called');
    void throttledDidFinishedLoad();
  });

  // https://electronjs.org/docs/api/web-contents#event-did-fail-load
  // https://github.com/webcatalog/neutron/blob/3d9e65c255792672c8bc6da025513a5404d98730/main-src/libs/views.js#L397
  view.webContents.on('did-fail-load', async (_event, errorCode, errorDesc, _validateUrl, isMainFrame) => {
    const [workspaceObject, workspaceDidFailLoad] = await Promise.all([
      workspaceService.get(workspace.id),
      workspaceService.workspaceDidFailLoad(workspace.id),
    ]);
    // this event might be triggered
    // even after the workspace obj and BrowserView
    // are destroyed. See https://github.com/atomery/webcatalog/issues/836
    if (workspaceObject === undefined) {
      return;
    }
    if (workspaceDidFailLoad) {
      return;
    }
    if (isMainFrame && errorCode < 0 && errorCode !== -3) {
      // Fix nodejs wiki start slow on system startup, which cause `-102 ERR_CONNECTION_REFUSED` even if wiki said it is booted, we have to retry several times
      if (errorCode === -102 && view.webContents.getURL().length > 0 && workspaceObject.homeUrl.startsWith('http')) {
        setTimeout(async () => {
          await loadInitialUrlWithCatch();
        }, 1000);
        return;
      }
      await workspaceService.updateMetaData(workspace.id, {
        isLoading: false,
        didFailLoadErrorMessage: `${errorCode} ${errorDesc}`,
      });
      if (workspaceObject.active && browserWindow !== undefined && !browserWindow.isDestroyed()) {
        // fix https://github.com/atomery/singlebox/issues/228
        const contentSize = browserWindow.getContentSize();
        view.setBounds(await getViewBounds(contentSize as [number, number], false, 0, 0)); // hide browserView to show error message
      }
    }
    // edge case to handle failed auth, use setTimeout to prevent infinite loop
    if (errorCode === -300 && view.webContents.getURL().length === 0 && workspaceObject.homeUrl.startsWith('http')) {
      setTimeout(async () => {
        await loadInitialUrlWithCatch();
      }, 1000);
    }
  });
  view.webContents.on('did-navigate', async (_event, url) => {
    const workspaceObject = await workspaceService.get(workspace.id);
    // this event might be triggered
    // even after the workspace obj and BrowserView
    // are destroyed. See https://github.com/atomery/webcatalog/issues/836
    if (workspaceObject === undefined) {
      return;
    }
    if (workspaceObject.active) {
      await windowService.sendToAllWindows(WindowChannel.updateCanGoBack, view.webContents.canGoBack());
      await windowService.sendToAllWindows(WindowChannel.updateCanGoForward, view.webContents.canGoForward());
    }
  });
  view.webContents.on('did-navigate-in-page', async (_event, url) => {
    const workspaceObject = await workspaceService.get(workspace.id);
    // this event might be triggered
    // even after the workspace obj and BrowserView
    // are destroyed. See https://github.com/atomery/webcatalog/issues/836
    if (workspaceObject === undefined) {
      return;
    }
    if (workspaceObject.active) {
      await windowService.sendToAllWindows(WindowChannel.updateCanGoBack, view.webContents.canGoBack());
      await windowService.sendToAllWindows(WindowChannel.updateCanGoForward, view.webContents.canGoForward());
    }
  });
  view.webContents.on('page-title-updated', async (_event, title) => {
    const workspaceObject = await workspaceService.get(workspace.id);
    // this event might be triggered
    // even after the workspace obj and BrowserView
    // are destroyed. See https://github.com/atomery/webcatalog/issues/836
    if (workspaceObject === undefined) {
      return;
    }
    if (workspaceObject.active) {
      browserWindow.setTitle(title);
    }
  });

  view.webContents.setWindowOpenHandler((details: Electron.HandlerDetails) =>
    handleNewWindow(
      details.url,
      {
        workspace,
        sharedWebPreferences,
        view,
        meta: viewMeta,
      },
      details.disposition,
      view.webContents,
    ),
  );
  // Handle downloads
  // https://electronjs.org/docs/api/download-item
  view.webContents.session.on('will-download', async (_event, item) => {
    const { askForDownloadPath, downloadPath } = await preferenceService.getPreferences();
    // Set the save path, making Electron not to prompt a save dialog.
    if (!askForDownloadPath) {
      const finalFilePath = path.join(downloadPath, item.getFilename());
      if (!fsExtra.existsSync(finalFilePath)) {
        // eslint-disable-next-line no-param-reassign
        item.savePath = finalFilePath;
      }
    } else {
      // set preferred path for save dialog
      const options = {
        ...item.getSaveDialogOptions(),
        defaultPath: path.join(downloadPath, item.getFilename()),
      };
      item.setSaveDialogOptions(options);
    }
  });
  // Unread count badge
  void preferenceService.get('unreadCountBadge').then((unreadCountBadge) => {
    if (unreadCountBadge) {
      view.webContents.on('page-title-updated', async (_event, title) => {
        const itemCountRegex = /[([{](\d*?)[)\]}]/;
        const match = itemCountRegex.exec(title);
        const incString = match !== null ? match[1] : '';
        // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
        const inc = Number.parseInt(incString, 10) || 0;
        await workspaceService.updateMetaData(workspace.id, {
          badgeCount: inc,
        });
        let count = 0;
        const workspaceMetaData = await workspaceService.getAllMetaData();
        Object.values(workspaceMetaData).forEach((metaData) => {
          if (typeof metaData?.badgeCount === 'number') {
            count += metaData.badgeCount;
          }
        });
        app.badgeCount = count;
        if (process.platform === 'win32') {
          if (count > 0) {
            const icon = nativeImage.createFromPath(path.resolve(buildResourcePath, 'overlay-icon.png'));
            browserWindow.setOverlayIcon(icon, `You have ${count} new messages.`);
          } else {
            // eslint-disable-next-line unicorn/no-null
            browserWindow.setOverlayIcon(null, '');
          }
        }
      });
    }
  });
  // Find In Page
  view.webContents.on('found-in-page', async (_event, result) => {
    await windowService.sendToAllWindows(ViewChannel.updateFindInPageMatches, result.activeMatchOrdinal, result.matches);
  });
  // Link preview
  view.webContents.on('update-target-url', (_event, url) => {
    try {
      view.webContents.send('update-target-url', url);
    } catch (error) {
      logger.warn(error); // eslint-disable-line no-console
    }
  });
}