mirror of https://github.com/kern/filepizza
Update for zeit/now support
parent
2153fd2fdf
commit
d52db44264
@ -1,14 +1,9 @@
|
||||
FROM node:latest
|
||||
MAINTAINER Alex Kern <alex@pavlovml.com>
|
||||
MAINTAINER Alex Kern <alex@kern.io>
|
||||
|
||||
# install
|
||||
RUN mkdir -p /filepizza
|
||||
WORKDIR /filepizza
|
||||
COPY package.json Makefile ./
|
||||
RUN make install
|
||||
COPY . ./
|
||||
RUN npm install && npm run build
|
||||
|
||||
# run
|
||||
ENV NODE_ENV production
|
||||
EXPOSE 80
|
||||
CMD ./dist/index.js
|
||||
CMD node ./dist/index.js
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
# ==============================================================================
|
||||
# config
|
||||
|
||||
.PHONY: all build clean install push run
|
||||
|
||||
all: run
|
||||
|
||||
WATCH ?= false
|
||||
TAG ?= latest
|
||||
|
||||
# ==============================================================================
|
||||
# phony targets
|
||||
|
||||
build:
|
||||
./node_modules/.bin/babel src --ignore __tests__,__mocks__ --out-dir dist
|
||||
./node_modules/.bin/webpack --optimize-minimize ./src/client
|
||||
docker build -t kern/filepizza:$(TAG) .
|
||||
|
||||
clean:
|
||||
@ rm -rf node_modules
|
||||
@ rm -rf dist
|
||||
|
||||
install:
|
||||
npm install
|
||||
|
||||
push: build
|
||||
docker push kern/filepizza:$(TAG)
|
||||
|
||||
run: | node_modules
|
||||
@ if [ "$(WATCH)" = false ]; then \
|
||||
./node_modules/.bin/babel-node src; \
|
||||
else \
|
||||
./node_modules/.bin/nodemon ./node_modules/.bin/babel-node -i dist src; \
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# file targets
|
||||
|
||||
node_modules:
|
||||
npm install
|
||||
@ -0,0 +1,22 @@
|
||||
import "babel-polyfill";
|
||||
import "./index.styl";
|
||||
import React from "react";
|
||||
import ReactRouter from "react-router";
|
||||
import routes from "./routes";
|
||||
import alt from "./alt";
|
||||
import webrtcSupport from "webrtcsupport";
|
||||
import SupportActions from "./actions/SupportActions";
|
||||
|
||||
let bootstrap = document.getElementById("bootstrap").innerHTML;
|
||||
alt.bootstrap(bootstrap);
|
||||
|
||||
window.FilePizza = () => {
|
||||
ReactRouter.run(routes, ReactRouter.HistoryLocation, function(Handler) {
|
||||
React.render(<Handler data={bootstrap} />, document);
|
||||
});
|
||||
|
||||
if (!webrtcSupport.support) SupportActions.noSupport();
|
||||
|
||||
let isChrome = navigator.userAgent.toLowerCase().indexOf("chrome") > -1;
|
||||
if (isChrome) SupportActions.isChrome();
|
||||
};
|
||||
@ -0,0 +1,97 @@
|
||||
import Bootstrap from "./Bootstrap";
|
||||
import ErrorPage from "./ErrorPage";
|
||||
import FrozenHead from "react-frozenhead";
|
||||
import React from "react";
|
||||
import SupportStore from "../stores/SupportStore";
|
||||
import { RouteHandler } from "react-router";
|
||||
import ga from "react-google-analytics";
|
||||
|
||||
ga("create", "UA-62785624-1", "auto");
|
||||
ga("send", "pageview");
|
||||
|
||||
export default class App extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = SupportStore.getState();
|
||||
|
||||
this._onChange = () => {
|
||||
this.setState(SupportStore.getState());
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SupportStore.listen(this._onChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SupportStore.unlisten(this._onChange);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<FrozenHead>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:url" content="https://file.pizza" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="FilePizza - Your files, delivered."
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="Peer-to-peer file transfers in your web browser."
|
||||
/>
|
||||
<meta
|
||||
property="og:image"
|
||||
content="https://file.pizza/images/fb.png"
|
||||
/>
|
||||
<title>FilePizza - Your files, delivered.</title>
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
<Bootstrap data={this.props.data} />
|
||||
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js" />
|
||||
<script src="/app.js" />
|
||||
</FrozenHead>
|
||||
|
||||
<body>
|
||||
<div className="container">
|
||||
{this.state.isSupported ? <RouteHandler /> : <ErrorPage />}
|
||||
</div>
|
||||
<footer className="footer">
|
||||
<p>
|
||||
<script
|
||||
id="fb13c4g"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
"(function(i){var f,s=document.getElementById(i);f=document.createElement('iframe');f.src='//api.flattr.com/button/view/?uid=kern&button=compact&url=http%3A%2F%2Fgithub.com%2Fkern%2Ffilepizza';f.title='Flattr';f.height=20;f.width=110;f.style.borderWidth=0;s.parentNode.insertBefore(f,s);})('fb13c4g');"
|
||||
}}
|
||||
/>{" "}
|
||||
Donations: <strong>1P7yFQAC3EmpvsB7K9s6bKPvXEP1LPoQnY</strong>
|
||||
</p>
|
||||
|
||||
<p className="byline">
|
||||
Cooked up by{" "}
|
||||
<a href="http://kern.io" target="_blank">
|
||||
Alex Kern
|
||||
</a>{" "}
|
||||
&{" "}
|
||||
<a href="http://neeraj.io" target="_blank">
|
||||
Neeraj Baid
|
||||
</a>{" "}
|
||||
while eating <strong>Sliver</strong> @ UC Berkeley ·{" "}
|
||||
<a href="https://github.com/kern/filepizza#faq" target="_blank">
|
||||
FAQ
|
||||
</a>{" "}
|
||||
·{" "}
|
||||
<a href="https://github.com/kern/filepizza" target="_blank">
|
||||
Fork us
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
<script>FilePizza()</script>
|
||||
<ga.Initializer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
var twilio = require("twilio");
|
||||
var winston = require("winston");
|
||||
|
||||
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN) {
|
||||
var twilioSID = process.env.TWILIO_SID;
|
||||
var twilioToken = process.env.TWILIO_TOKEN;
|
||||
var client = twilio(twilioSID, twilioToken);
|
||||
winston.info("Using Twilio TURN service");
|
||||
} else {
|
||||
var client = null;
|
||||
}
|
||||
|
||||
var DEFAULT_ICE_SERVERS = [
|
||||
{
|
||||
urls: "stun:stun.l.google.com:19302"
|
||||
}
|
||||
];
|
||||
|
||||
var CACHE_LIFETIME = 5 * 60 * 1000; // 5 minutes
|
||||
var cachedPromise = null;
|
||||
|
||||
function clearCache() {
|
||||
cachedPromise = null;
|
||||
}
|
||||
|
||||
exports.getICEServers = function() {
|
||||
if (client == null) return Promise.resolve(DEFAULT_ICE_SERVERS);
|
||||
if (cachedPromise) return cachedPromise;
|
||||
|
||||
cachedPromise = new Promise(function(resolve, reject) {
|
||||
client.tokens.create({}, function(err, token) {
|
||||
if (err) {
|
||||
winston.error(err);
|
||||
return resolve(DEFAULT_ICE_SERVERS);
|
||||
}
|
||||
|
||||
winston.info("Retrieved ICE servers from Twilio");
|
||||
setTimeout(clearCache, CACHE_LIFETIME);
|
||||
resolve(token.ice_servers);
|
||||
});
|
||||
});
|
||||
|
||||
return cachedPromise;
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
@import "nib";
|
||||
|
||||
beige = #F9F2E7
|
||||
dark-gray = #333
|
||||
gray = #777
|
||||
@ -0,0 +1,124 @@
|
||||
var db = require("./db");
|
||||
var express = require("express");
|
||||
var expressWinston = require("express-winston");
|
||||
var forceSSL = require("express-force-ssl");
|
||||
var fs = require("fs");
|
||||
var http = require("http");
|
||||
var https = require("https");
|
||||
var ice = require("./ice");
|
||||
var path = require("path");
|
||||
var socketIO = require("socket.io");
|
||||
var winston = require("winston");
|
||||
|
||||
var app = express();
|
||||
|
||||
if (process.env.SECURE) {
|
||||
var server = https.Server(
|
||||
{
|
||||
key: fs.readFileSync(process.env.SSL_KEY || "key.pem"),
|
||||
cert: fs.readFileSync(process.env.SSL_CERT || "cert.pem")
|
||||
},
|
||||
app
|
||||
);
|
||||
var port = process.env.PORT || 443;
|
||||
var insecurePort = process.env.INSECURE_PORT || 80;
|
||||
http.Server(app).listen(80);
|
||||
} else {
|
||||
var server = http.Server(app);
|
||||
var port =
|
||||
process.env.PORT || (process.env.NODE_ENV === "production" ? 80 : 3000);
|
||||
}
|
||||
|
||||
var io = socketIO(server);
|
||||
io.set("transports", ["polling"]);
|
||||
|
||||
var logDir = path.resolve(__dirname, "../log");
|
||||
|
||||
winston.add(winston.transports.DailyRotateFile, {
|
||||
filename: logDir + "/access.log",
|
||||
level: "info"
|
||||
});
|
||||
|
||||
winston.add(winston.transports.File, {
|
||||
filename: logDir + "/error.log",
|
||||
level: "error",
|
||||
handleExceptions: true,
|
||||
json: false
|
||||
});
|
||||
|
||||
server.on("error", function(err) {
|
||||
winston.error(err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on("unhandledRejection", (reason, p) => {
|
||||
p.catch(err => {
|
||||
log.error("Exiting due to unhandled rejection!");
|
||||
log.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
process.on("uncaughtException", err => {
|
||||
log.error("Exiting due to uncaught exception!");
|
||||
log.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
server.listen(port, function(err) {
|
||||
var host = server.address().address;
|
||||
var port = server.address().port;
|
||||
winston.info("FilePizza listening on %s:%s", host, port);
|
||||
});
|
||||
|
||||
if (!process.env.QUIET) {
|
||||
app.use(
|
||||
expressWinston.logger({
|
||||
winstonInstance: winston,
|
||||
expressFormat: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.FORCE_SSL) {
|
||||
app.set("forceSSLOptions", {
|
||||
trustXFPHeader: true
|
||||
});
|
||||
|
||||
app.use(forceSSL);
|
||||
}
|
||||
|
||||
app.get("/app.js", require("./middleware/javascript"));
|
||||
app.use(require("./middleware/static"));
|
||||
|
||||
app.use([
|
||||
require("./middleware/bootstrap"),
|
||||
require("./middleware/error"),
|
||||
require("./middleware/react")
|
||||
]);
|
||||
|
||||
io.on("connection", function(socket) {
|
||||
var upload = null;
|
||||
|
||||
socket.on("upload", function(metadata, res) {
|
||||
if (upload) return;
|
||||
db.create(socket).then(u => {
|
||||
upload = u;
|
||||
upload.fileName = metadata.fileName;
|
||||
upload.fileSize = metadata.fileSize;
|
||||
upload.fileType = metadata.fileType;
|
||||
upload.infoHash = metadata.infoHash;
|
||||
res(upload.token);
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("rtcConfig", function(_, res) {
|
||||
ice.getICEServers().then(function(iceServers) {
|
||||
res({ iceServers: iceServers });
|
||||
});
|
||||
});
|
||||
|
||||
socket.on("disconnect", function() {
|
||||
db.remove(upload);
|
||||
});
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import 'babel-polyfill'
|
||||
import React from 'react'
|
||||
import ReactRouter from 'react-router'
|
||||
import routes from './routes'
|
||||
import alt from './alt'
|
||||
import webrtcSupport from 'webrtcsupport'
|
||||
import SupportActions from './actions/SupportActions'
|
||||
|
||||
let bootstrap = document.getElementById('bootstrap').innerHTML
|
||||
alt.bootstrap(bootstrap)
|
||||
|
||||
window.FilePizza = () => {
|
||||
ReactRouter.run(routes, ReactRouter.HistoryLocation, function (Handler) {
|
||||
React.render(<Handler data={bootstrap} />, document)
|
||||
})
|
||||
|
||||
if (!webrtcSupport.support) SupportActions.noSupport()
|
||||
|
||||
let isChrome = navigator.userAgent.toLowerCase().indexOf('chrome') > -1
|
||||
if (isChrome) SupportActions.isChrome()
|
||||
|
||||
}
|
||||
@ -1,71 +0,0 @@
|
||||
import Bootstrap from './Bootstrap'
|
||||
import ErrorPage from './ErrorPage'
|
||||
import FrozenHead from 'react-frozenhead'
|
||||
import React from 'react'
|
||||
import SupportStore from '../stores/SupportStore'
|
||||
import { RouteHandler } from 'react-router'
|
||||
import ga from 'react-google-analytics'
|
||||
|
||||
ga('create', 'UA-62785624-1', 'auto');
|
||||
ga('send', 'pageview');
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.state = SupportStore.getState()
|
||||
|
||||
this._onChange = () => {
|
||||
this.setState(SupportStore.getState())
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SupportStore.listen(this._onChange)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SupportStore.unlisten(this._onChange)
|
||||
}
|
||||
|
||||
render() {
|
||||
return <html lang="en">
|
||||
<FrozenHead>
|
||||
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:url" content="http://file.pizza" />
|
||||
<meta property="og:title" content="FilePizza - Your files, delivered." />
|
||||
<meta property="og:description" content="Peer-to-peer file transfers in your web browser." />
|
||||
<meta property="og:image" content="http://file.pizza/images/fb.png" />
|
||||
<title>FilePizza - Your files, delivered.</title>
|
||||
<link rel="stylesheet" href="/fonts/fonts.css" />
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
<Bootstrap data={this.props.data} />
|
||||
<script src="https://cdn.jsdelivr.net/webtorrent/latest/webtorrent.min.js" />
|
||||
<script src="/app.js" />
|
||||
|
||||
</FrozenHead>
|
||||
|
||||
<body>
|
||||
<div className="container">
|
||||
{this.state.isSupported
|
||||
? <RouteHandler />
|
||||
: <ErrorPage />}
|
||||
</div>
|
||||
<footer className="footer">
|
||||
<p>
|
||||
<script id="fb13c4g" dangerouslySetInnerHTML={{__html: "(function(i){var f,s=document.getElementById(i);f=document.createElement('iframe');f.src='//api.flattr.com/button/view/?uid=kern&button=compact&url=http%3A%2F%2Fgithub.com%2Fkern%2Ffilepizza';f.title='Flattr';f.height=20;f.width=110;f.style.borderWidth=0;s.parentNode.insertBefore(f,s);})('fb13c4g');"}} /> Donations: <strong>1P7yFQAC3EmpvsB7K9s6bKPvXEP1LPoQnY</strong>
|
||||
</p>
|
||||
|
||||
<p className="byline">
|
||||
Cooked up by <a href="http://kern.io" target="_blank">Alex Kern</a> & <a href="http://neeraj.io" target="_blank">Neeraj Baid</a> while eating <strong>Sliver</strong> @ UC Berkeley · <a href="https://github.com/kern/filepizza#faq" target="_blank">FAQ</a> · <a href="https://github.com/kern/filepizza" target="_blank">Fork us</a>
|
||||
</p>
|
||||
</footer>
|
||||
<script>FilePizza()</script>
|
||||
<ga.Initializer />
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
var twilio = require('twilio')
|
||||
var winston = require('winston')
|
||||
|
||||
if (process.env.TWILIO_SID && process.env.TWILIO_TOKEN) {
|
||||
var twilioSID = process.env.TWILIO_SID
|
||||
var twilioToken = process.env.TWILIO_TOKEN
|
||||
var client = twilio(twilioSID, twilioToken)
|
||||
// winston.info('Created Twilio client', { sid: twilioSID, token: twilioToken })
|
||||
} else {
|
||||
var client = null
|
||||
}
|
||||
|
||||
var DEFAULT_ICE_SERVERS = [
|
||||
{
|
||||
url: 'stun:23.21.150.121', // deprecated, replaced by `urls`
|
||||
urls: 'stun:23.21.150.121'
|
||||
}
|
||||
]
|
||||
|
||||
var CACHE_LIFETIME = 5 * 60 * 1000 // 5 minutes
|
||||
var cachedPromise = null
|
||||
|
||||
function clearCache() {
|
||||
cachedPromise = null
|
||||
}
|
||||
|
||||
exports.getICEServers = function () {
|
||||
if (client == null) return Promise.resolve(DEFAULT_ICE_SERVERS)
|
||||
if (cachedPromise) return cachedPromise
|
||||
|
||||
cachedPromise = new Promise(function (resolve, reject) {
|
||||
client.tokens.create({}, function(err, token) {
|
||||
if (err) {
|
||||
winston.error(err)
|
||||
return resolve(DEFAULT_ICE_SERVERS)
|
||||
}
|
||||
|
||||
winston.info('Retrieved ICE servers from Twilio', token.ice_servers)
|
||||
setTimeout(clearCache, CACHE_LIFETIME)
|
||||
resolve(token.ice_servers)
|
||||
})
|
||||
})
|
||||
|
||||
return cachedPromise
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
var express = require('express')
|
||||
var nib = require('nib')
|
||||
var path = require('path')
|
||||
var stylus = require('stylus')
|
||||
|
||||
var CSS_PATH = path.resolve(__dirname, '../../css')
|
||||
var COMPILED_PATH = path.resolve(__dirname, '../../css/index.css')
|
||||
|
||||
var routes = module.exports = new express.Router()
|
||||
|
||||
routes.use(function (req, res, next) {
|
||||
req.url = '/index.css'
|
||||
next()
|
||||
}, stylus.middleware({
|
||||
src: CSS_PATH,
|
||||
dest: CSS_PATH,
|
||||
compile: function (str, path) {
|
||||
return stylus(str)
|
||||
.set('filename', path)
|
||||
.set('compress', true)
|
||||
.use(nib())
|
||||
.import('nib')
|
||||
}
|
||||
}), function (req, res) {
|
||||
res.sendFile(COMPILED_PATH)
|
||||
})
|
||||
@ -1,119 +0,0 @@
|
||||
var db = require('./db')
|
||||
var express = require('express')
|
||||
var expressWinston = require('express-winston')
|
||||
var forceSSL = require('express-force-ssl')
|
||||
var fs = require('fs')
|
||||
var http = require('http')
|
||||
var https = require('https')
|
||||
var ice = require('./ice')
|
||||
var path = require('path')
|
||||
var socketIO = require('socket.io')
|
||||
var winston = require('winston')
|
||||
|
||||
var app = express()
|
||||
|
||||
if (process.env.SECURE) {
|
||||
var server = https.Server({
|
||||
key: fs.readFileSync(process.env.SSL_KEY || 'key.pem'),
|
||||
cert: fs.readFileSync(process.env.SSL_CERT || 'cert.pem')
|
||||
}, app)
|
||||
var port = process.env.PORT || 443
|
||||
var insecurePort = process.env.INSECURE_PORT || 80
|
||||
http.Server(app).listen(80)
|
||||
} else {
|
||||
var server = http.Server(app)
|
||||
var port = process.env.PORT || (process.env.NODE_ENV === 'production' ? 80 : 3000)
|
||||
}
|
||||
|
||||
var io = socketIO(server)
|
||||
io.set('transports', ['polling'])
|
||||
|
||||
var logDir = path.resolve(__dirname, '../log')
|
||||
|
||||
winston.add(winston.transports.DailyRotateFile, {
|
||||
filename: logDir + '/access.log',
|
||||
level: 'info'
|
||||
})
|
||||
|
||||
winston.add(winston.transports.File, {
|
||||
filename: logDir + '/error.log',
|
||||
level: 'error',
|
||||
handleExceptions: true,
|
||||
json: false
|
||||
})
|
||||
|
||||
server.on('error', function (err) {
|
||||
console.error(err.message)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
p.catch(err => {
|
||||
log.error('Exiting due to unhandled rejection!')
|
||||
log.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
})
|
||||
|
||||
process.on('uncaughtException', err => {
|
||||
log.error('Exiting due to uncaught exception!')
|
||||
log.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
server.listen(port, function (err) {
|
||||
var host = server.address().address
|
||||
var port = server.address().port
|
||||
console.log('FilePizza listening on %s:%s', host, port)
|
||||
})
|
||||
|
||||
app.use(expressWinston.logger({
|
||||
winstonInstance: winston,
|
||||
expressFormat: true
|
||||
}))
|
||||
|
||||
if (process.env.FORCE_SSL) {
|
||||
app.set('forceSSLOptions', {
|
||||
trustXFPHeader: true
|
||||
})
|
||||
|
||||
app.use(forceSSL)
|
||||
}
|
||||
|
||||
app.get('/app.js', require('./middleware/javascript'))
|
||||
app.get('/app.css', require('./middleware/css'))
|
||||
app.use(require('./middleware/static'))
|
||||
|
||||
app.use([
|
||||
require('./middleware/bootstrap'),
|
||||
require('./middleware/error'),
|
||||
require('./middleware/react')
|
||||
])
|
||||
|
||||
io.on('connection', function (socket) {
|
||||
|
||||
var upload = null
|
||||
|
||||
socket.on('upload', function (metadata, res) {
|
||||
if (upload) return
|
||||
db.create(socket).then((u) => {
|
||||
upload = u
|
||||
upload.fileName = metadata.fileName
|
||||
upload.fileSize = metadata.fileSize
|
||||
upload.fileType = metadata.fileType
|
||||
upload.infoHash = metadata.infoHash
|
||||
res(upload.token)
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('rtcConfig', function (_, res) {
|
||||
ice.getICEServers().then(function (iceServers) {
|
||||
res({ iceServers: iceServers })
|
||||
})
|
||||
})
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
db.remove(upload)
|
||||
})
|
||||
|
||||
})
|
||||
Loading…
Reference in New Issue