From b80568b1c50351d5571ac9fcd2157c9ba0c87499 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 21 Mar 2018 12:00:56 +0000 Subject: [PATCH 1/4] Wrap GeminiScrollbar in a component, enabled forceGemini Fixes https://github.com/vector-im/riot-web/issues/6294 --- src/components/structures/GroupView.js | 6 ++-- src/components/structures/MyGroups.js | 7 ++-- src/components/structures/ScrollPanel.js | 6 ++-- src/components/structures/TagPanel.js | 7 ++-- src/components/structures/UserSettings.js | 9 ++--- .../views/dialogs/UnknownDeviceDialog.js | 5 +-- .../views/elements/GeminiScrollbarWrapper.js | 36 +++++++++++++++++++ .../views/groups/GroupMemberInfo.js | 6 ++-- .../views/groups/GroupMemberList.js | 6 ++-- src/components/views/groups/GroupRoomInfo.js | 6 ++-- src/components/views/groups/GroupRoomList.js | 6 ++-- src/components/views/rooms/MemberInfo.js | 6 ++-- src/components/views/rooms/MemberList.js | 6 ++-- src/components/views/rooms/RoomList.js | 8 ++--- .../views/rooms/SearchableEntityList.js | 6 ++-- 15 files changed, 83 insertions(+), 43 deletions(-) create mode 100644 src/components/views/elements/GeminiScrollbarWrapper.js diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 30e897e5dcc..2b5b3d5353c 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -31,7 +31,6 @@ import GroupStoreCache from '../../stores/GroupStoreCache'; import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; const LONG_DESC_PLACEHOLDER = _td( @@ -969,6 +968,7 @@ export default React.createClass({ const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Spinner = sdk.getComponent("elements.Spinner"); const TintableSvg = sdk.getComponent("elements.TintableSvg"); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; @@ -1119,9 +1119,9 @@ export default React.createClass({ { rightButtons } - + { bodyNodes } - + ); } else if (this.state.error) { diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index da7bebd16a3..7a93cfb886e 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import sdk from '../../index'; import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; @@ -63,6 +62,8 @@ export default withMatrixClient(React.createClass({ const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const TintableSvg = sdk.getComponent("elements.TintableSvg"); const GroupTile = sdk.getComponent("groups.GroupTile"); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); + let content; let contentHeader; @@ -73,7 +74,7 @@ export default withMatrixClient(React.createClass({ }); contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? - +

