diff --git a/CHANGELOG.md b/CHANGELOG.md index 2542a60af..31896f7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,42 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.110.0] - Not released +## [1.113] - Not released +### Added +- Email verification support. If verification is supported by server, the new + field (Verification code) is appear in Sign Up and Profile forms. When user + sets up or updates email address, they should receive verification code on it + and enter that code to the form. + +## [1.112] - 2022-11-25 +### Added +- Group administrators can now block users in their managed groups. A blocked + user cannot post to the group, but can read and comment if the group is not + private. +- Video attachments now have a player, when browser supports them +- YouTube shorts now supported in media viewer + +### Fixed +- No refresh needed to view private users and groups after subscription approved +- No refresh needed to interact with new subscription requests +- Hidden comment class name updated to avoid interference with Firefox builtin extension style + +## [1.111.2] - 2022-09-23 +### Fixed +- Fix broken PhotoSwipe icons + +## [1.111.1] - 2022-09-08 +### Fixed +- Restore Vazir font (new css-loader didn't load it in 1.111.0) + +## [1.111.0] - 2022-09-07 +### Added +- Instagram Reels are supported by native previews. + First contribution by [Mohammad Jafari](https://github.com/MMDJafari/). Thanks! +- It is now possible to hide posts by hashtags! Also, the underlying algorithm + allows to add other types of hiding criteria in the future. + +## [1.110.0] - 2022-06-29 ### Fixed - The erroneous "Remove from" items has been removed from the post's "More" menu - Fixed domain-name in donate link diff --git a/README.md b/README.md index 568447aa8..75253f245 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FFreeFeed%2Ffreefeed-react-client.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FFreeFeed%2Ffreefeed-react-client?ref=badge_shield) +[Node.js](https://nodejs.org) 14 or 16 is supported. + We use [yarn](https://yarnpkg.com/) as dependency manager (instead of npm) so you need to install it and run `yarn` after downloading this code. If you're using Windows, you should install developer tools by using `npm install --global --production windows-build-tools` from an elevated PowerShell or CMD.exe (run as Administrator). ## Starting Development Server with Hot-Reload diff --git a/config/default.js b/config/default.js index e10a78296..39efacdd6 100644 --- a/config/default.js +++ b/config/default.js @@ -70,7 +70,7 @@ export default { readMoreStyle: 'modern', homeFeedSort: ACTIVITY, homeFeedMode: HOMEFEED_MODE_CLASSIC, - homefeed: { hideUsers: [] }, + homefeed: { hideUsers: [], hideTags: [] }, hidesInNonHomeFeeds: false, pinnedGroups: [], hideUnreadNotifications: false, diff --git a/package.json b/package.json index 8fb487410..904b4bd2d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "reactive-pepyatka", - "version": "1.110.0", + "version": "1.113.0", "description": "", "main": "index.js", "dependencies": { @@ -9,9 +9,9 @@ "@fortawesome/free-solid-svg-icons": "~5.15.4", "@sentry/react": "~6.19.7", "autotrack": "~2.4.1", - "classnames": "~2.3.1", + "classnames": "~2.3.2", "custom-event": "~1.0.1", - "date-fns": "~2.28.0", + "date-fns": "~2.29.3", "debug": "~4.3.4", "filesize": "~8.0.7", "final-form": "~4.20.7", @@ -20,7 +20,7 @@ "keycode-js": "~3.1.0", "local-storage-fallback": "~4.1.2", "lodash": "~4.17.21", - "lru-cache": "~7.10.2", + "lru-cache": "~7.14.1", "memoize-one": "~6.0.0", "mousetrap": "~1.6.5", "porter-stemmer": "~0.9.1", @@ -33,40 +33,40 @@ "react-helmet": "~6.1.0", "react-photoswipe": "~1.3.0", "react-portal": "~4.2.2", - "react-redux": "~7.2.8", + "react-redux": "~7.2.9", "react-router": "~3.2.6", "react-router-redux": "~4.0.8", "react-select": "~1.2.1", "react-sortablejs": "~2.0.11", "react-textarea-autosize": "~8.3.4", - "recharts": "~2.1.12", + "recharts": "~2.1.16", "redux": "~4.1.2", "snarkdown": "~2.0.0", "social-text-tokenizer": "~2.2.0", "socket.io-client": "~2.3.1", "tabbable": "~5.2.1", - "ua-parser-js": "~1.0.2", + "ua-parser-js": "~1.0.32", "use-subscription": "~1.5.1", "validator": "~13.7.0", "vazir-font": "~30.1.0", "whatwg-fetch": "~3.6.2" }, "devDependencies": { - "@babel/core": "~7.18.6", - "@babel/eslint-parser": "~7.18.2", + "@babel/core": "~7.19.6", + "@babel/eslint-parser": "~7.19.1", "@babel/plugin-proposal-class-properties": "~7.18.6", "@babel/plugin-proposal-do-expressions": "~7.18.6", "@babel/plugin-syntax-class-properties": "~7.12.13", "@babel/plugin-transform-modules-commonjs": "~7.18.6", - "@babel/plugin-transform-react-constant-elements": "~7.18.6", + "@babel/plugin-transform-react-constant-elements": "~7.18.12", "@babel/plugin-transform-react-inline-elements": "~7.18.6", - "@babel/plugin-transform-runtime": "~7.18.6", - "@babel/preset-env": "~7.18.6", + "@babel/plugin-transform-runtime": "~7.19.6", + "@babel/preset-env": "~7.19.4", "@babel/preset-react": "~7.18.6", - "@babel/register": "~7.18.6", - "@babel/runtime": "~7.18.6", + "@babel/register": "~7.18.9", + "@babel/runtime": "~7.19.4", "@gfx/zopfli": "~1.0.15", - "@testing-library/jest-dom": "~5.16.4", + "@testing-library/jest-dom": "~5.16.5", "@testing-library/react": "~12.1.5", "@testing-library/react-hooks": "~7.0.2", "@testing-library/user-event": "~14.1.1", @@ -74,67 +74,67 @@ "babel-loader": "~8.2.5", "babel-plugin-lodash": "~3.3.4", "babel-plugin-transform-react-remove-prop-types": "~0.4.24", - "compression-webpack-plugin": "~9.2.0", - "copy-webpack-plugin": "~10.2.4", - "core-js": "~3.23.3", + "compression-webpack-plugin": "~10.0.0", + "copy-webpack-plugin": "~11.0.0", + "core-js": "~3.25.5", "cross-env": "~7.0.3", - "css-loader": "~5.2.7", - "css-minimizer-webpack-plugin": "~3.4.1", - "eslint": "~8.18.0", + "css-loader": "~6.7.2", + "css-minimizer-webpack-plugin": "~4.0.0", + "eslint": "~8.23.1", "eslint-config-prettier": "~8.5.0", "eslint-plugin-babel": "~5.3.1", "eslint-plugin-import": "~2.26.0", "eslint-plugin-lodash": "~7.4.0", - "eslint-plugin-prettier": "~4.1.0", - "eslint-plugin-promise": "~6.0.0", - "eslint-plugin-react": "~7.30.1", + "eslint-plugin-prettier": "~4.2.1", + "eslint-plugin-promise": "~6.0.1", + "eslint-plugin-react": "~7.31.11", "eslint-plugin-react-hooks": "~4.6.0", - "eslint-plugin-unicorn": "~42.0.0", + "eslint-plugin-unicorn": "~43.0.2", "eslint-plugin-you-dont-need-lodash-underscore": "~6.12.0", "eslint-webpack-plugin": "~3.2.0", "file-loader": "~6.2.0", "html-webpack-plugin": "~5.5.0", - "husky": "~8.0.1", + "husky": "~8.0.2", "identity-obj-proxy": "~3.0.0", "jest": "~27.5.1", - "jest-canvas-mock": "~2.3.1", + "jest-canvas-mock": "~2.4.0", "lint-staged": "~12.4.3", "mini-css-extract-plugin": "~2.6.1", - "mocha": "~9.2.2", + "mocha": "~10.0.0", "mochapack": "~2.1.4", "node-noop": "~1.0.0", - "node-sass": "~7.0.1", + "node-sass": "~7.0.3", "npm-run-all": "~4.1.5", "null-loader": "~4.0.1", "prettier": "~2.7.1", "pug": "~3.0.2", "pug-loader": "~2.4.0", "querystring": "~0.2.1", - "react-hot-loader": "~4.13.0", + "react-hot-loader": "~4.13.1", "react-markdown-loader": "~1.3.1", "react-test-renderer": "~17.0.2", - "regenerator-runtime": "~0.13.9", + "regenerator-runtime": "~0.13.11", "resolve-url-loader": "~5.0.0", "rimraf": "~3.0.2", - "sass-loader": "~12.6.0", + "sass-loader": "~13.0.2", "sinon": "~13.0.2", "style-loader": "~3.3.1", - "stylelint": "~14.9.1", - "stylelint-config-prettier": "~9.0.3", - "stylelint-config-standard-scss": "~3.0.0", + "stylelint": "~14.11.0", + "stylelint-config-prettier": "~9.0.4", + "stylelint-config-standard-scss": "~5.0.0", "stylelint-prettier": "~2.0.0", - "stylelint-scss": "~4.2.0", - "terser-webpack-plugin": "~5.3.3", - "unexpected": "~12.0.4", + "stylelint-scss": "~4.3.0", + "terser-webpack-plugin": "~5.3.6", + "unexpected": "~13.0.1", "unexpected-react": "~6.0.2", "unexpected-sinon": "~11.1.0", "url": "~0.11.0", - "webpack": "~5.73.0", - "webpack-bundle-analyzer": "~4.5.0", + "webpack": "~5.74.0", + "webpack-bundle-analyzer": "~4.6.1", "webpack-cli": "~4.10.0", - "webpack-dev-server": "~4.9.2", + "webpack-dev-server": "~4.11.1", "webpack-node-externals": "~3.0.0", - "webpack-version-file": "~0.1.6", + "webpack-version-file": "~0.1.7", "worker-loader": "~3.0.8" }, "resolutions": { @@ -167,5 +167,5 @@ "url": "https://github.com/FreeFeed/freefeed-react-client.git" }, "license": "MIT", - "packageManager": "yarn@3.2.1" + "packageManager": "yarn@3.3.0" } diff --git a/src/components/app-updated.jsx b/src/components/app-updated.jsx index c5e2dfa9f..0679e4495 100644 --- a/src/components/app-updated.jsx +++ b/src/components/app-updated.jsx @@ -18,7 +18,7 @@ export function AppUpdated() { return (
- There’s a new update for {CONFIG.siteTitle} available!{' '} + There’s an update for {CONFIG.siteTitle}!{' '} Refresh the page {' '} diff --git a/src/components/block-in-group-form.jsx b/src/components/block-in-group-form.jsx new file mode 100644 index 000000000..482aca6ab --- /dev/null +++ b/src/components/block-in-group-form.jsx @@ -0,0 +1,42 @@ +import { useCallback, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { blockUserInGroup } from '../redux/action-creators'; + +export function BlockInGroupForm({ groupName }) { + const dispatch = useDispatch(); + const status = useSelector((state) => state.blockUserInGroupStatus); + + const [text, setText] = useState(''); + const canSubmit = !!text.trim() && !status.loading; + const onInput = useCallback((e) => setText(e.target.value), []); + const onSubmit = useCallback( + (e) => (e.preventDefault(), dispatch(blockUserInGroup(groupName, text.trim()))), + [dispatch, groupName, text], + ); + + useEffect(() => status.success && setText(''), [status.success]); + + return ( +
+

+ + + + +

+ {status.error && ( +

+ {status.errorText} +

+ )} +
+ ); +} diff --git a/src/components/email-verification-subform.jsx b/src/components/email-verification-subform.jsx new file mode 100644 index 000000000..0ffa07dcf --- /dev/null +++ b/src/components/email-verification-subform.jsx @@ -0,0 +1,70 @@ +import { useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { sendVerificationCode } from '../redux/action-creators'; +import { groupErrClass } from './form-utils'; +import { useServerInfo } from './hooks/server-info'; + +export function EmailVerificationSubform({ emailField, codeField, create = false }) { + const dispatch = useDispatch(); + const sendStatus = useSelector((state) => state.sendVerificationCodeStatus); + const [serverInfo, serverInfoStatus] = useServerInfo(); + + const verificationEnabled = serverInfoStatus.success && serverInfo.emailVerificationEnabled; + + const shouldVerify = Boolean( + verificationEnabled && emailField.meta.dirty && !emailField.meta.invalid, + ); + + const [lastSentTo, setLastSentTo] = useState(''); + + const sendEmailCode = useCallback(() => { + dispatch(sendVerificationCode(emailField.input.value, create ? 'sign-up' : 'update')); + setLastSentTo(emailField.input.value); + }, [create, dispatch, emailField.input.value]); + + return ( + shouldVerify && ( +
+ +

+ To confirm {create ? 'your' : 'updated'} email address, please click{' '} + {' '} + and enter the code that we will send to {emailField.input.value}. +

+ {sendStatus.loading && ( +

+ Sending code to {lastSentTo}... +

+ )} + {sendStatus.success && ( +

+ Code was sent to {lastSentTo}, please check your mailbox (and, + probably, the Spam folder) +

+ )} + {sendStatus.error && ( +

+ Error sending to {lastSentTo}: {sendStatus.errorText} +

+ )} +

+ +

+
+ ) + ); +} diff --git a/src/components/feed.jsx b/src/components/feed.jsx index 60403ee09..27cf5f193 100644 --- a/src/components/feed.jsx +++ b/src/components/feed.jsx @@ -109,7 +109,7 @@ class Feed extends PureComponent { } } -const postIsHidden = (post) => !!(post.isHidden || post.hiddenByNames); +const postIsHidden = (post) => !!(post.isHidden || post.hiddenByCriteria); export default connect( (state) => { @@ -151,14 +151,17 @@ function FeedEntry({ post, section, ...props }) { const onPostUnmount = useCallback((offset) => setHideLinkTopOffset(offset), []); const isRecentlyHidden = - props.separateHiddenEntries && (post.isHidden || post.hiddenByNames) && section === 'visible'; + props.separateHiddenEntries && + (post.isHidden || post.hiddenByCriteria) && + section === 'visible'; return isRecentlyHidden ? ( ) : (

- © FreeFeed 1.110.0 (Not released) + © FreeFeed 1.113.0 (Not released)
About {' | '} diff --git a/src/components/link-preview/instagram.jsx b/src/components/link-preview/instagram.jsx index 490db3d6a..b75524e37 100644 --- a/src/components/link-preview/instagram.jsx +++ b/src/components/link-preview/instagram.jsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import * as aspectRatio from './helpers/size-cache'; import FoldableContent from './helpers/foldable-content'; -const INSTAGRAM_RE = /^https?:\/\/(?:www\.)?instagram\.com\/(?:p|tv)\/([\w-]+)/i; +const INSTAGRAM_RE = /^https?:\/\/(?:www\.)?instagram\.com\/(?:p|tv|reel)\/([\w-]+)/i; export function canShowURL(url) { return INSTAGRAM_RE.test(url); diff --git a/src/components/link-preview/video.jsx b/src/components/link-preview/video.jsx index 8adef7ff6..a58704eea 100644 --- a/src/components/link-preview/video.jsx +++ b/src/components/link-preview/video.jsx @@ -10,7 +10,7 @@ import cachedFetch from './helpers/cached-fetch'; import * as aspectRatio from './helpers/size-cache'; const YOUTUBE_VIDEO_RE = - /^https?:\/\/(?:www\.|m\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?(?:v=|.+&v=)))([\w-]+)/i; + /^https?:\/\/(?:www\.|m\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|shorts\/|v\/|watch\?(?:v=|.+&v=)))([\w-]+)/i; const VIMEO_VIDEO_RE = /^https?:\/\/vimeo\.com\/(\d+)(?:\/([a-z\d]+))?/i; const COUB_VIDEO_RE = /^https?:\/\/coub\.com\/view\/([a-z\d]+)/i; const IMGUR_VIDEO_RE = /^https?:\/\/i\.imgur\.com\/([a-z\d]+)\.(gifv|mp4)/i; @@ -164,7 +164,7 @@ function getVideoId(url) { function getDefaultAspectRatio(url) { if (YOUTUBE_VIDEO_RE.test(url)) { - return 9 / 16; + return isYoutubeShort(url) ? 16 / 9 : 9 / 16; } if (VIMEO_VIDEO_RE.test(url)) { return 9 / 16; @@ -207,7 +207,7 @@ function getYoutubeVideoInfo(url, withoutAutoplay) { return { byline: `Open on YouTube`, - aspectRatio: aspectRatio.set(url, 9 / 16), + aspectRatio: aspectRatio.set(url, isYoutubeShort(url) ? 16 / 9 : 9 / 16), previewURL: `https://img.youtube.com/vi/${videoID}/hqdefault.jpg`, playerURL: `https://www.youtube.com/embed/${videoID}?rel=0&fs=1${ withoutAutoplay ? '' : '&autoplay=1' @@ -387,3 +387,7 @@ function loadImage(url) { img.src = url; }); } + +function isYoutubeShort(url) { + return url.includes('/shorts/'); +} diff --git a/src/components/manage-subscribers.jsx b/src/components/manage-subscribers.jsx index 6d95c58df..eb8bcdf82 100644 --- a/src/components/manage-subscribers.jsx +++ b/src/components/manage-subscribers.jsx @@ -1,89 +1,147 @@ -import { PureComponent } from 'react'; -import { connect } from 'react-redux'; +import { useCallback, useEffect, useMemo } from 'react'; +import { connect, useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router'; import _ from 'lodash'; -import { unsubscribeFromGroup, makeGroupAdmin, unadminGroupAdmin } from '../redux/action-creators'; +import { + unsubscribeFromGroup, + makeGroupAdmin, + unadminGroupAdmin, + getGroupBlockedUsers, + unblockUserInGroup, +} from '../redux/action-creators'; import { tileUserListFactory, WITH_REMOVE_AND_MAKE_ADMIN_HANDLES, WITH_REMOVE_ADMIN_RIGHTS, } from './tile-user-list'; +import { BlockInGroupForm } from './block-in-group-form'; const SubsList = tileUserListFactory({ type: WITH_REMOVE_AND_MAKE_ADMIN_HANDLES }); const AdminsList = tileUserListFactory({ type: WITH_REMOVE_ADMIN_RIGHTS }); +const BlockedList = tileUserListFactory(); -class ManageSubscribersHandler extends PureComponent { - handleRemove = (username) => { - this.props.unsubscribeFromGroup(this.props.groupName, username); - }; +function ManageSubscribersHandler({ user: currentUser, groupName, amIAdmin, ...props }) { + const dispatch = useDispatch(); + const blockedUsersStatus = useSelector((state) => state.groupBlockedUsersStatus); + const blockedUsersIds = useSelector((state) => state.groupBlockedUsers); + const allUsers = useSelector((state) => state.users); - handleMakeAdmin = (user) => { - this.props.makeGroupAdmin(this.props.groupName, user); - }; + const blockedUsers = useMemo( + () => blockedUsersIds.map((id) => allUsers[id]), + [allUsers, blockedUsersIds], + ); - handleRemoveAdminRights = (user) => { - const isItMe = this.props.user.id === user.id; - this.props.unadminGroupAdmin(this.props.groupName, user, isItMe); - }; + const handleRemove = useCallback( + (username) => dispatch(unsubscribeFromGroup(groupName, username)), + [dispatch, groupName], + ); + const handleMakeAdmin = useCallback( + (user) => dispatch(makeGroupAdmin(groupName, user)), + [dispatch, groupName], + ); + const handleRemoveAdminRights = useCallback( + (user) => { + const isItMe = currentUser.id === user.id; + dispatch(unadminGroupAdmin(groupName, user, isItMe)); + }, + [currentUser.id, dispatch, groupName], + ); - render() { - const { props } = this; + useEffect( + () => amIAdmin && blockedUsersStatus.initial && dispatch(getGroupBlockedUsers(groupName)), + [amIAdmin, blockedUsersStatus.initial, dispatch, groupName], + ); - return ( -

-
- {props.boxHeader} -
-
-
-
- {props.groupName} › Manage subscribers -
-
- Browse subscribers -
-
-
-

Manage subscribers

- {props.users ?

Subscribers

: false} - {props.users ? ( - props.users.length === 0 ? ( -
- There’s not a single one subscriber yet. You might invite some friends to - change that. -
- ) : ( - - ) - ) : ( - false - )} + const blockedUsersActions = useMemo( + () => [ + { + title: 'Unblock', + handler: (user) => dispatch(unblockUserInGroup(groupName, user.username)), + }, + ], + [dispatch, groupName], + ); + + useEffect(() => { + if (!amIAdmin) { + props.router.replace(`/${groupName}/subscribers`); + } + }, [amIAdmin, groupName, props.router]); -

Admins

+ if (!amIAdmin) { + return null; + } - {props.amILastGroupAdmin ? ( + return ( +
+
+ {props.boxHeader} +
+
+
+
+ {groupName} › Manage subscribers +
+
+ Browse subscribers +
+
+
+

Manage subscribers

+ {props.users ?

Subscribers

: false} + {props.users ? ( + props.users.length === 0 ? (
- You are the only Admin for this group. Before you can drop administrative privileges - or leave this group, you have to promote another group member to Admin first. + There’s not a single one subscriber yet. You might invite some friends to + change that.
) : ( - + + ) + ) : ( + false + )} + +

+ Admins +

+ + {props.amILastGroupAdmin ? ( +
+ You are the only Admin for this group. Before you can drop administrative privileges + or leave this group, you have to promote another group member to Admin first. +
+ ) : ( + + )} + +

+ Blocked users +

+
+

+ Blocked users cannot write posts to the group. They can still see and comment the + posts in the group while it is not private. +

+ + {(blockedUsersStatus.loading || blockedUsersStatus.initial) && ( +

Loading list…

)} + {blockedUsersStatus.error &&

{blockedUsersStatus.errorText}

}
+ {blockedUsersStatus.success && blockedUsers.length > 0 ? ( + + ) : ( +

This group has no blocked users.

+ )}
- ); - } +
+ ); } + function selectState(state, ownProps) { const { boxHeader, groupAdmins: allGroupAdmins, user } = state; const groupName = ownProps.params.userName; @@ -93,18 +151,18 @@ function selectState(state, ownProps) { }); const users = _.sortBy(usersWhoAreNotAdmins, 'username'); - const amILastGroupAdmin = - groupAdmins.find((u) => u.username == state.user.username) != null && groupAdmins.length == 1; - - return { boxHeader, groupName, user, groupAdmins, users, amILastGroupAdmin }; -} + const amIAdmin = state.managedGroups.some((g) => g.username === groupName); + const amILastGroupAdmin = amIAdmin && groupAdmins.length == 1; -function selectActions(dispatch) { return { - unsubscribeFromGroup: (...args) => dispatch(unsubscribeFromGroup(...args)), - makeGroupAdmin: (...args) => dispatch(makeGroupAdmin(...args)), - unadminGroupAdmin: (...args) => dispatch(unadminGroupAdmin(...args)), + boxHeader, + groupName, + user, + groupAdmins, + users, + amIAdmin, + amILastGroupAdmin, }; } -export default connect(selectState, selectActions)(ManageSubscribersHandler); +export default connect(selectState)(ManageSubscribersHandler); diff --git a/src/components/media-viewer.jsx b/src/components/media-viewer.jsx index e796a14ff..dbfc74deb 100644 --- a/src/components/media-viewer.jsx +++ b/src/components/media-viewer.jsx @@ -55,18 +55,25 @@ const getEmbeddableItem = async (url, withoutAutoplay) => { } let playerHTML = null; - const w = 800; - const h = info.aspectRatio ? Math.round(w * info.aspectRatio) : 450; - const wrapperPadding = info.aspectRatio ? `${info.aspectRatio * 100}%` : null; + let w = 800; + let h = 450; + if (info.aspectRatio) { + if (info.aspectRatio <= 1) { + h = Math.round(w * info.aspectRatio); + } else { + h = 800; + w = Math.round(h / info.aspectRatio); + } + } if (info.html) { - playerHTML = `
${info.html}
`; + playerHTML = `
${info.html}
`; } else { let player = null; if (info.playerURL) { player = (