From 8be149f913af40f09dd4982da0741b158a564473 Mon Sep 17 00:00:00 2001 From: Grigorii Shartsev Date: Tue, 14 Mar 2023 10:01:41 +0100 Subject: [PATCH 1/5] feat(desktop): prepare webpack config for desktop - Define IS_DESKTOP global variable - Separate the config to common and web parts - Use cross-platform slashes on webpack rules' tests Signed-off-by: Grigorii Shartsev --- .eslintrc.js | 7 +++ src/env.d.ts | 33 +++++++++++++ src/main.js | 22 +++++---- src/test-setup.js | 1 + webpack.common.config.js | 82 ++++++++++++++++++++++++++++++++ webpack.config.js | 100 ++++++++++++--------------------------- 6 files changed, 164 insertions(+), 81 deletions(-) create mode 100644 src/env.d.ts create mode 100644 webpack.common.config.js diff --git a/.eslintrc.js b/.eslintrc.js index 7064426a2da..572355eab13 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,6 +2,13 @@ module.exports = { extends: [ '@nextcloud', ], + globals: { + // @nextcloud/webpack-vue-config globals + appName: 'readonly', + appVersion: 'readonly', + // Desktop build globals + IS_DESKTOP: 'readonly', + }, rules: { 'import/newline-after-import': 1, 'import/no-named-as-default-member': 0, diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 00000000000..9d0d3e6da51 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,33 @@ +/* + * @copyright Copyright (c) 2023 Grigorii Shartsev + * + * @author Grigorii Shartsev + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + */ + +declare global { + // @nextcloud/webpack-vue-config build globals + const appName: string + const appVersion: string + + /** + * Build constant to divide build for web app and desktop client + */ + const IS_DESKTOP: false +} + +export {} diff --git a/src/main.js b/src/main.js index b6f84c4fd1b..99ffb8d1b88 100644 --- a/src/main.js +++ b/src/main.js @@ -52,16 +52,18 @@ import 'leaflet/dist/leaflet.css' // eslint-disable-next-line import 'leaflet-defaulticon-compatibility' -// CSP config for webpack dynamic chunk loading -// eslint-disable-next-line -__webpack_nonce__ = btoa(getRequestToken()) - -// Correct the root of the app for chunk loading -// OC.linkTo matches the apps folders -// OC.generateUrl ensure the index.php (or not) -// We do not want the index.php since we're loading files -// eslint-disable-next-line -__webpack_public_path__ = generateFilePath('spreed', '', 'js/') +if (!IS_DESKTOP) { + // CSP config for webpack dynamic chunk loading + // eslint-disable-next-line + __webpack_nonce__ = btoa(getRequestToken()) + + // Correct the root of the app for chunk loading + // OC.linkTo matches the apps folders + // OC.generateUrl ensure the index.php (or not) + // We do not want the index.php since we're loading files + // eslint-disable-next-line + __webpack_public_path__ = generateFilePath('spreed', '', 'js/') +} Vue.prototype.t = translate Vue.prototype.n = translatePlural diff --git a/src/test-setup.js b/src/test-setup.js index ab00d4496bc..9b28877df27 100644 --- a/src/test-setup.js +++ b/src/test-setup.js @@ -77,6 +77,7 @@ global.OCP = { disableKeyboardShortcuts: () => false, }, } +global.IS_DESKTOP = false // Work around missing "URL.createObjectURL" (which is used in the code but not // relevant for the tests) in jsdom: https://github.com/jsdom/jsdom/issues/1721 diff --git a/webpack.common.config.js b/webpack.common.config.js new file mode 100644 index 00000000000..3a34b0cedfe --- /dev/null +++ b/webpack.common.config.js @@ -0,0 +1,82 @@ +/* + * @copyright Copyright (c) 2022 Grigorii Shartsev + * + * @author Grigorii Shartsev + * + * @license GNU AGPL version 3 or any later version + * + * 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 . + */ + +const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except') + +const nextcloudWebpackRules = require('@nextcloud/webpack-vue-config/rules') + +// Edit JS rule +nextcloudWebpackRules.RULE_JS.exclude = BabelLoaderExcludeNodeModulesExcept([ + '@nextcloud/event-bus', + 'ansi-regex', + 'color.js', + 'fast-xml-parser', + 'hot-patcher', + 'nextcloud-vue-collections', + 'semver', + 'strip-ansi', + 'tributejs', + 'vue-resize', + 'webdav', +]) + +module.exports = { + module: { + rules: [ + // Reuse @nextcloud/webpack-vue-config/rules + ...Object.values(nextcloudWebpackRules), + + { + /** + * webrtc-adapter main module does no longer provide + * "module.exports", which is expected by some elements using it + * (like "attachmediastream"), so it needs to be added back with + * a plugin. + */ + test: /node_modules[\\/]webrtc-adapter[\\/].*\.js$/, + loader: 'babel-loader', + options: { + plugins: ['add-module-exports'], + presets: [ + /** + * From "add-module-exports" documentation: + * "webpack doesn't perform commonjs transformation for + * codesplitting. Need to set commonjs conversion." + */ + ['@babel/env', { modules: 'commonjs' }], + ], + }, + }, + { + test: /\.wasm$/i, + type: 'asset/resource', + }, + { + test: /\.tflite$/i, + type: 'asset/resource', + }, + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, + ], + }, +} diff --git a/webpack.config.js b/webpack.config.js index 648efa4b7c1..1a60631d674 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,82 +1,40 @@ -const path = require('path') -const webpackConfig = require('@nextcloud/webpack-vue-config') -const webpackRules = require('@nextcloud/webpack-vue-config/rules') -const BabelLoaderExcludeNodeModulesExcept = require('babel-loader-exclude-node-modules-except') +const path = require('node:path') -webpackConfig.entry = { - 'admin-settings': path.join(__dirname, 'src', 'mainAdminSettings.js'), - collections: path.join(__dirname, 'src', 'collections.js'), - main: path.join(__dirname, 'src', 'main.js'), - recording: path.join(__dirname, 'src', 'mainRecording.js'), - 'files-sidebar': [ - path.join(__dirname, 'src', 'mainFilesSidebar.js'), - path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'), - ], - 'public-share-auth-sidebar': path.join(__dirname, 'src', 'mainPublicShareAuthSidebar.js'), - 'public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'), - flow: path.join(__dirname, 'src', 'flow.js'), - dashboard: path.join(__dirname, 'src', 'dashboard.js'), - deck: path.join(__dirname, 'src', 'deck.js'), - maps: path.join(__dirname, 'src', 'maps.js'), -} +const webpack = require('webpack') +const { merge } = require('webpack-merge') -webpackConfig.output.assetModuleFilename = '[name][ext]?v=[contenthash]' +const nextcloudWebpackConfig = require('@nextcloud/webpack-vue-config') -// Edit JS rule -webpackRules.RULE_JS.exclude = BabelLoaderExcludeNodeModulesExcept([ - '@nextcloud/event-bus', - 'ansi-regex', - 'color.js', - 'fast-xml-parser', - 'hot-patcher', - 'nextcloud-vue-collections', - 'semver', - 'strip-ansi', - 'tributejs', - 'vue-resize', - 'webdav', -]) +const commonWebpackConfig = require('./webpack.common.config.js') -// Replaces rules array -webpackConfig.module.rules = Object.values(webpackRules) +// Rules from @nextcloud/webpack-vue-config/rules already added by commonWebpackConfig +nextcloudWebpackConfig.module.rules = [] -webpackConfig.module.rules.push({ - /** - * webrtc-adapter main module does no longer provide - * "module.exports", which is expected by some elements using it - * (like "attachmediastream"), so it needs to be added back with - * a plugin. - */ - test: /node_modules\/webrtc-adapter\/.*\.js$/, - loader: 'babel-loader', - options: { - plugins: ['add-module-exports'], - presets: [ - /** - * From "add-module-exports" documentation: - * "webpack doesn't perform commonjs transformation for - * codesplitting. Need to set commonjs conversion." - */ - ['@babel/env', { modules: 'commonjs' }], +module.exports = merge(nextcloudWebpackConfig, commonWebpackConfig, { + entry: { + 'admin-settings': path.join(__dirname, 'src', 'mainAdminSettings.js'), + collections: path.join(__dirname, 'src', 'collections.js'), + main: path.join(__dirname, 'src', 'main.js'), + recording: path.join(__dirname, 'src', 'mainRecording.js'), + 'files-sidebar': [ + path.join(__dirname, 'src', 'mainFilesSidebar.js'), + path.join(__dirname, 'src', 'mainFilesSidebarLoader.js'), ], + 'public-share-auth-sidebar': path.join(__dirname, 'src', 'mainPublicShareAuthSidebar.js'), + 'public-share-sidebar': path.join(__dirname, 'src', 'mainPublicShareSidebar.js'), + flow: path.join(__dirname, 'src', 'flow.js'), + dashboard: path.join(__dirname, 'src', 'dashboard.js'), + deck: path.join(__dirname, 'src', 'deck.js'), + maps: path.join(__dirname, 'src', 'maps.js'), }, -}) -webpackConfig.module.rules.push({ - test: /\.wasm$/i, - type: 'asset/resource', -}) + output: { + assetModuleFilename: '[name][ext]?v=[contenthash]', + }, -webpackConfig.module.rules.push({ - test: /\.tflite$/i, - type: 'asset/resource', -}) + plugins: [ + new webpack.DefinePlugin({ IS_DESKTOP: false }), + ], -webpackConfig.module.rules.push({ - test: /\.worker\.js$/, - use: { loader: 'worker-loader' }, + cache: true, }) - -webpackConfig.cache = true - -module.exports = webpackConfig From 607977b40bda9d66ad29c7c427adf1ab8661c86f Mon Sep 17 00:00:00 2001 From: Grigorii Shartsev Date: Sun, 12 Mar 2023 06:51:21 +0100 Subject: [PATCH 2/5] feat(desktop): disable browser check on desktop Signed-off-by: Grigorii Shartsev --- src/App.vue | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/App.vue b/src/App.vue index a3a1029c7c1..4112b4eaef3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -430,8 +430,10 @@ export default { }, async mounted() { - // see browserCheck mixin - this.checkBrowser() + if (!IS_DESKTOP) { + // see browserCheck mixin + this.checkBrowser() + } // Check sidebar status in previous sessions if (BrowserStorage.getItem('sidebarOpen') === 'false') { this.$store.dispatch('hideSidebar') From 98f106c3e772160946b974e77b6e25a05a03a440 Mon Sep 17 00:00:00 2001 From: Grigorii Shartsev Date: Sun, 12 Mar 2023 14:34:31 +0100 Subject: [PATCH 3/5] feat(desktop): add desktop support in the VueRouter config Signed-off-by: Grigorii Shartsev --- src/router/router.js | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/router/router.js b/src/router/router.js index 93271470e50..53322abda9c 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -33,21 +33,33 @@ import WelcomeView from '../views/WelcomeView.vue' Vue.use(Router) -const webRootWithIndexPHP = getRootUrl() + '/index.php' -const doesURLContainIndexPHP = window.location.pathname.startsWith(webRootWithIndexPHP) -const base = generateUrl('/', {}, { - noRewrite: doesURLContainIndexPHP, -}) - -export default new Router({ - mode: 'history', +/** + * Generate base url for Talk Web app based on server's root + * + * @return {string} Vue Router base url + */ +function generateTalkWebBasePath() { // if index.php is in the url AND we got this far, then it's working: // let's keep using index.php in the url - base, + const webRootWithIndexPHP = getRootUrl() + '/index.php' + const doesURLContainIndexPHP = window.location.pathname.startsWith(webRootWithIndexPHP) + return generateUrl('/', {}, { + noRewrite: doesURLContainIndexPHP, + }) +} + +export default new Router({ + // On desktop (Electron) app is open via file:// protocol - History API is not available and no base path + mode: !IS_DESKTOP ? 'history' : 'hash', + base: !IS_DESKTOP ? generateTalkWebBasePath() : '', + linkActiveClass: 'active', + routes: [ { path: '/apps/spreed', + // On desktop add index path as root page + alias: IS_DESKTOP ? '/' : undefined, name: 'root', component: WelcomeView, props: true, From de28867a162bd63c522b10e5b60f4120be1e0e13 Mon Sep 17 00:00:00 2001 From: Grigorii Shartsev Date: Sun, 12 Mar 2023 17:04:37 +0100 Subject: [PATCH 4/5] feat(desktop): do not add `Talk -` to the the desktop title Signed-off-by: Grigorii Shartsev --- src/App.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/App.vue b/src/App.vue index 4112b4eaef3..6087cb4c2f8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -564,7 +564,7 @@ export default { } // When a conversation is opened directly, the "Talk - " part is // missing from the title - if (this.defaultPageTitle.indexOf(t('spreed', 'Talk') + ' - ') !== 0) { + if (!IS_DESKTOP && this.defaultPageTitle.indexOf(t('spreed', 'Talk') + ' - ') !== 0) { this.defaultPageTitle = t('spreed', 'Talk') + ' - ' + this.defaultPageTitle } } From c6f16f96fdc29527bf577c907f589a0fc1156f83 Mon Sep 17 00:00:00 2001 From: Grigorii Shartsev Date: Sun, 12 Mar 2023 14:34:50 +0100 Subject: [PATCH 5/5] fix(desktop): remove page reload on session/signaling issues Signed-off-by: Grigorii Shartsev --- src/mixins/sessionIssueHandler.js | 22 ++++++++++++++-------- src/utils/signaling.js | 14 ++++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/mixins/sessionIssueHandler.js b/src/mixins/sessionIssueHandler.js index 30a68deafb3..585150c5839 100644 --- a/src/mixins/sessionIssueHandler.js +++ b/src/mixins/sessionIssueHandler.js @@ -48,21 +48,27 @@ const sessionIssueHandler = { SessionStorage.removeItem('joined_conversation') // Need to delay until next tick, otherwise the PreventUnload is still being triggered // Putting the window in front with the warning and irritating the user - this.$nextTick(() => { - // FIXME: can't use router push as it somehow doesn't clean up - // fully and leads the other instance where "Join here" was clicked - // to redirect to "not found" - window.location = url - }) + if (!IS_DESKTOP) { + this.$nextTick(() => { + // FIXME: can't use router push as it somehow doesn't clean up + // fully and leads the other instance where "Join here" was clicked + // to redirect to "not found" + window.location = generateUrl(url) + }) + } else { + // TODO: DESKTOP: to not hard-code the address? + window.location = `/talk_window/#${url}` + } }, duplicateSessionTriggered() { - this.redirectTo(generateUrl('/apps/spreed/duplicate-session')) + // TODO: DESKTOP: should close the duplicated window instead of redirect + this.redirectTo('/apps/spreed/duplicate-session') }, deletedSessionTriggered() { // workaround: force page refresh to kill stray WebRTC connections - this.redirectTo(generateUrl('/apps/spreed/not-found')) + this.redirectTo('/apps/spreed/not-found') }, }, } diff --git a/src/utils/signaling.js b/src/utils/signaling.js index cf3064145fe..624c490b847 100644 --- a/src/utils/signaling.js +++ b/src/utils/signaling.js @@ -282,10 +282,16 @@ Signaling.Base.prototype.joinCall = function(token, flags, silent) { }.bind(this)) .catch(function() { reject(new Error()) - // Server maintenance, lobby kicked in, or room not found. - // We first redirect to the conversation again and that - // will then show the proper error message to the user. - window.location = generateUrl('call/' + token) + if (!IS_DESKTOP) { + // Server maintenance, lobby kicked in, or room not found. + // We first redirect to the conversation again and that + // will then show the proper error message to the user. + window.location = generateUrl('call/' + token) + } else { + // TODO: Is it true, reload is equal to generateUrl('call/' + token) here? + // Or can we always just reload the page? + window.location.reload() + } }) }) }