{ _t( @@ -92,7 +93,7 @@ export default withMatrixClient(React.createClass({

{ groupNodes }
- : + :
{ _t( "You're not currently a member of any communities.", diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index cbb6001d5f2..722dc2251f5 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -17,7 +17,6 @@ limitations under the License. const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; -const GeminiScrollbar = require('react-gemini-scrollbar'); import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; @@ -669,10 +668,11 @@ module.exports = React.createClass({ }, render: function() { + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); // TODO: the classnames on the div and ol could do with being updated to // reflect the fact that we don't necessarily contain a list of messages. // it's not obvious why we have a separate div and ol anyway. - return (
@@ -680,7 +680,7 @@ module.exports = React.createClass({ { this.props.children }
-
+ ); }, }); diff --git a/src/components/structures/TagPanel.js b/src/components/structures/TagPanel.js index 7a187e42985..790c497a67d 100644 --- a/src/components/structures/TagPanel.js +++ b/src/components/structures/TagPanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import TagOrderStore from '../../stores/TagOrderStore'; import GroupActions from '../../actions/GroupActions'; @@ -102,6 +101,8 @@ const TagPanel = React.createClass({ const DNDTagTile = sdk.getComponent('elements.DNDTagTile'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const TintableSvg = sdk.getComponent('elements.TintableSvg'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); + const tags = this.state.orderedTags.map((tag, index) => { return
- ) } - +
diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index f6629b4b09c..85223c4eef2 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -30,7 +30,6 @@ import Promise from 'bluebird'; const packageJson = require('../../../package.json'); const UserSettingsStore = require('../../UserSettingsStore'); const CallMediaHandler = require('../../CallMediaHandler'); -const GeminiScrollbar = require('react-gemini-scrollbar'); const Email = require('../../email'); const AddThreepid = require('../../AddThreepid'); const SdkConfig = require('../../SdkConfig'); @@ -1118,6 +1117,7 @@ module.exports = React.createClass({ const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const Notifications = sdk.getComponent("settings.Notifications"); const EditableText = sdk.getComponent('elements.EditableText'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const avatarUrl = ( this.state.avatarUrl ? MatrixClientPeg.get().mxcUrlToHttp(this.state.avatarUrl) : null @@ -1213,8 +1213,9 @@ module.exports = React.createClass({ onCancelClick={this.props.onClose} /> - +

{ _t("Profile") }

@@ -1327,7 +1328,7 @@ module.exports = React.createClass({ { this._renderDeactivateAccount() } -
+
); }, diff --git a/src/components/views/dialogs/UnknownDeviceDialog.js b/src/components/views/dialogs/UnknownDeviceDialog.js index 26aa5d3ecb2..07f8b6187f1 100644 --- a/src/components/views/dialogs/UnknownDeviceDialog.js +++ b/src/components/views/dialogs/UnknownDeviceDialog.js @@ -146,6 +146,7 @@ export default React.createClass({ }, render: function() { + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.props.devices === null) { const Spinner = sdk.getComponent("elements.Spinner"); return ; @@ -191,7 +192,7 @@ export default React.createClass({ title={_t('Room contains unknown devices')} contentId='mx_Dialog_content' > - +

{ _t('"%(RoomName)s" contains devices that you haven\'t seen before.', {RoomName: this.props.room.name}) }

@@ -199,7 +200,7 @@ export default React.createClass({ { _t("Unknown devices") }: -
+ diff --git a/src/components/views/elements/GeminiScrollbarWrapper.js b/src/components/views/elements/GeminiScrollbarWrapper.js new file mode 100644 index 00000000000..1de82bebc29 --- /dev/null +++ b/src/components/views/elements/GeminiScrollbarWrapper.js @@ -0,0 +1,36 @@ +/* +Copyright 2018 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import GeminiScrollbar from 'react-gemini-scrollbar'; + +class GeminiScrollbarWrapper extends React.Component { + render() { + // Enable forceGemini so that gemini is always enabled. This is + // to avoid future issues where a feature is implemented without + // doing QA on every OS/browser combination. + // + // By default GeminiScrollbar allows native scrollbars to be used + // on macOS. Use forceGemini to enable Gemini's non-native + // scrollbars on all OSs. + return + { this.props.children } + ; + } +} + +export default GeminiScrollbarWrapper; + diff --git a/src/components/views/groups/GroupMemberInfo.js b/src/components/views/groups/GroupMemberInfo.js index 097fb1f7dbd..4970a26e5b9 100644 --- a/src/components/views/groups/GroupMemberInfo.js +++ b/src/components/views/groups/GroupMemberInfo.js @@ -25,7 +25,6 @@ import { _t } from '../../../languageHandler'; import { GroupMemberType } from '../../../groups'; import GroupStoreCache from '../../../stores/GroupStoreCache'; import AccessibleButton from '../elements/AccessibleButton'; -import GeminiScrollbar from 'react-gemini-scrollbar'; module.exports = React.createClass({ displayName: 'GroupMemberInfo', @@ -180,9 +179,10 @@ module.exports = React.createClass({ ); const EmojiText = sdk.getComponent('elements.EmojiText'); + const GeminiScrollbarWrapper = sdk.getComponent('elements.GeminiScrollbarWrapper'); return (
- + @@ -199,7 +199,7 @@ module.exports = React.createClass({
{ adminTools } - +
); }, diff --git a/src/components/views/groups/GroupMemberList.js b/src/components/views/groups/GroupMemberList.js index 71c168f5e63..17a91d83fa3 100644 --- a/src/components/views/groups/GroupMemberList.js +++ b/src/components/views/groups/GroupMemberList.js @@ -18,7 +18,6 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import GroupStoreCache from '../../../stores/GroupStoreCache'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import PropTypes from 'prop-types'; const INITIAL_LOAD_NUM_MEMBERS = 30; @@ -134,6 +133,7 @@ export default React.createClass({ }, render: function() { + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.fetching || this.state.fetchingInvitedMembers) { const Spinner = sdk.getComponent("elements.Spinner"); return (
@@ -162,10 +162,10 @@ export default React.createClass({ return (
{ inputBox } - + { joined } { invited } - +
); }, diff --git a/src/components/views/groups/GroupRoomInfo.js b/src/components/views/groups/GroupRoomInfo.js index fa4ed89ae06..2d2b4e655c8 100644 --- a/src/components/views/groups/GroupRoomInfo.js +++ b/src/components/views/groups/GroupRoomInfo.js @@ -22,7 +22,6 @@ import Modal from '../../../Modal'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import GroupStoreCache from '../../../stores/GroupStoreCache'; -import GeminiScrollbar from 'react-gemini-scrollbar'; module.exports = React.createClass({ displayName: 'GroupRoomInfo', @@ -157,6 +156,7 @@ module.exports = React.createClass({ const EmojiText = sdk.getComponent('elements.EmojiText'); const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.groupRoomRemoveLoading || !this.state.groupRoom) { const Spinner = sdk.getComponent("elements.Spinner"); return
@@ -216,7 +216,7 @@ module.exports = React.createClass({ const avatar = ; return (
- + @@ -233,7 +233,7 @@ module.exports = React.createClass({
{ adminTools } - +
); }, diff --git a/src/components/views/groups/GroupRoomList.js b/src/components/views/groups/GroupRoomList.js index 189fa944e2f..0515865c6bb 100644 --- a/src/components/views/groups/GroupRoomList.js +++ b/src/components/views/groups/GroupRoomList.js @@ -17,7 +17,6 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import GroupStoreCache from '../../../stores/GroupStoreCache'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import PropTypes from 'prop-types'; const INITIAL_LOAD_NUM_ROOMS = 30; @@ -120,16 +119,17 @@ export default React.createClass({ ); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const TruncatedList = sdk.getComponent("elements.TruncatedList"); return (
{ inputBox } - + { this.makeGroupRoomTiles(this.state.searchQuery) } - +
); }, diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index c449ad26838..2789c0e4cd5 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -39,7 +39,6 @@ import Unread from '../../../Unread'; import { findReadReceiptFromUserId } from '../../../utils/Receipt'; import withMatrixClient from '../../../wrappers/withMatrixClient'; import AccessibleButton from '../elements/AccessibleButton'; -import GeminiScrollbar from 'react-gemini-scrollbar'; import RoomViewStore from '../../../stores/RoomViewStore'; import SdkConfig from '../../../SdkConfig'; @@ -897,11 +896,12 @@ module.exports = withMatrixClient(React.createClass({
; } + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const EmojiText = sdk.getComponent('elements.EmojiText'); return (
- +
@@ -925,7 +925,7 @@ module.exports = withMatrixClient(React.createClass({ { this._renderDevices() } { spinner } - +
); }, diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 620cfe33fc7..3dfcd38cbd1 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -21,7 +21,6 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; const MatrixClientPeg = require("../../../MatrixClientPeg"); const sdk = require('../../../index'); -const GeminiScrollbar = require('react-gemini-scrollbar'); const rate_limited_func = require('../../../ratelimitedfunc'); const CallHandler = require("../../../CallHandler"); @@ -395,6 +394,7 @@ module.exports = React.createClass({ render: function() { const TruncatedList = sdk.getComponent("elements.TruncatedList"); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); let invitedSection = null; if (this._getChildCountInvited() > 0) { @@ -423,14 +423,14 @@ module.exports = React.createClass({ return (
{ inputBox } - + { invitedSection } - +
); }, diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index 6ccf04c5d8c..ef1a3adc1d9 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -20,7 +20,6 @@ const React = require("react"); const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; -const GeminiScrollbar = require('react-gemini-scrollbar'); const MatrixClientPeg = require("../../../MatrixClientPeg"); const CallHandler = require('../../../CallHandler'); const dis = require("../../../dispatcher"); @@ -351,7 +350,7 @@ module.exports = React.createClass({ return Boolean(isRoomVisible[taggedRoom.roomId]); }); - + if (filteredRooms.length > 0 || tagName.match(STANDARD_TAGS_REGEX)) { filteredLists[tagName] = filteredRooms; } @@ -601,10 +600,11 @@ module.exports = React.createClass({ render: function() { const RoomSubList = sdk.getComponent('structures.RoomSubList'); + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const self = this; return ( -
-
+
); }, }); diff --git a/src/components/views/rooms/SearchableEntityList.js b/src/components/views/rooms/SearchableEntityList.js index ff75524a5da..321f365da76 100644 --- a/src/components/views/rooms/SearchableEntityList.js +++ b/src/components/views/rooms/SearchableEntityList.js @@ -19,7 +19,6 @@ const MatrixClientPeg = require("../../../MatrixClientPeg"); const Modal = require("../../../Modal"); const sdk = require("../../../index"); import { _t } from '../../../languageHandler'; -const GeminiScrollbar = require('react-gemini-scrollbar'); // A list capable of displaying entities which conform to the SearchableEntity // interface which is an object containing getJsx(): Jsx and matches(query: string): boolean @@ -164,11 +163,12 @@ const SearchableEntityList = React.createClass({
); } + const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); list = ( - { list } - + ); } From 39e9d52c042521aa430bc6a66a517887b598a6d8 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 21 Mar 2018 15:58:14 +0000 Subject: [PATCH 2/4] Make sure to proxy special prop `ref` --- src/components/structures/ScrollPanel.js | 17 ++++++++++++++--- .../views/elements/GeminiScrollbarWrapper.js | 2 +- src/components/views/rooms/RoomList.js | 9 +++++++-- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/components/structures/ScrollPanel.js b/src/components/structures/ScrollPanel.js index 722dc2251f5..0fdbc9a3493 100644 --- a/src/components/structures/ScrollPanel.js +++ b/src/components/structures/ScrollPanel.js @@ -19,6 +19,7 @@ const ReactDOM = require("react-dom"); import PropTypes from 'prop-types'; import Promise from 'bluebird'; import { KeyCode } from '../../Keyboard'; +import sdk from '../../index.js'; const DEBUG_SCROLL = false; // var DEBUG_SCROLL = true; @@ -223,7 +224,7 @@ module.exports = React.createClass({ onResize: function() { this.props.onResize(); this.checkScroll(); - this.refs.geminiPanel.forceUpdate(); + if (this._gemScroll) this._gemScroll.forceUpdate(); }, // after an update to the contents of the panel, check that the scroll is @@ -664,7 +665,17 @@ module.exports = React.createClass({ throw new Error("ScrollPanel._getScrollNode called when unmounted"); } - return this.refs.geminiPanel.scrollbar.getViewElement(); + if (!this._gemScroll) { + // Likewise, we should have the ref by this point, but if not + // turn the NPE into something meaningful. + throw new Error("ScrollPanel._getScrollNode called before gemini ref collected"); + } + + return this._gemScroll.scrollbar.getViewElement(); + }, + + _collectGeminiScroll: function(gemScroll) { + this._gemScroll = gemScroll; }, render: function() { @@ -672,7 +683,7 @@ module.exports = React.createClass({ // TODO: the classnames on the div and ol could do with being updated to // reflect the fact that we don't necessarily contain a list of messages. // it's not obvious why we have a separate div and ol anyway. - return (
diff --git a/src/components/views/elements/GeminiScrollbarWrapper.js b/src/components/views/elements/GeminiScrollbarWrapper.js index 1de82bebc29..c56ff67c9c2 100644 --- a/src/components/views/elements/GeminiScrollbarWrapper.js +++ b/src/components/views/elements/GeminiScrollbarWrapper.js @@ -26,7 +26,7 @@ class GeminiScrollbarWrapper extends React.Component { // By default GeminiScrollbar allows native scrollbars to be used // on macOS. Use forceGemini to enable Gemini's non-native // scrollbars on all OSs. - return + return { this.props.children } ; } diff --git a/src/components/views/rooms/RoomList.js b/src/components/views/rooms/RoomList.js index ef1a3adc1d9..acf04831e80 100644 --- a/src/components/views/rooms/RoomList.js +++ b/src/components/views/rooms/RoomList.js @@ -507,7 +507,8 @@ module.exports = React.createClass({ onShowMoreRooms: function() { // kick gemini in the balls to get it to wake up // XXX: uuuuuuugh. - this.refs.gemscroll.forceUpdate(); + if (!this._gemScroll) return; + this._gemScroll.forceUpdate(); }, _getEmptyContent: function(section) { @@ -598,6 +599,10 @@ module.exports = React.createClass({ return ret; }, + _collectGemini(gemScroll) { + this._gemScroll = gemScroll; + }, + render: function() { const RoomSubList = sdk.getComponent('structures.RoomSubList'); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); @@ -605,7 +610,7 @@ module.exports = React.createClass({ const self = this; return ( + autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
Date: Wed, 21 Mar 2018 16:00:50 +0000 Subject: [PATCH 3/4] Convert GeminiScrollbarWrapper to stateless component --- .../views/elements/GeminiScrollbarWrapper.js | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/components/views/elements/GeminiScrollbarWrapper.js b/src/components/views/elements/GeminiScrollbarWrapper.js index c56ff67c9c2..445b1a25965 100644 --- a/src/components/views/elements/GeminiScrollbarWrapper.js +++ b/src/components/views/elements/GeminiScrollbarWrapper.js @@ -14,23 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; import GeminiScrollbar from 'react-gemini-scrollbar'; -class GeminiScrollbarWrapper extends React.Component { - render() { - // Enable forceGemini so that gemini is always enabled. This is - // to avoid future issues where a feature is implemented without - // doing QA on every OS/browser combination. - // - // By default GeminiScrollbar allows native scrollbars to be used - // on macOS. Use forceGemini to enable Gemini's non-native - // scrollbars on all OSs. - return - { this.props.children } - ; - } +function GeminiScrollbarWrapper(props) { + // Enable forceGemini so that gemini is always enabled. This is + // to avoid future issues where a feature is implemented without + // doing QA on every OS/browser combination. + // + // By default GeminiScrollbar allows native scrollbars to be used + // on macOS. Use forceGemini to enable Gemini's non-native + // scrollbars on all OSs. + return + { props.children } + ; } - export default GeminiScrollbarWrapper; From 944940057eb25b9af4a1d1f5628e5251aa9efcb0 Mon Sep 17 00:00:00 2001 From: Luke Barnard Date: Wed, 21 Mar 2018 17:23:19 +0000 Subject: [PATCH 4/4] Remember that React needs React --- src/components/views/elements/GeminiScrollbarWrapper.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/GeminiScrollbarWrapper.js b/src/components/views/elements/GeminiScrollbarWrapper.js index 445b1a25965..af801c01131 100644 --- a/src/components/views/elements/GeminiScrollbarWrapper.js +++ b/src/components/views/elements/GeminiScrollbarWrapper.js @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; import GeminiScrollbar from 'react-gemini-scrollbar'; function GeminiScrollbarWrapper(props) {