diff --git a/.gitignore b/.gitignore index 661998b1d..52f16b2e0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /static/locales yarn-error.log package-lock.json -.idea/ \ No newline at end of file +.idea/ +/build/daemon.ver \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b200a1689..da95738cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ script: - | if [ "$TARGET" == "windows" ]; then docker run --rm \ - --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \ + --env-file <(env | grep -iE 'DEBUG|TARGET|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|GH_|GITHUB_|BT_|AWS_|STRIP|BUILD_') \ -v ${PWD}:/project \ electronuserland/builder:wine \ /bin/bash -c "yarn --link-duplicates --pure-lockfile && yarn build --win --publish onTag"; diff --git a/CHANGELOG.md b/CHANGELOG.md index 33124f8e5..75a1301b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,23 +5,36 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ## [Unreleased] -### Fixed - - * Edit option missing from certain published claims ([#175](https://github.com/lbryio/lbry-desktop/issues/1756)) - ### Added - - * Added 3D file viewer for OBJ & STL file types ([#1558](https://github.com/lbryio/lbry-desktop/pull/1558)) - * Added thumbnail preview on publish page ([#1755](https://github.com/lbryio/lbry-desktop/pull/1755)) - * Wallet Encryption/Decryption user flows ([#1785](https://github.com/lbryio/lbry-desktop/pull/1785)) +* Wallet Encryption/Decryption user flows ([#1785](https://github.com/lbryio/lbry-desktop/pull/1785)) ### Changed - * Rename the Github repo to lbry-desktop ([#1765](https://github.com/lbryio/lbry-desktop/pull/1765)) +### Fixed + + +## [0.23.0] - 2018-07-25 + +### Added + * 3D file viewer for OBJ & STL file types ([#1558](https://github.com/lbryio/lbry-desktop/pull/1558)) + * Thumbnail preview on publish page ([#1755](https://github.com/lbryio/lbry-desktop/pull/1755)) + * Abandoned claim transactions now show in wallet history ([#1769](https://github.com/lbryio/lbry-desktop/pull/1769)) + * Emoji support in the claim description ([#1800](https://github.com/lbryio/lbry-desktop/pull/1800)) + * PDF preview ([#1576](https://github.com/lbryio/lbry-desktop/pull/1576)) + +### Changed + * Upgraded LBRY Protocol to [version 0.20.4](https://github.com/lbryio/lbry/releases/tag/v0.20.4) to assist with download availability and lower CPU usage on idle. + * Upgraded Electron-Builder and Updater to support signing the daemon and improving the auto update process ([#1784](https://github.com/lbryio/lbry-desktop/pull/1784)) + * Channel page now uses caching, faster switching between channels/claims ([#1750](https://github.com/lbryio/lbry-desktop/pull/1750)) * Only show video error modal if you are on the video page & don't retry to play failed videos ([#1768](https://github.com/lbryio/lbry-desktop/pull/1768)) * Actually hide NSFW files if a user chooses to hide NSFW content via the settings page ([#1748](https://github.com/lbryio/lbry-desktop/pull/1748)) * Hide the "Community top bids" section if user chooses to hide NSFW content ([#1760](https://github.com/lbryio/lbry-desktop/pull/1760)) - * Add a more descriptive error message when Shapeshift is unavailable ([#1771](https://github.com/lbryio/lbry-desktop/pull/1771)) + * More descriptive error message when Shapeshift is unavailable ([#1771](https://github.com/lbryio/lbry-desktop/pull/1771)) + * Rename the Github repo to lbry-desktop ([#1765](https://github.com/lbryio/lbry-desktop/pull/1765)) + +### Fixed + * Edit option missing from certain published claims ([#1756](https://github.com/lbryio/lbry-desktop/issues/1756)) + * Fix navigation issue with channels that have more than one page ([#1797](https://github.com/lbryio/lbry-desktop/pull/1797)) ## [0.22.2] - 2018-07-09 diff --git a/build/checkDaemonPlatform.js b/build/checkDaemonPlatform.js deleted file mode 100644 index 4ae40a7c4..000000000 --- a/build/checkDaemonPlatform.js +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable no-console,import/no-extraneous-dependencies,import/no-commonjs */ - -/** - * This script is necessary for checking that the daemon that has been downloaded during the - * yarn installing process is the one for the building target. For example, on Travis the - * Windows package is built on Linux, thus yarn will download the daemon for Linux instead of - * Windows. The script will test that and then download the right daemon for the targeted platform. - */ -const os = require('os'); -const downloadDaemon = require('./downloadDaemon'); - -module.exports = context => { - - let currentPlatform = os.platform(); - if (currentPlatform === 'darwin') currentPlatform = 'macoss'; - if (currentPlatform === 'win32') currentPlatform = 'windows'; - - let buildingPlatformTarget = context.platform.toString(); - if (buildingPlatformTarget === 'mac') buildingPlatformTarget = 'macos'; - - if (buildingPlatformTarget !== currentPlatform) { - console.log( - "\x1b[34minfo\x1b[0m Daemon platform doesn't match target platform. Redownloading the daemon." - ); - - return downloadDaemon(buildingPlatformTarget); - } - return Promise.resolve(); -}; diff --git a/build/downloadDaemon.js b/build/downloadDaemon.js index 11b0b5ba2..4e45f1ad2 100644 --- a/build/downloadDaemon.js +++ b/build/downloadDaemon.js @@ -1,6 +1,6 @@ /* eslint-disable no-console,import/no-extraneous-dependencies,import/no-commonjs */ const path = require('path'); -const fs = require('fs-path'); +const fs = require('fs'); const packageJSON = require('../package.json'); const axios = require('axios'); const decompress = require('decompress'); @@ -11,56 +11,84 @@ const downloadDaemon = targetPlatform => new Promise((resolve, reject) => { const daemonURLTemplate = packageJSON.lbrySettings.lbrynetDaemonUrlTemplate; const daemonVersion = packageJSON.lbrySettings.lbrynetDaemonVersion; - const daemonDir = packageJSON.lbrySettings.lbrynetDaemonDir; - const daemonFileName = packageJSON.lbrySettings.lbrynetDaemonFileName; + const daemonDir = path.join(__dirname,'..',packageJSON.lbrySettings.lbrynetDaemonDir); + let daemonFileName = packageJSON.lbrySettings.lbrynetDaemonFileName; let currentPlatform = os.platform(); - if (currentPlatform === 'darwin') currentPlatform = 'macos'; - if (currentPlatform === 'win32') currentPlatform = 'windows'; - - const daemonPlatform = targetPlatform || currentPlatform; + var daemonPlatform = process.env.TARGET || targetPlatform || currentPlatform; + if (daemonPlatform === 'mac' || daemonPlatform === 'darwin') daemonPlatform = 'macos'; + if (daemonPlatform === 'win32' || daemonPlatform === 'windows') { + daemonPlatform = 'windows'; + daemonFileName = daemonFileName + '.exe'; + } + const daemonFilePath = path.join(daemonDir, daemonFileName); + const daemonVersionPath = path.join(__dirname, 'daemon.ver'); + const tmpZipPath = path.join(__dirname, '..', 'dist', 'daemon.zip'); const daemonURL = daemonURLTemplate .replace(/DAEMONVER/g, daemonVersion) .replace(/OSNAME/g, daemonPlatform); - const tmpZipPath = 'dist/daemon.zip'; - console.log('\x1b[34minfo\x1b[0m Downloading daemon...'); - axios - .request({ - responseType: 'arraybuffer', - url: daemonURL, - method: 'get', - headers: { - 'Content-Type': 'application/zip', - }, - }) - .then( - result => - new Promise((newResolve, newReject) => { - fs.writeFile(tmpZipPath, result.data, error => { - if (error) return newReject(error); - return newResolve(); - }); - }) - ) - .then(() => del(`${daemonDir}/${daemonFileName}*`)) - .then(() => - decompress(tmpZipPath, daemonDir, { - filter: file => - path.basename(file.path).replace(path.extname(file.path), '') === daemonFileName, + // If a daemon and daemon.ver exists, check to see if it matches the current daemon version + const hasDaemonDownloaded = fs.existsSync(daemonFilePath); + const hasDaemonVersion = fs.existsSync(daemonVersionPath); + let downloadedDaemonVersion; + if (hasDaemonVersion) { + downloadedDaemonVersion = fs.readFileSync(daemonVersionPath, "utf8"); + } + + if (hasDaemonDownloaded && hasDaemonVersion && downloadedDaemonVersion === daemonVersion) { + console.log('\x1b[34minfo\x1b[0m Daemon already downloaded'); + resolve('Done'); + return; + } else { + console.log('\x1b[34minfo\x1b[0m Downloading daemon...'); + axios + .request({ + responseType: 'arraybuffer', + url: daemonURL, + method: 'get', + headers: { + 'Content-Type': 'application/zip', + }, }) - ) - .then(() => { - console.log('\x1b[32msuccess\x1b[0m Daemon downloaded!'); - resolve(true); - }) - .catch(error => { - console.error( - `\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m` - ); - reject(error); - }); + .then( + result => + new Promise((newResolve, newReject) => { + const distPath = path.join(__dirname, '..', 'dist'); + const hasDistFolder = fs.existsSync(distPath); + + if (!hasDistFolder) { + fs.mkdirSync(distPath); + } + + fs.writeFile(tmpZipPath, result.data, error => { + if (error) return newReject(error); + return newResolve(); + }); + }) + ) + .then(() => del(`${daemonFilePath}*`)) + .then(() => decompress(tmpZipPath, daemonDir, { + filter: file => + path.basename(file.path) === daemonFileName, + })) + .then(() => { + console.log('\x1b[32msuccess\x1b[0m Daemon downloaded!'); + if (hasDaemonVersion) { + del(daemonVersionPath); + } + + fs.writeFileSync(daemonVersionPath, daemonVersion, "utf8") + resolve('Done'); + }) + .catch(error => { + console.error( + `\x1b[31merror\x1b[0m Daemon download failed due to: \x1b[35m${error}\x1b[0m` + ); + reject(error); + }) + }; }); module.exports = downloadDaemon; diff --git a/electron-builder.json b/electron-builder.json index 6b13458f3..abdd40022 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -73,6 +73,5 @@ } ] }, - "artifactName": "${productName}_${version}.${ext}", - "beforeBuild": "./build/checkDaemonPlatform.js" + "artifactName": "${productName}_${version}.${ext}" } diff --git a/package.json b/package.json index d1642df28..0c2724d47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "LBRY", - "version": "0.22.2", + "version": "0.23.0", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "keywords": [ "lbry" @@ -31,7 +31,7 @@ "release": "yarn compile && electron-builder build", "precommit": "lint-staged", "preinstall": "yarn cache clean lbry-redux", - "postinstall": "electron-builder install-app-deps & node build/downloadDaemon.js" + "postinstall": "electron-builder install-app-deps && node build/downloadDaemon.js" }, "dependencies": { "bluebird": "^3.5.1", @@ -71,6 +71,7 @@ "redux-persist-transform-filter": "0.0.16", "redux-thunk": "^2.2.0", "remark": "^9.0.0", + "remark-emoji": "^2.0.1", "remark-react": "^4.0.3", "render-media": "^3.1.0", "reselect": "^3.0.0", diff --git a/src/renderer/component/common/markdown-preview.js b/src/renderer/component/common/markdown-preview.jsx similarity index 83% rename from src/renderer/component/common/markdown-preview.js rename to src/renderer/component/common/markdown-preview.jsx index 2c28163aa..b00ae7025 100644 --- a/src/renderer/component/common/markdown-preview.js +++ b/src/renderer/component/common/markdown-preview.jsx @@ -2,6 +2,7 @@ import * as React from 'react'; import remark from 'remark'; import reactRenderer from 'remark-react'; +import remarkEmoji from 'remark-emoji'; import ExternalLink from 'component/externalLink'; import defaultSchema from 'hast-util-sanitize/lib/github.json'; @@ -16,7 +17,11 @@ type MarkdownProps = { promptLinks?: boolean, }; -const SimpleLink = ({ href, title, children }) => ({children}); +const SimpleLink = ({ href, title, children }) => ( + + {children} + +); const MarkdownPreview = (props: MarkdownProps) => { const { content, externalLinks, promptLinks } = props; @@ -30,6 +35,7 @@ const MarkdownPreview = (props: MarkdownProps) => {
{ remark() + .use(remarkEmoji) .use(reactRenderer, remarkOptions) .processSync(content).contents } diff --git a/src/renderer/component/page/view.jsx b/src/renderer/component/page/view.jsx index ce21e27d7..34998d528 100644 --- a/src/renderer/component/page/view.jsx +++ b/src/renderer/component/page/view.jsx @@ -39,8 +39,16 @@ class Page extends React.PureComponent { componentDidUpdate(prevProps: Props) { const { loading } = this.props; + const { showLoader } = this.state; + if (!this.loaderTimeout && !prevProps.loading && loading) { this.beginLoadingTimeout(); + } else if (!loading && this.loaderTimeout) { + clearTimeout(this.loaderTimeout); + + if (showLoader) { + this.removeLoader(); + } } } @@ -56,6 +64,10 @@ class Page extends React.PureComponent { }, LOADER_TIMEOUT); } + removeLoader() { + this.setState({ showLoader: false }); + } + loaderTimeout: ?TimeoutID; render() { diff --git a/src/renderer/component/uriIndicator/view.jsx b/src/renderer/component/uriIndicator/view.jsx index 80f2f9ec2..d1b1d90d4 100644 --- a/src/renderer/component/uriIndicator/view.jsx +++ b/src/renderer/component/uriIndicator/view.jsx @@ -80,7 +80,7 @@ class UriIndicator extends React.PureComponent { noPadding className="btn--uri-indicator" navigate="/show" - navigateParams={{ uri: channelLink }} + navigateParams={{ uri: channelLink, page: 1 }} > {inner} diff --git a/src/renderer/component/viewers/threeViewer/index.jsx b/src/renderer/component/viewers/threeViewer/index.jsx index 63dc38622..0ceea09d5 100644 --- a/src/renderer/component/viewers/threeViewer/index.jsx +++ b/src/renderer/component/viewers/threeViewer/index.jsx @@ -4,6 +4,7 @@ import LoadingScreen from 'component/common/loading-screen'; // ThreeJS import * as THREE from './internal/three'; import detectWebGL from './internal/detector'; +import ThreeGrid from './internal/grid'; import ThreeScene from './internal/scene'; import ThreeLoader from './internal/loader'; import ThreeRenderer from './internal/renderer'; @@ -77,6 +78,12 @@ class ThreeViewer extends React.PureComponent { window.removeEventListener('resize', this.handleResize, false); } + transformGroup(group) { + this.fitMeshToCamera(group); + this.createWireFrame(group); + this.updateControlsTarget(group.position); + } + createOrbitControls(camera, canvas) { const { autoRotate } = this.props; const controls = new THREE.OrbitControls(camera, canvas); @@ -87,6 +94,7 @@ class ThreeViewer extends React.PureComponent { controls.minDistance = 1; controls.maxDistance = 50; controls.autoRotate = autoRotate; + controls.enablePan = false; return controls; } @@ -114,32 +122,6 @@ class ThreeViewer extends React.PureComponent { group.add(this.wireframe); } - createMesh(geometry) { - const material = new THREE.MeshPhongMaterial({ - opacity: 1, - transparent: true, - depthWrite: true, - vertexColors: THREE.FaceColors, - // Positive value pushes polygon further away - polygonOffsetFactor: 1, - polygonOffsetUnits: 1, - }); - - // Set material color - material.color.set(this.materialColors.green); - - const mesh = new THREE.Mesh(geometry, material); - - // Assign name - mesh.name = 'objectGroup'; - - this.scene.add(mesh); - this.fitMeshToCamera(mesh); - this.createWireFrame(mesh); - this.updateControlsTarget(mesh.position); - return mesh; - } - toggleWireFrame(show = false) { this.wireframe.opacity = show ? 1 : 0; this.mesh.material.opacity = show ? 0 : 1; @@ -151,7 +133,7 @@ class ThreeViewer extends React.PureComponent { group.traverse(child => { if (child instanceof THREE.Mesh) { - const box = new THREE.Box3().setFromObject(group); + const box = new THREE.Box3().setFromObject(child); // Max max.x = box.max.x > max.x ? box.max.x : max.x; max.y = box.max.y > max.y ? box.max.y : max.y; @@ -165,12 +147,18 @@ class ThreeViewer extends React.PureComponent { const meshY = Math.abs(max.y - min.y); const meshX = Math.abs(max.x - min.x); - const scaleFactor = 15 / Math.max(meshX, meshY); + + const scaleFactor = 10 / Math.max(meshX, meshY); group.scale.set(scaleFactor, scaleFactor, scaleFactor); group.position.setY((meshY / 2) * scaleFactor); + + // Reset object position + const box = new THREE.Box3().setFromObject(group); + box.getCenter(group.position); + group.position.multiplyScalar(-1); - group.position.setY((meshY * scaleFactor) / 2); + group.position.setY(group.position.y + meshY * scaleFactor); } startLoader() { @@ -217,12 +205,52 @@ class ThreeViewer extends React.PureComponent { this.controls.update(); } - renderModel(fileType, data) { + renderStl(data) { const geometry = this.createGeometry(data); - this.mesh = this.createMesh(geometry); + const group = new THREE.Mesh(geometry, this.material); + // Assign name + group.name = 'objectGroup'; + this.scene.add(group); + this.transformGroup(group); + this.mesh = group; + } + + renderObj(event) { + const mesh = event.detail.loaderRootNode; + const group = new THREE.Group(); + group.name = 'objGroup'; + + // Assign new material + mesh.traverse(child => { + if (child instanceof THREE.Mesh) { + // Get geometry from child + const geometry = new THREE.Geometry(); + geometry.fromBufferGeometry(child.geometry); + // Create and regroup inner objects + const innerObj = new THREE.Mesh(geometry, this.material); + group.add(innerObj); + } + }); + + this.scene.add(group); + this.transformGroup(group); + this.mesh = group; + } + + renderModel(fileType, parsedData) { + const renderTypes = { + stl: data => this.renderStl(data), + obj: data => this.renderObj(data), + }; + + if (renderTypes[fileType]) { + renderTypes[fileType](parsedData); + } } renderScene() { + const { gridColor, centerLineColor } = this.theme; + this.renderer = ThreeRenderer({ antialias: true, shadowMap: true, @@ -230,20 +258,40 @@ class ThreeViewer extends React.PureComponent { this.scene = ThreeScene({ showFog: true, - showGrid: true, ...this.theme, }); const viewer = this.viewer.current; const canvas = this.renderer.domElement; const { offsetWidth: width, offsetHeight: height } = viewer; + + // Grid + this.grid = ThreeGrid({ size: 100, gridColor, centerLineColor }); + this.scene.add(this.grid); + // Camera this.camera = new THREE.PerspectiveCamera(80, width / height, 0.1, 1000); this.camera.position.set(-9.5, 14, 11); + // Controls this.controls = this.createOrbitControls(this.camera, canvas); + // Set viewer size this.renderer.setSize(width, height); + + // Create model material + this.material = new THREE.MeshPhongMaterial({ + opacity: 1, + transparent: true, + // depthWrite: true, + vertexColors: THREE.FaceColors, + // Positive value pushes polygon further away + // polygonOffsetFactor: 1, + // polygonOffsetUnits: 1, + }); + // Set material color + this.material.color.set(this.materialColors.green); + // Load file and render mesh this.startLoader(); diff --git a/src/renderer/component/viewers/threeViewer/internal/grid.js b/src/renderer/component/viewers/threeViewer/internal/grid.js new file mode 100644 index 000000000..0793c6913 --- /dev/null +++ b/src/renderer/component/viewers/threeViewer/internal/grid.js @@ -0,0 +1,13 @@ +import { GridHelper, Color } from './three'; + +const ThreeGrid = ({ size, gridColor, centerLineColor }) => { + const divisions = size / 2; + const grid = new GridHelper(size, divisions, new Color(centerLineColor), new Color(gridColor)); + + grid.material.opacity = 0.4; + grid.material.transparent = true; + + return grid; +}; + +export default ThreeGrid; diff --git a/src/renderer/component/viewers/threeViewer/internal/loader.js b/src/renderer/component/viewers/threeViewer/internal/loader.js index dc4ce7482..d69a93344 100644 --- a/src/renderer/component/viewers/threeViewer/internal/loader.js +++ b/src/renderer/component/viewers/threeViewer/internal/loader.js @@ -1,4 +1,4 @@ -import { LoadingManager, STLLoader, OBJLoader } from './three'; +import { LoadingManager, STLLoader, OBJLoader2 } from './three'; const Manager = ({ onLoad, onStart, onError }) => { const manager = new LoadingManager(); @@ -12,7 +12,7 @@ const Manager = ({ onLoad, onStart, onError }) => { const Loader = (fileType, manager) => { const fileTypes = { stl: () => new STLLoader(manager), - obj: () => new OBJLoader(manager), + obj: () => new OBJLoader2(manager), }; return fileTypes[fileType] ? fileTypes[fileType]() : null; }; diff --git a/src/renderer/component/viewers/threeViewer/internal/scene.js b/src/renderer/component/viewers/threeViewer/internal/scene.js index f858c8832..a22c4a996 100644 --- a/src/renderer/component/viewers/threeViewer/internal/scene.js +++ b/src/renderer/component/viewers/threeViewer/internal/scene.js @@ -1,18 +1,5 @@ import * as THREE from './three'; -const addGrid = (scene, { gridColor, centerLineColor, size }) => { - const divisions = size / 2; - const grid = new THREE.GridHelper( - size, - divisions, - new THREE.Color(centerLineColor), - new THREE.Color(gridColor) - ); - grid.material.opacity = 0.4; - grid.material.transparent = true; - scene.add(grid); -}; - const addLights = (scene, color, groundColor) => { // Light color const lightColor = new THREE.Color(color); @@ -30,7 +17,7 @@ const addLights = (scene, color, groundColor) => { scene.add(shadowLight); }; -const Scene = ({ backgroundColor, groundColor, showFog, showGrid, gridColor, centerLineColor }) => { +const Scene = ({ backgroundColor, groundColor, showFog }) => { // Convert color const bgColor = new THREE.Color(backgroundColor); // New scene @@ -39,17 +26,8 @@ const Scene = ({ backgroundColor, groundColor, showFog, showGrid, gridColor, cen scene.background = bgColor; // Fog effect scene.fog = showFog === true ? new THREE.Fog(bgColor, 1, 95) : null; - // Add grid - if (showGrid) { - addGrid(scene, { - size: 100, - gridColor, - centerLineColor, - }); - } // Add basic lights addLights(scene, '#FFFFFF', groundColor); - // Return new three scene return scene; }; diff --git a/src/renderer/component/viewers/threeViewer/internal/three.js b/src/renderer/component/viewers/threeViewer/internal/three.js index 6cb0051d0..4e9f9bdb1 100644 --- a/src/renderer/component/viewers/threeViewer/internal/three.js +++ b/src/renderer/component/viewers/threeViewer/internal/three.js @@ -4,7 +4,8 @@ import * as THREE from 'three'; // Fix: https://github.com/mrdoob/three.js/issues/9562#issuecomment-383390251 global.THREE = THREE; require('three/examples/js/controls/OrbitControls'); -require('three/examples/js/loaders/OBJLoader'); +require('three/examples/js/loaders/LoaderSupport'); +require('three/examples/js/loaders/OBJLoader2'); require('three/examples/js/loaders/STLLoader'); module.exports = global.THREE; diff --git a/src/renderer/modal/modalIncompatibleDaemon/view.jsx b/src/renderer/modal/modalIncompatibleDaemon/view.jsx index bf015aec0..b7b7100fd 100644 --- a/src/renderer/modal/modalIncompatibleDaemon/view.jsx +++ b/src/renderer/modal/modalIncompatibleDaemon/view.jsx @@ -23,7 +23,7 @@ class ModalIncompatibleDaemon extends React.PureComponent { onAborted={quit} > {__( - 'This browser is running with an incompatible version of the LBRY protocol, please close the LBRY app and rerun the installation package to repair it' + 'This browser is running with an incompatible version of the LBRY protocol, please close the LBRY app and rerun the installation package to repair it. ' )}