diff --git a/.eslintrc.js b/.eslintrc.js index 2eb57dea8..0134474b5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,6 +33,10 @@ module.exports = { }, rules: { + // import/default is not compatible with SFC Setup .vue files + // It works fine on server because by default in @nextcloud/eslint-config .vue files are not inspected via eslint-plugin-import thus import/extensions doesn't include .vue + // See: https://github.com/import-js/eslint-plugin-import/blob/main/README.md#importextensions + 'import/default': 'off', /** * ESLint */ diff --git a/README.md b/README.md index c917f268f..f9e1bd5e0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ ## 👾 Drawbacks - Currently not supported: - - Screen sharing ([#11](https://github.com/nextcloud/talk-desktop/issues/11)) - Setting User Status ([#26](https://github.com/nextcloud/talk-desktop/issues/26)) - Search ([#30](https://github.com/nextcloud/talk-desktop/issues/30)) - Untrusted certificate on Linux ([#23](https://github.com/nextcloud/talk-desktop/issues/23)) diff --git a/package-lock.json b/package-lock.json index 9740af8e2..f52d92c4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.27.0", "license": "AGPL-3.0", "dependencies": { + "@mdi/svg": "^7.4.47", "@nextcloud/axios": "^2.4.0", "@nextcloud/browser-storage": "^0.3.0", "@nextcloud/capabilities": "^1.1.0", @@ -4148,6 +4149,11 @@ "unist-util-is": "^3.0.0" } }, + "node_modules/@mdi/svg": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.4.47.tgz", + "integrity": "sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw==" + }, "node_modules/@nextcloud/auth": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.2.1.tgz", @@ -23630,6 +23636,11 @@ } } }, + "@mdi/svg": { + "version": "7.4.47", + "resolved": "https://registry.npmjs.org/@mdi/svg/-/svg-7.4.47.tgz", + "integrity": "sha512-WQ2gDll12T9WD34fdRFgQVgO8bag3gavrAgJ0frN4phlwdJARpE6gO1YvLEMJR0KKgoc+/Ea/A0Pp11I00xBvw==" + }, "@nextcloud/auth": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.2.1.tgz", diff --git a/package.json b/package.json index 062ca87b9..6783bddea 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "release:package": "zx ./scripts/prepare-release-packages.mjs" }, "dependencies": { + "@mdi/svg": "^7.4.47", "@nextcloud/axios": "^2.4.0", "@nextcloud/browser-storage": "^0.3.0", "@nextcloud/capabilities": "^1.1.0", diff --git a/src/main.js b/src/main.js index bd0a1b335..cd63ae90e 100644 --- a/src/main.js +++ b/src/main.js @@ -20,7 +20,7 @@ */ const path = require('node:path') -const { app, dialog, BrowserWindow, ipcMain } = require('electron') +const { app, dialog, BrowserWindow, ipcMain, desktopCapturer, systemPreferences, shell } = require('electron') const { setupMenu } = require('./app/app.menu.js') const { setupReleaseNotificationScheduler } = require('./app/githubReleaseNotification.service.js') const { enableWebRequestInterceptor, disableWebRequestInterceptor } = require('./app/webRequestInterceptor.js') @@ -28,7 +28,7 @@ const { createAuthenticationWindow } = require('./authentication/authentication. const { openLoginWebView } = require('./authentication/login.window.js') const { createHelpWindow } = require('./help/help.window.js') const { createUpgradeWindow } = require('./upgrade/upgrade.window.js') -const { getOs, isLinux } = require('./shared/os.utils.js') +const { getOs, isLinux, isMac, isWayland } = require('./shared/os.utils.js') const { createTalkWindow } = require('./talk/talk.window.js') const { createWelcomeWindow } = require('./welcome/welcome.window.js') const { installVueDevtools } = require('./install-vue-devtools.js') @@ -82,6 +82,35 @@ ipcMain.on('app:relaunch', () => { app.relaunch() app.exit(0) }) +ipcMain.handle('app:getDesktopCapturerSources', async () => { + // macOS 10.15 Catalina or higher requires consent for screen access + if (isMac() && systemPreferences.getMediaAccessStatus('screen') !== 'granted') { + // Open System Preferences to allow screen recording + await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture') + // We cannot detect that the user has granted access, so return no sources + // The user will have to try again after granting access + return null + } + + // We cannot show live previews on Wayland, so we show thumbnails + const thumbnailWidth = isWayland() ? 320 : 0 + + const sources = await desktopCapturer.getSources({ + types: ['screen', 'window'], + fetchWindowIcons: true, + thumbnailSize: { + width: thumbnailWidth, + height: thumbnailWidth * 9 / 16, + }, + }) + + return sources.map((source) => ({ + id: source.id, + name: source.name, + icon: source.appIcon && !source.appIcon.isEmpty() ? source.appIcon.toDataURL() : null, + thumbnail: source.thumbnail && !source.thumbnail.isEmpty() ? source.thumbnail.toDataURL() : null, + })) +}) app.whenReady().then(async () => { try { diff --git a/src/preload.js b/src/preload.js index 9af7cf47f..451803a3f 100644 --- a/src/preload.js +++ b/src/preload.js @@ -83,6 +83,12 @@ const TALK_DESKTOP = { * @return {Promise} */ setBadgeCount: (count) => ipcRenderer.invoke('app:setBadgeCount', count), + /** + * Get available desktop capture sources: screens and windows + * + * @return {Promise<{ id: string, name: string, icon?: string }[]|null>} + */ + getDesktopCapturerSources: () => ipcRenderer.invoke('app:getDesktopCapturerSources'), /** * Relaunch the application */ diff --git a/src/shared/os.utils.js b/src/shared/os.utils.js index 2cf170194..7d50a16c1 100644 --- a/src/shared/os.utils.js +++ b/src/shared/os.utils.js @@ -76,11 +76,21 @@ function isWindows() { return os.type() === 'Windows_NT' } +/** + * Is it Linux with Wayland window communication protocol? + * @return {boolean} + */ +function isWayland() { + // TODO: is it better than checking for XDG_SESSION_TYPE === 'wayland'? + return !!process.env.WAYLAND_DISPLAY +} + /** * @typedef OsVersion * @property {boolean} isLinux - Is Linux? * @property {boolean} isMac - Is Mac? * @property {boolean} isWindows - Is Windows? + * @property {boolean} isWayland - Is Linux with Wayland window communication protocol? * @property {string} version - Full string representation of OS version */ @@ -94,6 +104,7 @@ function getOs() { isLinux: isLinux(), isMac: isMac(), isWindows: isWindows(), + isWayland: isWayland(), version: getOsVersion(), } } @@ -104,5 +115,6 @@ module.exports = { isLinux, isMac, isWindows, + isWayland, getOs, } diff --git a/src/talk/renderer/AppGetDesktopMediaSource.vue b/src/talk/renderer/AppGetDesktopMediaSource.vue new file mode 100644 index 000000000..ea147f856 --- /dev/null +++ b/src/talk/renderer/AppGetDesktopMediaSource.vue @@ -0,0 +1,56 @@ + + + + + diff --git a/src/talk/renderer/components/DesktopMediaSourceDialog.vue b/src/talk/renderer/components/DesktopMediaSourceDialog.vue new file mode 100644 index 000000000..8f48fd053 --- /dev/null +++ b/src/talk/renderer/components/DesktopMediaSourceDialog.vue @@ -0,0 +1,156 @@ + + + + + + + diff --git a/src/talk/renderer/components/DesktopMediaSourcePreview.vue b/src/talk/renderer/components/DesktopMediaSourcePreview.vue new file mode 100644 index 000000000..6ca43d2fa --- /dev/null +++ b/src/talk/renderer/components/DesktopMediaSourcePreview.vue @@ -0,0 +1,194 @@ + + + + + + + diff --git a/src/talk/renderer/getDesktopMediaSource.js b/src/talk/renderer/getDesktopMediaSource.js new file mode 100644 index 000000000..b4e0683c0 --- /dev/null +++ b/src/talk/renderer/getDesktopMediaSource.js @@ -0,0 +1,41 @@ +/* + * @copyright Copyright (c) 2024 Grigorii Shartsev + * + * @author Grigorii Shartsev + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import Vue from 'vue' + +import AppGetDesktopMediaSource from './AppGetDesktopMediaSource.vue' + +/** @type {import('vue').ComponentPublicInstance} */ +let appGetDesktopMediaSourceInstance + +/** + * Prompt user to select a desktop media source to share and return the selected sourceId or an empty string if canceled + * + * @return {Promise<{ sourceId: string }>} sourceId of the selected mediaSource or an empty string if canceled + */ +export async function getDesktopMediaSource() { + if (!appGetDesktopMediaSourceInstance) { + const container = document.body.appendChild(document.createElement('div')) + appGetDesktopMediaSourceInstance = new Vue(AppGetDesktopMediaSource).$mount(container) + } + + return appGetDesktopMediaSourceInstance.promptDesktopMediaSource() +} diff --git a/src/talk/renderer/talk.main.js b/src/talk/renderer/talk.main.js index a88384e8e..167a59bf9 100644 --- a/src/talk/renderer/talk.main.js +++ b/src/talk/renderer/talk.main.js @@ -25,6 +25,7 @@ import './assets/styles.css' import 'regenerator-runtime' // TODO: Why isn't it added on bundling import { init, initTalkHashIntegration } from './init.js' import { setupWebPage } from '../../shared/setupWebPage.js' +import { getDesktopMediaSource } from './getDesktopMediaSource.js' // Initially open the welcome page, if not specified if (!window.location.hash) { @@ -43,3 +44,7 @@ await import('@talk/src/main.js') initTalkHashIntegration(OCA.Talk.instance) await import('./notifications/notifications.store.js') + +window.OCA.Talk.Desktop = { + getDesktopMediaSource, +}