emotes + addon list controls
This commit is contained in:
parent
b2eee5cf4c
commit
84c4db7e99
171
css/main.css
171
css/main.css
|
@ -123,17 +123,6 @@
|
|||
min-height: 20px;
|
||||
}
|
||||
|
||||
.bd-search {
|
||||
margin: 0 0 10px 0px;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
border: 0;
|
||||
background-color: #202225;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
|
||||
.bd-server-card {
|
||||
position: relative;
|
||||
border-width: 1px;
|
||||
|
@ -299,6 +288,126 @@
|
|||
|
||||
|
||||
|
||||
|
||||
.bd-addon-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bd-addon-controls .bd-search {
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.bd-addon-dropdowns {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.bd-select-wrap + .bd-select-wrap {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.bd-select-wrap {
|
||||
color: #f6f6f7;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bd-select-wrap label {
|
||||
opacity: .3;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.bd-select {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bd-select-controls {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.bd-select-transparent .bd-select-controls {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.bd-select-arrow, .bd-select-controls svg {
|
||||
margin-left: 10px;
|
||||
fill: #FFFFFF;
|
||||
}
|
||||
|
||||
.bd-select .bd-select-options {
|
||||
position: absolute;
|
||||
background: #2F3136;
|
||||
border-radius: 0 0 3px 3px;
|
||||
max-height: 300px;
|
||||
min-width: 100%;
|
||||
overflow-y: auto;
|
||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 1px 5px 0px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
border-top: 0;
|
||||
margin-top: -1px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.bd-select-transparent .bd-select-options {
|
||||
border: 1px solid rgba(0, 0, 0, 0.3);
|
||||
margin-top: 3px;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.bd-select .bd-select-option {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.bd-select .bd-select-option:hover,
|
||||
.bd-select .bd-select-option.selected {
|
||||
background: #26272B;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.bd-search-wrapper {
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
outline: none;
|
||||
border: 0;
|
||||
background-color: #202225;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bd-search {
|
||||
padding: 2px 3px;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bd-search-wrapper > svg {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* BEGIN EMOTE STYLING */
|
||||
/* =================== */
|
||||
#emote-container {
|
||||
|
@ -1219,11 +1328,11 @@ body .ace_closeButton:active {
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.bd-slist {
|
||||
.bd-addon-list {
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.bd-slist li {
|
||||
.bd-addon-list .bd-addon-card {
|
||||
max-height: 175px;
|
||||
margin-bottom: 20px;
|
||||
padding: 5px 8px;
|
||||
|
@ -1231,23 +1340,23 @@ body .ace_closeButton:active {
|
|||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-dark .bd-slist li {
|
||||
.theme-dark .bd-addon-list .bd-addon-card {
|
||||
background-color: rgba(32,34,37,.6);
|
||||
color: #f6f6f7;
|
||||
border-color: #202225;
|
||||
}
|
||||
.theme-light .bd-slist li {
|
||||
.theme-light .bd-addon-list .bd-addon-card {
|
||||
background-color: #f8f9f9;
|
||||
color: #4f545c;
|
||||
border-color: #dcddde;
|
||||
}
|
||||
|
||||
.bd-slist li.settings-open {
|
||||
.bd-addon-list .bd-addon-card.settings-open {
|
||||
max-height: 800px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.bd-slist .bd-header {
|
||||
.bd-addon-list .bd-header {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
|
@ -1257,36 +1366,36 @@ body .ace_closeButton:active {
|
|||
border-bottom: 1px solid transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-dark .bd-slist .bd-header {
|
||||
.theme-dark .bd-addon-list .bd-header {
|
||||
color: #f6f6f7;
|
||||
border-bottom-color: rgba(114,118,125,.3);
|
||||
}
|
||||
.theme-light .bd-slist .bd-header {
|
||||
.theme-light .bd-addon-list .bd-header {
|
||||
color: #4f545c;
|
||||
border-bottom-color: rgba(185,187,190,.3);
|
||||
}
|
||||
|
||||
.bd-slist .bd-description {
|
||||
.bd-addon-list .bd-description {
|
||||
word-break: break-word;
|
||||
max-height: 100px;
|
||||
margin: 5px 0;
|
||||
padding: 5px 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.theme-dark .bd-slist .bd-description {
|
||||
.theme-dark .bd-addon-list .bd-description {
|
||||
color: #b9bbbe;
|
||||
}
|
||||
.theme-light .bd-slist .bd-description {
|
||||
.theme-light .bd-addon-list .bd-description {
|
||||
color: #72767d;
|
||||
}
|
||||
|
||||
.bd-slist .scroller::-webkit-scrollbar-track-piece,
|
||||
.bd-slist .scroller::-webkit-scrollbar-thumb {
|
||||
.bd-addon-list .scroller::-webkit-scrollbar-track-piece,
|
||||
.bd-addon-list .scroller::-webkit-scrollbar-thumb {
|
||||
border-radius:0 !important;
|
||||
border-color:transparent;
|
||||
}
|
||||
|
||||
.bd-slist .bd-footer {
|
||||
.bd-addon-list .bd-footer {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
|
@ -1296,14 +1405,14 @@ body .ace_closeButton:active {
|
|||
border-top: 1px solid transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-dark .bd-slist .bd-footer {
|
||||
.theme-dark .bd-addon-list .bd-footer {
|
||||
border-top-color: rgba(114,118,125,.3);
|
||||
}
|
||||
.theme-light .bd-slist .bd-footer {
|
||||
.theme-light .bd-addon-list .bd-footer {
|
||||
border-top-color: rgba(185,187,190,.3);
|
||||
}
|
||||
|
||||
.bd-slist .bd-footer button {
|
||||
.bd-addon-list .bd-footer button {
|
||||
background: #7289da;
|
||||
color: #FFF;
|
||||
border-radius: 5px;
|
||||
|
@ -1313,15 +1422,15 @@ body .ace_closeButton:active {
|
|||
transition: opacity 250ms ease;
|
||||
}
|
||||
|
||||
.bd-slist .bd-footer button:disabled {
|
||||
.bd-addon-list .bd-footer button:disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.bd-slist .bd-footer a {
|
||||
.bd-addon-list .bd-footer a {
|
||||
color: #7289da;
|
||||
}
|
||||
|
||||
.bd-slist .bd-footer a:hover {
|
||||
.bd-addon-list .bd-footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* ======================= */
|
||||
|
|
64
js/main.js
64
js/main.js
File diff suppressed because one or more lines are too long
|
@ -206,6 +206,8 @@ export default new class EmoteModule extends Builtin {
|
|||
async loadEmoteData(categories) {
|
||||
if (!categories) categories = this.categories;
|
||||
if (!Array.isArray(categories)) categories = [categories];
|
||||
const all = Object.keys(Emotes);
|
||||
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
|
||||
Toasts.show(Strings.Emotes.loading, {type: "info"});
|
||||
this.emotesLoaded = false;
|
||||
|
||||
|
@ -233,6 +235,8 @@ export default new class EmoteModule extends Builtin {
|
|||
unloadEmoteData(categories) {
|
||||
if (!categories) categories = this.categories;
|
||||
if (!Array.isArray(categories)) categories = [categories];
|
||||
const all = Object.keys(Emotes);
|
||||
categories = categories.map(k => all.find(c => c.toLowerCase() == k.toLowerCase()));
|
||||
for (const category of categories) {
|
||||
delete Emotes[category];
|
||||
Emotes[category] = {};
|
||||
|
@ -242,7 +246,7 @@ export default new class EmoteModule extends Builtin {
|
|||
downloadEmotes(category) {
|
||||
const url = this.getRemoteFile(category);
|
||||
this.log(`Downloading ${category} from ${url}`);
|
||||
const options = {url: url, timeout: 8000, json: true};
|
||||
const options = {url: url, timeout: 10000, json: true};
|
||||
return new Promise(resolve => {
|
||||
request.get(options, (error, response, parsedData) => {
|
||||
if (error || response.statusCode != 200) {
|
||||
|
|
|
@ -20,7 +20,7 @@ export default [
|
|||
name: "Categories",
|
||||
collapsible: true,
|
||||
settings: [
|
||||
{type: "switch", id: "twitch", value: true},
|
||||
{type: "switch", id: "twitchglobal", value: true},
|
||||
{type: "switch", id: "twitchsubscriber", value: false},
|
||||
{type: "switch", id: "frankerfacez", value: true},
|
||||
{type: "switch", id: "bttv", value: true}
|
||||
|
|
|
@ -149,15 +149,15 @@ export default {
|
|||
},
|
||||
categories: {
|
||||
name: "Categories",
|
||||
twitch: {
|
||||
name: "Twitch",
|
||||
twitchglobal: {
|
||||
name: "Twitch Globals",
|
||||
note: "Show Twitch global emotes"
|
||||
},
|
||||
twitchsubscriber: {
|
||||
name: "Twitch",
|
||||
name: "Twitch Subscribers",
|
||||
note: "Show Twitch subscriber emotes"
|
||||
},
|
||||
ffz: {
|
||||
frankerfacez: {
|
||||
name: "FrankerFaceZ",
|
||||
note: "Show emotes from FFZ"
|
||||
},
|
||||
|
|
|
@ -148,9 +148,13 @@ export default class AddonManager {
|
|||
if (!fs.existsSync(possiblePath) || filename !== fs.realpathSync(possiblePath)) return Reflect.apply(originalRequire, this, arguments);
|
||||
let fileContent = fs.readFileSync(filename, "utf8");
|
||||
fileContent = stripBOM(fileContent);
|
||||
const stats = fs.statSync(filename);
|
||||
const meta = self.extractMeta(fileContent);
|
||||
meta.id = meta.name;
|
||||
meta.filename = path.basename(filename);
|
||||
meta.added = stats.atimeMs;
|
||||
meta.modified = stats.mtimeMs;
|
||||
meta.size = stats.size;
|
||||
fileContent = self.getFileModification(module, fileContent, meta);
|
||||
module._compile(fileContent, filename);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import {React} from "modules";
|
||||
|
||||
export default class DownArrow extends React.Component {
|
||||
render() {
|
||||
const size = this.props.size || "16px";
|
||||
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
|
||||
<path d="M8.12 9.29L12 13.17l3.88-3.88c.39-.39 1.02-.39 1.41 0 .39.39.39 1.02 0 1.41l-4.59 4.59c-.39.39-1.02.39-1.41 0L6.7 10.7c-.39-.39-.39-1.02 0-1.41.39-.38 1.03-.39 1.42 0z"/>
|
||||
</svg>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {React} from "modules";
|
||||
|
||||
export default class Search extends React.Component {
|
||||
render() {
|
||||
const size = this.props.size || "16px";
|
||||
return <svg className={this.props.className || ""} fill="#FFFFFF" viewBox="0 0 24 24" style={{width: size, height: size}}>
|
||||
<path fill="none" d="M0 0h24v24H0V0z"/>
|
||||
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
|
||||
</svg>;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import {React, WebpackModules, Strings} from "modules";
|
|||
import SettingsTitle from "../settings/title";
|
||||
import ServerCard from "./card";
|
||||
import Connection from "../../structs/psconnection";
|
||||
import Search from "../settings/components/search";
|
||||
|
||||
const SettingsView = WebpackModules.getByDisplayName("SettingsView");
|
||||
|
||||
|
@ -87,7 +88,8 @@ export default class PublicServers extends React.Component {
|
|||
}
|
||||
|
||||
get searchBox() {
|
||||
return React.createElement("input", {onKeyDown: this.searchKeyDown, type: "text", className: "bd-search", placeholder: `${Strings.PublicServers.search}...`, maxLength: "50"});
|
||||
return <Search onKeyDown={this.searchKeyDown} placeholder={`${Strings.PublicServers.search}...`} />;
|
||||
// return React.createElement("input", {onKeyDown: this.searchKeyDown, type: "text", className: "bd-search", placeholder: `${Strings.PublicServers.search}...`, maxLength: "50"});
|
||||
}
|
||||
|
||||
get title() {
|
||||
|
|
|
@ -84,10 +84,10 @@ export default class AddonCard extends React.Component {
|
|||
const props = {id: `${name}-settings`, className: "addon-settings", ref: this.panelRef};
|
||||
if (typeof(settingsPanel) == "string") props.dangerouslySetInnerHTML = this.settingsPanel;
|
||||
|
||||
return <li className="settings-open bd-switch-item">
|
||||
return <div className="bd-addon-card settings-open bd-switch-item">
|
||||
<div className="bd-close" onClick={this.closeSettings}><CloseButton /></div>
|
||||
<div {...props}>{this.settingsPanel instanceof React.Component ? this.settingsPanel : null}</div>
|
||||
</li>;
|
||||
</div>;
|
||||
}
|
||||
|
||||
buildLink(which) {
|
||||
|
@ -115,7 +115,7 @@ export default class AddonCard extends React.Component {
|
|||
const description = this.getString(addon.description);
|
||||
const version = this.getString(addon.version);
|
||||
|
||||
return <li dataName={name} dataVersion={version} className="settings-closed bd-switch-item">
|
||||
return <div dataName={name} dataVersion={version} className="bd-addon-card settings-closed bd-switch-item">
|
||||
<div className="bd-header">
|
||||
<span className="bd-header-title">{this.buildTitle(name, version, author)}</span>
|
||||
<div className="bd-controls">
|
||||
|
@ -128,6 +128,6 @@ export default class AddonCard extends React.Component {
|
|||
</div>
|
||||
<div className="bd-description-wrap scroller-wrap fade"><div className="bd-description scroller">{description}</div></div>
|
||||
{this.footer}
|
||||
</li>;
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,27 +3,84 @@ import {React, Settings, Strings} from "modules";
|
|||
import SettingsTitle from "./title";
|
||||
import ReloadIcon from "../icons/reload";
|
||||
import AddonCard from "./addoncard";
|
||||
import Select from "./components/select";
|
||||
import Search from "./components/search";
|
||||
|
||||
const sortOptions = [
|
||||
{label: "Name", value: "name"},
|
||||
{label: "Author", value: "author"},
|
||||
{label: "Version", value: "version"},
|
||||
{label: "Date Added", value: "added"},
|
||||
{label: "Date Modified", value: "modified"}
|
||||
];
|
||||
|
||||
const directionOptions = [
|
||||
{label: "Ascending", value: "true"},
|
||||
{label: "Descending", value: "false"}
|
||||
];
|
||||
|
||||
export default class AddonList extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {sort: "name", ascending: true, query: ""};
|
||||
this.sort = this.sort.bind(this);
|
||||
this.reverse = this.reverse.bind(this);
|
||||
this.search = this.search.bind(this);
|
||||
}
|
||||
|
||||
reload() {
|
||||
if (this.props.refreshList) this.props.refreshList();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
reverse(value) {
|
||||
this.setState({ascending: value == "true"});
|
||||
}
|
||||
|
||||
sort(value) {
|
||||
this.setState({sort: value});
|
||||
}
|
||||
|
||||
search(event) {
|
||||
this.setState({query: event.target.value.toLocaleLowerCase()});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {title, folder, addonList, addonState, onChange, reload} = this.props;
|
||||
const showReloadIcon = !Settings.get("settings", "addons", "autoReload");
|
||||
const button = folder ? {title: Strings.Addons.openFolder.format({type: title}), onClick: () => {require("electron").shell.openItem(folder);}} : null;
|
||||
const sortedAddons = addonList.sort((a, b) => {
|
||||
const first = a[this.state.sort];
|
||||
const second = b[this.state.sort];
|
||||
if (typeof(first) == "string") return first.toLocaleLowerCase().localeCompare(second.toLocaleLowerCase());
|
||||
if (first > second) return 1;
|
||||
if (second > first) return -1;
|
||||
return 0;
|
||||
});
|
||||
if (!this.state.ascending) sortedAddons.reverse();
|
||||
return [
|
||||
<SettingsTitle key="title" text={title} button={button} otherChildren={showReloadIcon && <ReloadIcon className="bd-reload" onClick={this.reload.bind(this)} />} />,
|
||||
<ul key="addonList" className={"bd-slist"}>
|
||||
{addonList.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase())).map(addon => {
|
||||
<div className="bd-controls bd-addon-controls">
|
||||
<Search onChange={this.search} placeholder={`Search...`} />
|
||||
<div className="bd-addon-dropdowns">
|
||||
<Select options={sortOptions} label="Sort By:" onChange={this.sort} style="transparent" />
|
||||
<Select options={directionOptions} label="Order:" onChange={this.reverse} style="transparent" />
|
||||
</div>
|
||||
</div>,
|
||||
<div key="addonList" className={"bd-addon-list"}>
|
||||
{sortedAddons.map(addon => {
|
||||
if (this.state.query) {
|
||||
let matches = addon.name.toLocaleLowerCase().includes(this.state.query);
|
||||
matches = matches || addon.author.toLocaleLowerCase().includes(this.state.query);
|
||||
matches = matches || addon.description.toLocaleLowerCase().includes(this.state.query);
|
||||
if (!matches) return null;
|
||||
}
|
||||
const hasSettings = addon.type && typeof(addon.plugin.getSettingsPanel) === "function";
|
||||
const getSettings = hasSettings && addon.plugin.getSettingsPanel.bind(addon.plugin);
|
||||
return <AddonCard showReloadIcon={showReloadIcon} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} />;
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {React} from "modules";
|
||||
import SearchIcon from "../../icons/search";
|
||||
|
||||
export default class Search extends React.Component {
|
||||
render() {
|
||||
return <div className="bd-search-wrapper">
|
||||
<input onChange={this.props.onChange} onKeyDown={this.props.onKeyDown} type="text" className="bd-search" placeholder={this.props.placeholder} maxLength="50" />
|
||||
<SearchIcon />
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import {React} from "modules";
|
||||
import Arrow from "../../icons/downarrow";
|
||||
|
||||
export default class Select extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {open: false, value: this.props.value || this.props.options[0].value};
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.showMenu = this.showMenu.bind(this);
|
||||
this.hideMenu = this.hideMenu.bind(this);
|
||||
}
|
||||
|
||||
showMenu(event) {
|
||||
event.preventDefault();
|
||||
this.setState({open: true}, () => {
|
||||
document.addEventListener("click", this.hideMenu);
|
||||
});
|
||||
}
|
||||
|
||||
hideMenu() {
|
||||
this.setState({open: false}, () => {
|
||||
document.removeEventListener("click", this.hideMenu);
|
||||
});
|
||||
}
|
||||
|
||||
onChange(value) {
|
||||
this.setState({value});
|
||||
if (this.props.onChange) this.props.onChange(value);
|
||||
}
|
||||
|
||||
get selected() {return this.props.options.find(o => o.value == this.state.value);}
|
||||
|
||||
get options() {
|
||||
const selected = this.selected;
|
||||
return <div className="bd-select-options">
|
||||
{this.props.options.map(opt =>
|
||||
<div className={`bd-select-option${selected.value == opt.value ? " selected" : ""}`} onClick={this.onChange.bind(this, opt.value)}>{opt.label}</div>
|
||||
)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style == "transparent" ? " bd-select-transparent" : "";
|
||||
const isOpen = this.state.open ? " menu-open" : "";
|
||||
return <div className="bd-select-wrap">
|
||||
<label className="bd-label">{this.props.label}</label>
|
||||
<div className={`bd-select${style}${isOpen}`} onClick={this.showMenu}>
|
||||
<div className="bd-select-controls">
|
||||
<div className="bd-select-value">{this.selected.label}</div>
|
||||
<Arrow className="bd-select-arrow" />
|
||||
</div>
|
||||
{this.state.open && this.options}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue