mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-29 09:35:54 +00:00
quick and dirty comments
This commit is contained in:
parent
050f2df426
commit
4b8bf805cd
9 changed files with 431 additions and 2 deletions
|
|
@ -131,6 +131,8 @@ time { color: #444 }
|
|||
#prev { float: left }
|
||||
#next { float: right }
|
||||
|
||||
#comments { display: none }
|
||||
|
||||
footer { text-align: center
|
||||
; font-size: 1.2em
|
||||
; margin: 0
|
||||
|
|
|
|||
54
assets/request.js
Normal file
54
assets/request.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
(function() {
|
||||
if (typeof SJS === 'undefined') SJS = {}
|
||||
var load, _jsonpCounter = 1
|
||||
SJS.request = function(options, cb) { // jsonp request, quacks like mikeal's request module
|
||||
var jsonpCallbackName = '_jsonpCallback' + _jsonpCounter++
|
||||
, url = options.uri + '?callback=SJS.' + jsonpCallbackName
|
||||
SJS[jsonpCallbackName] = function(obj) {
|
||||
cb(null, obj)
|
||||
setTimeout(function() { delete SJS[jsonpCallbackName] }, 0)
|
||||
}
|
||||
load(url)
|
||||
}
|
||||
|
||||
// bootstrap loader from LABjs
|
||||
load = function(url) {
|
||||
var oDOC = document
|
||||
, handler
|
||||
, head = oDOC.head || oDOC.getElementsByTagName("head")
|
||||
|
||||
// loading code borrowed directly from LABjs itself
|
||||
// (now removes script elem when done and nullifies its reference --sjs)
|
||||
setTimeout(function () {
|
||||
if ("item" in head) { // check if ref is still a live node list
|
||||
if (!head[0]) { // append_to node not yet ready
|
||||
setTimeout(arguments.callee, 25)
|
||||
return
|
||||
}
|
||||
head = head[0]; // reassign from live node list ref to pure node ref -- avoids nasty IE bug where changes to DOM invalidate live node lists
|
||||
}
|
||||
var scriptElem = oDOC.createElement("script"),
|
||||
scriptdone = false
|
||||
scriptElem.onload = scriptElem.onreadystatechange = function () {
|
||||
if ((scriptElem.readyState && scriptElem.readyState !== "complete" && scriptElem.readyState !== "loaded") || scriptdone) {
|
||||
return false
|
||||
}
|
||||
scriptElem.onload = scriptElem.onreadystatechange = null
|
||||
scriptElem.parentNode.removeChild(scriptElem)
|
||||
scriptElem = null
|
||||
scriptdone = true
|
||||
};
|
||||
scriptElem.src = url
|
||||
head.insertBefore(scriptElem, head.firstChild)
|
||||
}, 0)
|
||||
|
||||
// required: shim for FF <= 3.5 not having document.readyState
|
||||
if (oDOC.readyState == null && oDOC.addEventListener) {
|
||||
oDOC.readyState = "loading"
|
||||
oDOC.addEventListener("DOMContentLoaded", function handler() {
|
||||
oDOC.removeEventListener("DOMContentLoaded", handler, false)
|
||||
oDOC.readyState = "complete"
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
}())
|
||||
14
blog.rb
14
blog.rb
|
|
@ -15,7 +15,9 @@ end
|
|||
|
||||
template = File.read(File.join('templates', 'blog', 'post.html'))
|
||||
|
||||
Posts = JSON.parse(File.read(File.join(srcdir, 'posts.json')))
|
||||
# read posts
|
||||
posts_file = File.join(srcdir, 'posts.json')
|
||||
Posts = JSON.parse(File.read(posts_file))
|
||||
posts = Posts['published'].map do |filename|
|
||||
lines = File.readlines(File.join(srcdir, filename))
|
||||
post = { :filename => filename }
|
||||
|
|
@ -33,24 +35,32 @@ posts = Posts['published'].map do |filename|
|
|||
end
|
||||
post[:content] = lines.join
|
||||
post[:body] = RDiscount.new(post[:content]).to_html
|
||||
# comments on by default
|
||||
post[:comments] = true if post[:comments].nil?
|
||||
post
|
||||
end
|
||||
|
||||
# generate posts
|
||||
posts.each_with_index do |post, i|
|
||||
post[:html] = Mustache.render(template, { :title => post[:title],
|
||||
:post => post,
|
||||
:previous => i < posts.length - 1 && posts[i + 1],
|
||||
:next => i > 0 && posts[i - 1]
|
||||
:next => i > 0 && posts[i - 1],
|
||||
:comments => post[:comments]
|
||||
})
|
||||
end
|
||||
|
||||
# generate landing page
|
||||
index_template = File.read(File.join('templates', 'blog', 'index.html'))
|
||||
index_html = Mustache.render(index_template, { :posts => posts,
|
||||
:post => posts.first,
|
||||
:previous => posts[1]
|
||||
})
|
||||
|
||||
# write landing page
|
||||
File.open(File.join(destdir, 'index.html'), 'w') {|f| f.puts(index_html) }
|
||||
|
||||
# write posts
|
||||
posts.each do |post|
|
||||
File.open(File.join(destdir, post[:filename]), 'w') {|f| f.puts(post[:html]) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var discussion = document.getElementById('discussion')
|
||||
, discussionToggle = document.getElementById('discussion-toggle')
|
||||
, hidden = true
|
||||
discussionToggle.onclick = function() {
|
||||
hidden = !hidden
|
||||
discussion.style.display = hidden ? 'none' : 'block'
|
||||
discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑'
|
||||
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
|
||||
html('comments', body)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<header>
|
||||
<h1><a href=index.html>sjs' blog</a></h1>
|
||||
|
|
@ -274,6 +296,21 @@ addLineNumbersToAllGists();
|
|||
<a id=prev href=a-preview-of-mach-o-file-generation.html>← A preview of Mach-O file generation</a>
|
||||
<br style=clear:both>
|
||||
</div>
|
||||
<div class=center><a id=discussion-toggle href=#>↓ show discussion ↓</a></div>
|
||||
<div id=discussion>
|
||||
<div id=comment-form>
|
||||
<form method=post action=http://bohodev.net:8000/comment>
|
||||
<input name=from type=hidden value=37signals-chalk-dissected.html>
|
||||
<p>Name: <input name=name size=30></p>
|
||||
<p>URL: <input name=url size=30></p>
|
||||
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
|
||||
<p><input type=submit value=Add to discussion></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id=comments>
|
||||
<img id=discussion-spinner src=../assets/spinner.gif>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href=https://twitter.com/_sjs>@_sjs</a>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var discussion = document.getElementById('discussion')
|
||||
, discussionToggle = document.getElementById('discussion-toggle')
|
||||
, hidden = true
|
||||
discussionToggle.onclick = function() {
|
||||
hidden = !hidden
|
||||
discussion.style.display = hidden ? 'none' : 'block'
|
||||
discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑'
|
||||
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
|
||||
html('comments', body)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<header>
|
||||
<h1><a href=index.html>sjs' blog</a></h1>
|
||||
|
|
@ -69,6 +91,21 @@ straightforward, an example is in asm/binary.rb, in the #output method.</p>
|
|||
<a id=next href=37signals-chalk-dissected.html>37signals' Chalk Dissected →</a>
|
||||
<br style=clear:both>
|
||||
</div>
|
||||
<div class=center><a id=discussion-toggle href=#>↓ show discussion ↓</a></div>
|
||||
<div id=discussion>
|
||||
<div id=comment-form>
|
||||
<form method=post action=http://bohodev.net:8000/comment>
|
||||
<input name=from type=hidden value=a-preview-of-mach-o-file-generation.html>
|
||||
<p>Name: <input name=name size=30></p>
|
||||
<p>URL: <input name=url size=30></p>
|
||||
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
|
||||
<p><input type=submit value=Add to discussion></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id=comments>
|
||||
<img id=discussion-spinner src=../assets/spinner.gif>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href=https://twitter.com/_sjs>@_sjs</a>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var discussion = document.getElementById('discussion')
|
||||
, discussionToggle = document.getElementById('discussion-toggle')
|
||||
, hidden = true
|
||||
discussionToggle.onclick = function() {
|
||||
hidden = !hidden
|
||||
discussion.style.display = hidden ? 'none' : 'block'
|
||||
discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑'
|
||||
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
|
||||
html('comments', body)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<header>
|
||||
<h1><a href=index.html>sjs' blog</a></h1>
|
||||
|
|
@ -292,6 +314,21 @@ would almost have a useful Mach object file.)</i></p>
|
|||
<a id=next href=a-preview-of-mach-o-file-generation.html>A preview of Mach-O file generation →</a>
|
||||
<br style=clear:both>
|
||||
</div>
|
||||
<div class=center><a id=discussion-toggle href=#>↓ show discussion ↓</a></div>
|
||||
<div id=discussion>
|
||||
<div id=comment-form>
|
||||
<form method=post action=http://bohodev.net:8000/comment>
|
||||
<input name=from type=hidden value=basics-of-the-mach-o-file-format.html>
|
||||
<p>Name: <input name=name size=30></p>
|
||||
<p>URL: <input name=url size=30></p>
|
||||
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
|
||||
<p><input type=submit value=Add to discussion></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id=comments>
|
||||
<img id=discussion-spinner src=../assets/spinner.gif>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href=https://twitter.com/_sjs>@_sjs</a>
|
||||
</footer>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,28 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var discussion = document.getElementById('discussion')
|
||||
, discussionToggle = document.getElementById('discussion-toggle')
|
||||
, hidden = true
|
||||
discussionToggle.onclick = function() {
|
||||
hidden = !hidden
|
||||
discussion.style.display = hidden ? 'none' : 'block'
|
||||
discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑'
|
||||
SJS.request({uri: 'http://bohodev.net:8000/comments/'}, function(err, body) {
|
||||
html('comments', body)
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<header>
|
||||
<h1><a href=index.html>sjs' blog</a></h1>
|
||||
|
|
@ -140,6 +162,21 @@ of the Mach-O file format</a></i><p>
|
|||
<a id=next href=basics-of-the-mach-o-file-format.html>Basics of the Mach-O file format →</a>
|
||||
<br style=clear:both>
|
||||
</div>
|
||||
<div class=center><a id=discussion-toggle href=#>↓ show discussion ↓</a></div>
|
||||
<div id=discussion>
|
||||
<div id=comment-form>
|
||||
<form method=post action=http://bohodev.net:8000/comment>
|
||||
<input name=from type=hidden value=working-with-c-style-structs-in-ruby.html>
|
||||
<p>Name: <input name=name size=30></p>
|
||||
<p>URL: <input name=url size=30></p>
|
||||
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
|
||||
<p><input type=submit value=Add to discussion></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id=comments>
|
||||
<img id=discussion-spinner src=../assets/spinner.gif>
|
||||
</div>
|
||||
</div>
|
||||
<footer>
|
||||
<a href=https://twitter.com/_sjs>@_sjs</a>
|
||||
</footer>
|
||||
|
|
|
|||
174
discussd/discussd.js
Executable file
174
discussd/discussd.js
Executable file
|
|
@ -0,0 +1,174 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
var fs = require('fs')
|
||||
, http = require('http')
|
||||
, keys = require('keys')
|
||||
, DefaultOptions = { host: 'localhost'
|
||||
, port: 2020
|
||||
, postsFile: 'posts.json'
|
||||
}
|
||||
|
||||
function main() {
|
||||
var options = parseArgs(DefaultOptions)
|
||||
, db = new keys.Dirty('./discuss.dirty')
|
||||
, context = { db: db
|
||||
, posts: null
|
||||
}
|
||||
, server = http.createServer(requestHandler(context))
|
||||
, loadPosts = function(cb) {
|
||||
readJSON(options.postsFile, function(err, posts) {
|
||||
if (err) {
|
||||
console.error('failed to parse posts file, is it valid JSON?')
|
||||
console.dir(e)
|
||||
process.exit(1)
|
||||
}
|
||||
context.posts = posts.published
|
||||
var n = context.posts.length
|
||||
console.log((context.posts === null ? '' : 're') + 'loaded ' + n + ' posts...')
|
||||
if (typeof cb == 'function') cb()
|
||||
})
|
||||
}
|
||||
, listen = function() {
|
||||
console.log(process.argv[0] + ' listening on ' + options.host + ':' + options.port)
|
||||
server.listen(options.port, options.host)
|
||||
}
|
||||
loadPosts(function() {
|
||||
fs.watchFile(options.postsFile, loadPosts)
|
||||
if (db._loaded) {
|
||||
listen()
|
||||
} else {
|
||||
db.db.on('load', listen)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function readJSON(f, cb) {
|
||||
fs.readFile(f, function(err, buf) {
|
||||
var data
|
||||
if (!err) {
|
||||
try {
|
||||
data = JSON.parse(buf.toString())
|
||||
} catch (e) {
|
||||
err = e
|
||||
}
|
||||
}
|
||||
cb(err, data)
|
||||
})
|
||||
}
|
||||
|
||||
function requestHandler(context) {
|
||||
function addComment(data) {
|
||||
if (missingParams(data) || context.posts.indexOf(data.post) === -1) {
|
||||
console.log('missing params or invalid post title in ' + JSON.stringify(data, null, 2))
|
||||
return false
|
||||
}
|
||||
var comments = context.db.get(data.post) || []
|
||||
comments.push({ name: data.name
|
||||
, email: data.email
|
||||
, body: data.body
|
||||
, timestamp: Date.now()
|
||||
})
|
||||
context.db.set(data.post, comments)
|
||||
console.log('[' + new Date() + '] add comment ' + JSON.stringify(data, null, 2))
|
||||
return true
|
||||
}
|
||||
|
||||
return function(req, res) {
|
||||
var body = ''
|
||||
, m
|
||||
if (req.method === 'POST' && req.url.match(/^\/comment\/?$/)) {
|
||||
req.on('data', function(chunk) { body += chunk })
|
||||
req.on('end', function() {
|
||||
var data
|
||||
try {
|
||||
data = JSON.parse(body)
|
||||
} catch (x) {
|
||||
badRequest(res)
|
||||
return
|
||||
}
|
||||
if (!addComment(data)) {
|
||||
badRequest(res)
|
||||
return
|
||||
}
|
||||
res.writeHead(204)
|
||||
res.end()
|
||||
// TODO mail watchers about the comment
|
||||
})
|
||||
} else if (req.method === 'GET' && (m = req.url.match(/^\/comments\/(.*)$/))) {
|
||||
var post = m[1]
|
||||
, comments
|
||||
, s
|
||||
if (context.posts.indexOf(post) === -1) {
|
||||
badRequest(res)
|
||||
return
|
||||
}
|
||||
comments = context.db.get(post) || []
|
||||
s = JSON.stringify({comments: comments})
|
||||
res.writeHead(200, { 'content-type': 'appliaction/json'
|
||||
, 'content-length': s.length
|
||||
})
|
||||
res.end(s)
|
||||
} else {
|
||||
console.log('unhandled request')
|
||||
console.dir(req)
|
||||
badRequest(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(defaults) {
|
||||
var expectingArg
|
||||
, options = Object.keys(defaults).reduce(function(os, k) {
|
||||
os[k] = defaults[k]
|
||||
return os
|
||||
}, {})
|
||||
process.argv.slice(2).forEach(function(arg) {
|
||||
if (expectingArg) {
|
||||
options[expectingArg] = arg
|
||||
expectingArg = null
|
||||
} else {
|
||||
// remove leading dashes
|
||||
while (arg.charAt(0) === '-') {
|
||||
arg = arg.slice(1)
|
||||
}
|
||||
switch (arg) {
|
||||
case 'h':
|
||||
case 'host':
|
||||
expectingArg = 'host'
|
||||
break
|
||||
|
||||
case 'p':
|
||||
case 'port':
|
||||
expectingArg = 'port'
|
||||
break
|
||||
|
||||
default:
|
||||
console.warn('unknown option: ' + arg + ' (setting anyway)')
|
||||
expectingArg = arg
|
||||
}
|
||||
}
|
||||
})
|
||||
return options
|
||||
}
|
||||
|
||||
function badRequest(res) {
|
||||
var s = 'bad request'
|
||||
res.writeHead(400, { 'content-type': 'text/plain'
|
||||
, 'content-length': s.length
|
||||
})
|
||||
res.end(s)
|
||||
}
|
||||
|
||||
var missingParams = (function() {
|
||||
var requiredParams = 'name email body'.split(' ')
|
||||
return function(d) {
|
||||
var anyMissing = false
|
||||
requiredParams.forEach(function(p) {
|
||||
var v = (d[p] || '').trim()
|
||||
if (!v) anyMissing = true
|
||||
})
|
||||
return anyMissing
|
||||
}
|
||||
}())
|
||||
|
||||
if (module == require.main) main()
|
||||
|
|
@ -14,6 +14,30 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
|
||||
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
|
||||
})();
|
||||
{{#comments}}
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', ready, false)
|
||||
} else if (window.attachEvent) {
|
||||
window.attachEvent('onload', ready)
|
||||
}
|
||||
function ready() {
|
||||
function html(id, h) {
|
||||
document.getElementById(id).innerHTML = h
|
||||
}
|
||||
|
||||
var discussion = document.getElementById('discussion')
|
||||
, discussionToggle = document.getElementById('discussion-toggle')
|
||||
, hidden = true
|
||||
discussionToggle.onclick = function() {
|
||||
hidden = !hidden
|
||||
discussion.style.display = hidden ? 'none' : 'block'
|
||||
discussionToggle.innerHTML = hidden ? '↓ show discussion ↓' : '↑ hide discussion ↑'
|
||||
SJS.request({uri: 'http://bohodev.net:8000/comments/{{filename}}'}, function(err, body) {
|
||||
html('comments', body)
|
||||
})
|
||||
}
|
||||
}
|
||||
{{/comments}}
|
||||
</script>
|
||||
<header>
|
||||
<h1><a href=index.html>sjs' blog</a></h1>
|
||||
|
|
@ -40,6 +64,23 @@ _gaq.push( ['_setAccount', 'UA-214054-5']
|
|||
{{/next}}
|
||||
<br style=clear:both>
|
||||
</div>
|
||||
<div class=center><a id=discussion-toggle href=#>↓ show discussion ↓</a></div>
|
||||
{{#comments}}
|
||||
<div id=discussion>
|
||||
<div id=comment-form>
|
||||
<form method=post action=http://bohodev.net:8000/comment>
|
||||
<input name=from type=hidden value={{#post}}{{filename}}{{/post}}>
|
||||
<p>Name: <input name=name size=30></p>
|
||||
<p>URL: <input name=url size=30></p>
|
||||
<p>Thoughts: <textarea name=body cols=40 rows=5></textarea></p>
|
||||
<p><input type=submit value=Add to discussion></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id=comments>
|
||||
<img id=discussion-spinner src=../assets/spinner.gif>
|
||||
</div>
|
||||
</div>
|
||||
{{/comments}}
|
||||
<footer>
|
||||
<a href=https://twitter.com/_sjs>@_sjs</a>
|
||||
</footer>
|
||||
|
|
|
|||
Loading…
Reference in a new issue