Compare commits

..
24 Commits
Author SHA1 Message Date
gisle 853076fd5e Fixed story chapters being unresponsive if there are no comments.
the build was successful
2019-03-17 21:44:07 +01:00
gisle 06bf2b67a2 Added correction of Ctrl+Bs in log posts showing as ** due to markdown being particular about where the bolds should be.
the build was successful
2019-03-17 21:10:58 +01:00
gisle 40259b8839 Fixed validation of fictionalDate input in add/edit story.
the build was successful
2019-03-17 20:29:26 +01:00
gisle 3fc9d5de7f Fixed validation of fictionalDate input in create/edit chapter modals. 2019-03-17 20:25:03 +01:00
gisle c69e528105 Added UI for Commit entries to change history
the build was successful
2019-03-17 20:15:35 +01:00
gisle 6ea9632623 Comments are pretty much done!
the build was successful
2019-03-17 20:00:49 +01:00
gisle 490725ecd2 Long time no see. A bunch of stuff I cba to summarize.
the build was successful
2019-03-07 23:13:46 +01:00
gisle 494d0404ed Merge branch '1.2' 2019-01-19 11:19:41 +01:00
gisle 624f7046af server: Added resilience to trust proxy resolution to handle IP changes and bad start order. 2019-01-19 11:18:44 +01:00
gisle 8bfc0dad14 api: Fixed fictionalDate being Jan 1, 1970 instead of null if none is set.
the build was successful
2019-01-17 21:12:05 +01:00
gisle 3ed3e50486 api: Fixed fatal error if Comment.character is null.
the build was successful
2019-01-17 21:06:43 +01:00
gisle 5260d96f37 api: Added Comment and hooked it up to the Story.find response.
the build was successful
2019-01-17 20:51:26 +01:00
gisle 8b3d642104 logs: Added default limit that's cleared by adding a filter or clicking show more at the bottom.
the build was successful
2019-01-12 15:35:21 +01:00
gisle 46369f84bd data: Fixed incosnsitensies and undefines in history.
the build was successful
2019-01-12 14:34:59 +01:00
gisle 68f3effa8d story-content: Added new checks for story tag management so any logged-in user can view the form if the story is open.
the build was successful
2019-01-12 12:18:55 +01:00
gisle 4657ae2aec logs: Added prioritization of filter names starting with the search query, for example bringing 'Li' higher up when the search query is 'li'.
the build was successful
2019-01-05 11:46:46 +01:00
gisle f134d57b79 data: Fixed centiseconds being used instead of seconds in change history entry timestamps.
the build was successful
2019-01-05 11:31:25 +01:00
gisle 9b4da98ac4 logs-content: Added escaping of back-ticks (`) to avoid monospace in log posts where a nick containing a back-tick has been copied into the text.
the build was successful
2019-01-05 11:28:22 +01:00
gisle 599ca83f0f logs-content: Fixed quote detection in text post not detecting double-apostrpohes (KVIrc has anti-RP sentiments among their dev team)
the build was successful
2019-01-05 11:19:45 +01:00
gisle b952007f3b logs-content: Fixed character names being omitted on multipart posts where an action post follows a text post.
the build was successful
2019-01-05 11:16:29 +01:00
gisle c0e74f4756 server: Added DNS resolution when setting trust proxy since IPs are temporary in Docker.
the build failed
2019-01-05 11:02:38 +01:00
gisle 68a8e7ef97 story-content: Added hiding of first chapter's title if it matches story name to conserve vertical space.
the build was successful
2019-01-05 10:46:16 +01:00
gisle 5b81f7ff0d data: Fixed unknown nick list sometimes disappearing in character nicks modal.
the build was successful
2018-12-09 12:21:47 +01:00
gisle 78a14ae86b data: Increased limit on change list to 2000. 2018-12-09 12:21:15 +01:00
47 changed files with 1286 additions and 70 deletions
+5
View File
@@ -0,0 +1,5 @@
import moment from "moment";
<if(input.value != null)>
<span class=["date", input.class]>${(input.utc ? moment.utc(input.value) : moment(input.value)).format(input.format || "MMM D, YYYY")}</span>
</if>
+1
View File
@@ -1,3 +1,4 @@
<div class=["markdown-content", input.class]>
<include(input.renderBody) />
<include(state.render) />
</div>
+3
View File
@@ -47,6 +47,9 @@ module.exports = class {
if (this.input.enabled) {
window.removeEventListener("keydown", this.onPress)
}
this.mounted = false
this.opened = false
}
/**
+68
View File
@@ -51,3 +51,71 @@ body {
background-color: #555;
color: #000;
}
/*
* Tooltip magic.
*/
.tooltip {
position: relative;
border-bottom: 0.5px dotted; /* If you want dots under the hoverable text */
span {
cursor: default;
}
> .tooltip-content {
visibility: hidden;
width: 400px;
text-align: left;
padding: 0.25em 1ch;
position: absolute;
z-index: 11000;
pointer-events: none;
top: 0;
left: 105%;
border: 1px solid;
> h1 {
font-size: 1.5em;
margin: 0;
text-align: center;
border: none;
}
> h2 {
font-size: 1em;
margin: 0;
margin-bottom: 0.15em;
margin-top: -0.25em;
font-weight: 200;
opacity: 0.5;
text-align: center;
border: none;
}
p:last-of-type {
padding-bottom: 0;
}
}
}
.no-tooltip {
position: relative;
}
.tooltip:hover {
> .tooltip-content {
visibility: visible;
}
}
@media screen and (max-width: 1200px) {
.tooltip {
> .tooltip-content {
visibility: hidden !important;
}
}
}
+3 -3
View File
@@ -92,7 +92,7 @@ body.theme-data {
}
div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, body.theme-logs div.markdown-content table, body.theme-logs div.markdown-content pre {
div.markdown-content .orange, body.theme-logs .color-blockquote, body.theme-logs div.markdown-content blockquote, body.theme-logs div.markdown-content table, body.theme-logs div.markdown-content pre {
background-color: rgba(221, 170, 17, 0.125);
color: #DA1;
@@ -100,7 +100,7 @@ div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, b
color: #DA1;
}
}
div.markdown-content .blue, body.theme-story div.markdown-content blockquote, body.theme-story div.markdown-content table, body.theme-story div.markdown-content pre {
div.markdown-content .blue, body.theme-story .color-blockquote, body.theme-story div.markdown-content blockquote, body.theme-story div.markdown-content table, body.theme-story div.markdown-content pre {
background-color: rgba(34, 187, 255, 0.125);
color: #4BF;
@@ -108,7 +108,7 @@ div.markdown-content .blue, body.theme-story div.markdown-content blockquote, bo
color: #4BF; //
}
}
div.markdown-content .green, body.theme-mapp div.markdown-content blockquote, body.theme-mapp div.markdown-content table, body.theme-mapp div.markdown-content pre {
div.markdown-content .green, body.theme-mapp .color-blockquote, body.theme-mapp div.markdown-content blockquote, body.theme-mapp div.markdown-content table, body.theme-mapp div.markdown-content pre {
background-color: rgba(17,255,136, 0.125);
color: #1F8;
+17 -1
View File
@@ -72,6 +72,21 @@ module.exports = class {
break
}
case "Comment": {
const storyKey = data.keys.find(k => k.model === "Story") || {}
const chapterKey = data.keys.find(k => k.model === "Chapter")
const chapter = data.objects.find(o => o.type === "Chapter") || {}
const comment = data.objects.find(o => o.type === "Comment") || {}
this.state.operation += ' ' + comment.characterName + " comment in chapter"
this.state.links = [{
text: chapter.title || `ID (${chapterKey.id})`,
href: `/story/${storyKey.id}#${chapterKey.id}`,
}]
break
}
case "Log": {
const key = data.keys.find(k => k.id !== "*")
@@ -113,6 +128,7 @@ const opText = {
edit: "edited",
import: "imported",
tag: "tagged",
untag: "untagged",
"move-in": "moved in",
"move-out": "moved-out",
"move-out": "moved out",
}
@@ -1,7 +1,7 @@
import moment from "moment";
<div class="change">
<span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:SS")}] * </span>
<span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:ss")}] * </span>
<span class="author color-text">${input.data.author} </span>
<span class="text color-text">${state.operation} </span>
<span for(link in state.links | status-var=loop) key=link.href class="link">
@@ -10,7 +10,7 @@
</div>
<label>Current Nicks</label>
<div for(nick in input.character.nicks) class="existing-nick-row" >
<div for(nick in input.character.nicks) class="existing-nick-row" key=nick >
<div class="content color-text">${nick}</div>
<button disabled=state.loading on-click("removeNick", nick)>Remove</button>
</div>
@@ -13,6 +13,7 @@ module.exports = class {
removed: false,
multipart: false,
anchored: false,
prevWasText: false,
}
this.mounted = false;
@@ -48,6 +49,7 @@ module.exports = class {
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time)
this.state.multipart = (input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
this.state.prevWasText = (input.prev.kind === "text");
this.updatePost(input)
}
@@ -79,14 +81,28 @@ module.exports = class {
this.state.name = input.post.nick
this.state.nameSuffix = ""
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "_").trim();
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim();
// Fix Ctrl-Bs
let left = false
this.state.text = this.state.text.replace(/\s*\*\*\s*/g, str => {
left = !left
if (left) {
return " **"
} else if (!left) {
return "** "
}
return str
})
if (this.state.text.startsWith("|")) {
this.state.name = ""
this.state.text = this.state.text.slice(1).trim();
}
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("|")) {
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
const colonIndex = this.state.text.indexOf(": ");
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
this.state.text = this.state.text.replace(": ", ": \"") + "\"";
@@ -26,7 +26,7 @@
<markdown class="post-content post-scene" source=state.text />
</div>
<div if(input.post.kind === "action") class="post-body color-text">
$ const prefix = (!state.multipart ? state.shortName + state.nameSuffix + " " : "");
$ const prefix = ((!state.multipart || state.prevWasText) ? state.shortName + state.nameSuffix + " " : "");
<markdown class="post-content post-action" source=(prefix + state.text) />
</div>
<div if(input.post.kind === "text") class="post-body color-text">
@@ -49,9 +49,9 @@ module.exports = class {
const search = this.state.search.toLocaleLowerCase()
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => a.name.localeCompare(b.name))
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => a.localeCompare(b))
this.state.characters = input.characters.filter(c => !this.state.activeMap.characters[c.id]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
this.state.channels = input.channels.filter(c => !this.state.activeMap.channels[c.name]).filter(c => this.matches(c.name, search)).sort((a,b) => compareFilters(a.name, b.name, search))
this.state.eventNames = input.eventNames.filter(e => !this.state.activeMap.events[e]).filter(e => this.matches(e, search)).sort((a,b) => compareFilters(a, b, search))
}
updateActive({filter, characters, channels, eventNames}) {
@@ -126,4 +126,29 @@ module.exports = class {
return str.toLowerCase().indexOf(search) !== -1
}
}
/**
*
* @param {string} a
* @param {string} b
* @param {string} search
*/
function compareFilters(a, b, search) {
if (search != "") {
if (search.charAt(0) != "#" && a.charAt(0) == "#" && b.charAt(0) == "#") {
search = '#' + search
}
const aStarts = a.toLocaleLowerCase().startsWith(search)
const bStarts = b.toLocaleLowerCase().startsWith(search)
if (aStarts && !bStarts) {
return -1
} else if (bStarts && !aStarts) {
return 1
}
}
return a.localeCompare(b)
}
+12 -1
View File
@@ -24,11 +24,12 @@ module.exports = class {
} else {
this.state.filter = Object.assign({}, this.state.filter, {search: null})
}
} else {
this.state.filter = Object.assign({}, this.state.filter, {[type]: (this.state.filter[type] || []).filter(f => f !== filter).concat(filter)})
}
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.updateQuery(this.state.filter)
this.refresh()
@@ -67,6 +68,9 @@ module.exports = class {
if (filter.search) {
queries.push("search=" + encodeURIComponent(filter.search))
}
if (filter.limit) {
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
}
if (queries.length > 0) {
history.replaceState("", "", `/logs/?${queries.join("&")}`)
@@ -75,6 +79,13 @@ module.exports = class {
}
}
clearLimit() {
this.state.filter = Object.assign({}, this.state.filter, {limit: 0})
this.refresh()
this.updateQuery(this.state.filter)
}
refresh() {
logHeaderApi.list(this.state.filter).then(logs => {
this.state.logs = logs
+6 -1
View File
@@ -1,8 +1,13 @@
<background src="/assets/images/bg.png" opacity=0.25 />
<logs-menu key="menu" on-select("open") selected=(input.selected || {}) user=input.user filter=state.filter characters=input.characters />
<filter-menu on-add("addFilter") on-remove("removeFilter") filter=state.filter characters=input.characters channels=input.channels eventNames=input.eventNames />
<main class="with-right">
<main class="logs-page with-right">
<logs-table logs=state.logs on-addfilter("addFilter") />
<if(state.filter.limit > 0)>
<div class="show-more-button" on-click("clearLimit")>
<span>Show All</span>
</div>
</if>
</main>
<add-log-modal enabled=(state.modal === "log.add") on-close("close") />
<import-log-modal enabled=(state.modal === "log.import") on-close("close") />
@@ -0,0 +1,17 @@
main.logs-page {
div.show-more-button {
margin: 1em 0;
padding: 0.5em 2em;
font-size: 2em;
text-align: center;
user-select: none;
color: #DA1;
}
div.show-more-button:hover {
cursor: pointer;
text-decoration: underline;
}
}
@@ -50,6 +50,13 @@ module.exports = class {
break
}
case "title": {
this.state.color = "color-primary"
this.state.text = value
break
}
default: {
this.state.color = "color-menu"
@@ -1,6 +1,18 @@
<if(state.text != null && state.text != "")>
<div class=["chapter-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip>
<a if(state.href != null) href=state.href>${state.text}</a>
<span else>${state.text}</span>
<span else class="tooltip">
<span>${state.text}</span>
<div if(input.character != null) class="tooltip-content color-highlight-dark color-primary">
<h1>${input.character.name}</h1>
<h2>${input.character.author}</h2>
<markdown source=input.character.description />
</div>
<div else-if(input.ukAuthor != null) class="tooltip-content color-highlight-dark color-primary">
<h1>(Unknown Character)</h1>
<h2>${input.ukAuthor}</h2>
</div>
</span>
</div>
</if>
@@ -13,6 +13,10 @@ div.chapter-meta {
a:hover {
border-bottom: 0.5px solid;
}
.tooltip {
border-bottom: 0.25px dotted;
}
}
div.chapter-meta.weak {
@@ -3,9 +3,42 @@ module.exports = class {
this.state = {
modal: null,
removed: false,
commentLabels: {
Article: "Comment",
Message: "Message",
Chat: "Chat Message",
},
selectedComment: null,
}
}
onOpenComment(comment, modal) {
this.state.selectedComment = comment
this.state.modal = modal
}
addComment(comment) {
this.updateChapter({
comments: this.input.chapter.comments.concat(comment),
})
}
editComment(comment) {
const comments = this.input.chapter.comments.slice()
const index = comments.findIndex(c => c.id === comment.id)
if (index != -1) {
comments[index] = comment
} else {
comments.push(comment)
}
this.updateChapter({comments})
}
removeComment(comment) {
this.updateChapter({comments: this.input.chapter.comments.filter(c => c.id !== comment.id)})
}
updateChapter(data) {
this.emit("edit", data)
}
@@ -1,6 +1,6 @@
<div class="story-chapter">
<a class="anchor" id=input.chapter.id />
<h1 if(input.chapter.title) class="title color-primary">${input.chapter.title}</h1>
<h1 if(input.chapter.title && !input.hideTitle) class="title color-primary">${input.chapter.title}</h1>
<div class="metadata">
<div class="options color-menu">
<if-permitted user=input.user author=input.chapter.author permission="chapter.edit">
@@ -16,10 +16,25 @@
<chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
<chapter-meta weak kind="date" value=input.chapter.createdDate />
<chapter-meta weak kind="author" value=input.chapter.author />
<chapter-meta weak kind="message" value=input.chapter.commentMode />
</div>
<markdown class="chapter-content color-text" source=input.chapter.source />
<div class=["comment-section", "type-" + (input.chapter.commentMode.toLowerCase() || "disabled")]>
<comment for(comment in input.chapter.comments) key=comment.id
comment=comment
locked=input.chapter.commentsLocked
mode=input.chapter.commentMode
user=input.user
on-open("onOpenComment", comment)
/>
<if-permitted if(input.chapter.canComment) user=input.user author=input.chapter.author permission="chapter.edit">
<div class="add-comment color-menu" on-click("open", "addComment") >Add ${state.commentLabels[input.chapter.commentMode] || "Comment"}</div>
</if-permitted>
</div>
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
<create-comment-modal enabled=(state.modal === "addComment") chapter=input.chapter user=input.user on-close("close") on-add("addComment") />
<edit-comment-modal enabled=(state.modal === "editComment") chapter=input.chapter comment=state.selectedComment user=input.user on-close("close") on-edit("editComment") />
<remove-comment-modal enabled=(state.modal === "removeComment") comment=state.selectedComment user=input.user on-close("close") on-remove("removeComment", state.selectedComment) />
<move-chapter-modal enabled=(state.modal === "move") chapter=input.chapter user=input.user on-close("close") on-move("moveChapter") />
<remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") />
</div>
@@ -49,4 +49,30 @@
div.chapter-content {
font-size: 1.25em;
}
div.comment-section {
margin: 1em 4ch 2em 4ch;
div.add-comment {
text-align: center;
margin-top: 1em;
padding: 1em 0;
font-size: 1.5em;
border: 1px solid;
opacity: 0.5;
cursor: pointer;
}
div.add-comment:hover {
opacity: 1;
}
div.add-comment:first-child {
margin-top: 0;
}
}
@media screen and (max-width: 1200px) {
div.comment-section {
margin: 1em 2ch 2em 2ch;
}
}
}
@@ -0,0 +1,18 @@
module.exports = class {
onCreate(input) {
this.state = {
showCharacter: false,
paragraphs: input.comment.source.split('\n').filter(l => l.length > 1),
}
}
onInput(input) {
if (this.state) {
this.state.paragraphs = input.comment.source.split('\n').filter(l => l.length > 1)
}
}
toggle(key) {
this.state[key] = !this.state[key]
}
}
@@ -0,0 +1,117 @@
import moment from "moment"
<div class=["comment", "mode-" + input.mode.toLowerCase()]>
<if (input.mode === "Message")>
<div class="metadata">
<div class="options color-menu">
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
</if-permitted>
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
</if-permitted>
</div>
<chapter-meta weak kind="date" value=input.comment.createdDate />
<chapter-meta weak kind="author" value=input.comment.author />
</div>
<div class="content color-blockquote">
<table class="header">
<tbody>
<tr if(input.comment.subject != "")>
<th>Subject:</th>
<td>${input.comment.subject}</td>
</tr>
<tr>
<th>From:</th>
<td>
<div if(input.comment.character != null) class="tooltip" style="display: inline-block">
<span>${input.comment.characterName}</span>
<div class="tooltip-content color-highlight-dark">
<h1>${input.comment.character.name}</h1>
<h2>${input.comment.character.author}</h2>
<markdown source=input.comment.character.description />
</div>
</div>
<div else>
<span class="tooltip color-primary">
<span>${input.comment.characterName}</span>
<div class="tooltip-content color-highlight-dark color-primary">
<h1>(Unknown Character)</h1>
<h2>${input.comment.author}</h2>
</div>
</span>
</div>
</td>
</tr>
<tr if(input.comment.fictionalDate != null)>
<th>Date:</th>
<td>${moment(input.comment.fictionalDate).format("MMM D, YYYY")}</td>
</tr>
</tbody>
</table>
<markdown class="body" source=input.comment.source />
</div>
</if>
<if(input.mode === "Chat")>
<div class="content">
<div class="options color-menu">
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
</if-permitted>
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
</if-permitted>
</div>
<for (paragraph in state.paragraphs)>
<div class="message-text color-text">
<markdown class="body" source=paragraph>
<span class="decoration color-menu">[</span>
<date class="color-menu" value=input.comment.fictionalDate></date>
<span class="decoration color-menu">]&nbsp;</span>
<span class="decoration color-menu">&lt;</span>
<span if(input.comment.character != null) class="tooltip color-primary">
<span>${input.comment.characterName}</span>
<div class="tooltip-content color-highlight-dark color-primary">
<h1>${input.comment.character.name}</h1>
<h2>${input.comment.character.author}</h2>
<markdown source=input.comment.character.description />
</div>
</span>
<span class="tooltip color-primary" else>
<span>${input.comment.characterName}</span>
<div class="tooltip-content color-highlight-dark color-primary">
<h1>(Unknown Character)</h1>
<h2>${input.comment.author}</h2>
</div>
</span>
<span class="decoration color-menu">&gt;&nbsp;</span>
</markdown>
</div>
</for>
</div>
</if>
<if(input.mode === "Article")>
<div class="metadata">
<div class="options color-menu">
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
</if-permitted>
<if-permitted user=input.user author=input.comment.author permission="story.edit">
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
</if-permitted>
</div>
<chapter-meta kind="title" value=input.comment.subject />
<chapter-meta kind="name" value=input.comment.characterName character=(input.comment.character) ukAuthor=input.comment.author />
<chapter-meta kind="date" value=input.comment.fictionalDate />
<chapter-meta weak kind="date" value=input.comment.createdDate />
<chapter-meta weak kind="author" value=input.comment.author />
</div>
<div class="content color-text">
<markdown class="body" source=input.comment.source />
</div>
</if>
</div>
@@ -0,0 +1,113 @@
div.comment {
position: relative;
div.options {
width: 100%;
max-width: 95ch;
}
}
div.comment.mode-message {
margin-bottom: 1.5em;
font-size: 1em;
.metadata {
padding-bottom: 0.5em;
}
.content {
outline: 1px solid;
padding: 0.25em 1ch;
font-size: 1.25em;
table.header {
border-bottom: 0.5px solid;
margin: 0;
width: 100%;
tbody {
tr {
text-align: left;
th {
width: 12ch;
padding-right: 1ch;
}
td {
width: 100%;
}
}
}
}
}
div.body {
padding-top: 0.5em;
p:first-of-type {
margin-top: 0;
padding-top: 0;
}
}
}
div.comment.mode-chat {
div.options {
display: none;
bottom: -0.25em;
a {
background: black;
opacity: 0.666;
}
a:hover {
opacity: 1;
}
}
div.content {
line-height: 2em;
font-size: 1.125em;
div.tooltip-content {
text-indent: 0;
}
div.message-text {
vertical-align: top;
padding-left: 2ch;
text-indent: -2ch;
div.body {
p:first-of-type {
display: inline;
}
p:last-of-type {
margin-bottom: 0;
padding-bottom: 0;
}
p {
line-height: 1.5em;
}
}
}
}
}
div.comment.mode-article {
margin-bottom: 1em;
.content {
font-size: 1.25em;
}
}
div.comment.mode-chat:hover {
div.options {
display: block;
}
}
@@ -11,6 +11,7 @@ module.exports = class {
title: "",
source: "",
fictionalDate: "",
commentMode: "Disabled",
},
}
@@ -19,18 +20,18 @@ module.exports = class {
onInput(input) {
if (input.chapter && !this.first) {
let {fictionalDate, title, source} = input.chapter
let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
if (fictionalDate != null) {
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
}
this.state.values = {fictionalDate, title, source}
this.state.values = {fictionalDate, title, source, commentMode, commentsLocked}
}
}
change(key, ev) {
this.state.values[key] = ev.target.value
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
}
open() {
@@ -52,15 +53,19 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date!`
if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return
}
} else {
fictionalDate = null
}
const input = {storyId: this.input.storyId, title: values.title, source: values.source, fictionalDate}
const input = {storyId: this.input.storyId, title: values.title, source: values.source, fictionalDate, commentMode: values.commentMode}
chapterApi.addChapter(input).then(chapter => {
this.emit("add", chapter)
@@ -70,11 +75,12 @@ module.exports = class {
title: "",
source: "",
fictionalDate: "",
commentMode: "Disabled",
}
}).catch(errs => {
console.warn("Failed to edit:", errs)
this.state.error = "Failed to edit: " + errs[0].message
this.state.error = "Failed to edit: " + (errs[0]||errs||{}).message || errs
}).then(() => {
this.state.loading = false
})
@@ -12,5 +12,13 @@
<label>Content</label>
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
<label>Comments</label>
<select key="kind" class="big" placeholder="Kind" on-change("change", "commentMode") value=state.values.commentMode>
<option value="Disabled" selected=(state.values.commentMode === "Disabled")>Disabled – Hide all comments</option>
<option value="Article" selected=(state.values.commentMode === "Article")>Article – Looks like story chapters</option>
<option value="Chat" selected=(state.values.commentMode === "Chat")>Chat – One-line chat messages</option>
<option value="Message" selected=(state.values.commentMode === "Message")>Message – Extranet Mail</option>
</select>
<button disabled=state.loading on-click("save")>Save</button>
</modal>
@@ -0,0 +1,139 @@
const moment = require("moment")
const {commentApi} = require("../../../../../rpdata/api/Comment")
const {charactersApi} = require("../../../../../rpdata/api/Character")
module.exports = class {
onCreate(input) {
this.state = {
error: null,
loading: false,
values: {
subject: "",
source: "",
characterName: "",
characterId: "",
fictionalDate: "",
},
characters: [],
opened: false,
placeholder: "(Required)",
}
this.first = false
}
onMount() {
charactersApi.list({author: (this.input.user||{}).name}).then((characters) => {
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
})
const comments = this.input.chapter.comments || []
const lastComment = comments[comments.length - 1]
if (lastComment != null) {
let date = lastComment.fictionalDate || this.input.chapter.fictionalDate
if (date != null) {
this.change("fictionalDate", {target: {value: moment(date).format("MMM D, YYYY")}})
}
}
}
change(key, ev) {
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
if (key === "characterId") {
if (ev.target.value !== "") {
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
if (character != null) {
this.state.placeholder = character.name
} else {
this.state.placeholder = "(Required)"
}
} else {
this.state.placeholder = "(Required)"
}
}
}
open() {
if (!this.state.opened) {
charactersApi.list({author: (this.input.user||{}).name}).then((characters) => {
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
})
}
}
close() {
this.first = false
this.state.opened = false
this.emit("close")
}
save() {
const values = Object.assign({}, this.state.values)
if (values.source.length == 0) {
this.state.error = "The comment body is empty."
console.warn(values)
return
}
if (this.input.chapter.commentMode !== "Chat" && values.subject.trim().length == 0) {
this.state.error = "The subject is empty."
return
}
if (values.characterName.trim().length < 1) {
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
if (character == null) {
this.state.error = "You need to give a display name for an anonymous character."
return
}
values.characterName = character.name
}
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date!`
return
}
} else {
fictionalDate = null
}
const input = {
chapterId: this.input.chapter.id,
subject: values.subject,
source: values.source,
characterName: values.characterName,
characterId: values.characterId,
fictionalDate: fictionalDate,
}
if (input.characterId == "") {
input.characterId = null
}
commentApi.addComment(input).then(comment => {
this.emit("add", comment)
this.emit("close")
this.state.values = {
subject: "",
source: "",
characterName: "",
characterId: "",
fictionalDate: "",
}
this.state.opened = false
}).catch(errs => {
console.warn("Failed to add comemnt:", errs)
this.state.error = "Failed to add comemnt: " + (errs[0]||errs||{}).message || errs
}).then(() => {
this.state.loading = false
})
}
}
@@ -0,0 +1,27 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
<h1>Add Comment</h1>
<p key="error" class="color-error">${state.error}</p>
<if(input.chapter.commentMode !== "Chat")>
<label>Subject</label>
<input key="subject" autofocus placeholder="" class="big" on-change("change", "subject") value=state.values.subject />
</if>
<label>Character ${state.characterId}</label>
<select on-change("change", "characterId")>
<option value="" selected=state.values.characterId=="">Anonymous</option>
<option for(character in state.characters) value=character.id selected=(state.values.characterId==character.id)>${character.name}</option>
</select>
<label>Display Name</label>
<input key="characterName" autofocus placeholder=state.placeholder on-change("change", "characterName") value=state.values.characterName />
<label>IC Date</label>
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
<label>Content</label>
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
<button disabled=state.loading on-click("save")>Save</button>
</modal>
@@ -11,6 +11,8 @@ module.exports = class {
title: "",
source: "",
fictionalDate: "",
commentMode: "Article",
commentsLocked: false,
},
}
@@ -19,18 +21,20 @@ module.exports = class {
onInput(input) {
if (input.chapter && !this.first) {
let {fictionalDate, title, source} = input.chapter
let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
if (fictionalDate != null) {
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY")
} else {
fictionalDate = ""
}
this.state.values = {fictionalDate, title, source}
this.state.values = {fictionalDate, title, source, commentMode, commentsLocked}
}
}
change(key, ev) {
this.state.values[key] = ev.target.value
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
}
open() {
@@ -47,15 +51,19 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date`
if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return
}
} else {
fictionalDate = null
}
const input = {id: this.input.chapter.id, title: values.title, source: values.source}
const input = {id: this.input.chapter.id, title: values.title, source: values.source, commentsLocked: values.commentsLocked, commentMode: values.commentMode}
if (fictionalDate != null) {
input.fictionalDate = fictionalDate
@@ -12,5 +12,17 @@
<label>Content</label>
<textarea key="source" placeholder="Content" class="tall" on-change("change", "source") value=state.values.source />
<label>Comments</label>
<select key="kind" class="big" placeholder="Kind" on-change("change", "commentMode") value=state.values.commentMode>
<option value="Disabled" selected=(state.values.commentMode === "Disabled")>Disabled – Hide all comments</option>
<option value="Article" selected=(state.values.commentMode === "Article")>Article – Looks like story chapters</option>
<option value="Chat" selected=(state.values.commentMode === "Chat")>Chat – One-line chat messages</option>
<option value="Message" selected=(state.values.commentMode === "Message")>Message – Extranet Mail</option>
</select>
<toggle value=state.values.commentsLocked on="Locked" off="Unlocked"
onDesc="Logged-in users can post comments."
offDesc="Nobody can post comments to this chapter."
on-change("change", "commentsLocked") />
<button disabled=state.loading on-click("save")>Save</button>
</modal>
@@ -0,0 +1,131 @@
const moment = require("moment")
const {commentApi} = require("../../../../../rpdata/api/Comment")
const {charactersApi} = require("../../../../../rpdata/api/Character")
module.exports = class {
onCreate(input) {
this.state = {
error: null,
loading: false,
values: {
subject: "",
source: "",
characterName: "",
characterId: "",
fictionalDate: "",
},
characters: [],
opened: false,
placeholder: "(Required)",
}
}
onInput(input) {
if (input.comment) {
charactersApi.list({author: (input.user||{}).name}).then((characters) => {
this.state.characters = characters.sort((a,b) => a.name.localeCompare(b.name))
})
const comment = input.comment
this.state.values = {
subject: comment.subject,
characterId: comment.characterId,
characterName: comment.characterName,
fictionalDate: moment(comment.fictionalDate).format("MMM D, YYYY"),
source: comment.source,
}
}
}
change(key, ev) {
this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
if (key === "characterId") {
if (ev.target.value !== "") {
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
if (character != null) {
this.state.placeholder = character.name
} else {
this.state.placeholder = "(Required)"
}
} else {
this.state.placeholder = "(Required)"
}
}
}
close() {
this.emit("close")
}
save() {
const values = Object.assign({}, this.state.values)
let clearFictionalDate = false
if (values.source.length == 0) {
this.state.error = "The comment body is empty."
console.warn(values)
return
}
if (this.input.chapter.commentMode !== "Chat" && values.subject.trim().length == 0) {
this.state.error = "The subject is empty."
return
}
if (values.characterName.trim().length < 1) {
const character = this.state.characters.find(c => c.id === this.state.values.characterId)
if (character == null) {
this.state.error = "You need to give a display name for an anonymous character."
return
}
values.characterName = character.name
}
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date!`
return
}
} else {
fictionalDate = null
clearFictionalDate = true
}
const input = {
commentId: this.input.comment.id,
subject: values.subject,
source: values.source,
characterName: values.characterName,
characterId: values.characterId,
fictionalDate: fictionalDate,
clearFictionalDate: clearFictionalDate,
}
if (input.characterId == "") {
input.characterId = null
}
commentApi.editComment(input).then(comment => {
this.emit("edit", comment)
this.close()
this.state.values = {
subject: "",
source: "",
characterName: "",
characterId: "",
fictionalDate: "",
}
}).catch(errs => {
console.warn("Failed to add comemnt:", errs)
this.state.error = "Failed to add comemnt: " + (errs[0]||errs||{}).message || errs
}).then(() => {
this.state.loading = false
})
}
}
@@ -0,0 +1,27 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") >
<h1>Edit Comment</h1>
<p key="error" class="color-error">${state.error}</p>
<if(input.chapter.commentMode !== "Chat")>
<label>Subject</label>
<input key="subject" autofocus placeholder="" class="big" on-change("change", "subject") value=state.values.subject />
</if>
<label>Character ${state.characterId}</label>
<select on-change("change", "characterId")>
<option value="" selected=state.values.characterId=="">Anonymous</option>
<option for(character in state.characters) value=character.id selected=(state.values.characterId==character.id)>${character.name}</option>
</select>
<label>Display Name</label>
<input key="characterName" autofocus placeholder=state.placeholder on-change("change", "characterName") value=state.values.characterName />
<label>IC Date</label>
<input key="icdate" placeholder="(Optional)" on-change("change", "fictionalDate") value=state.values.fictionalDate />
<label>Content</label>
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source />
<button disabled=state.loading on-click("save")>Save</button>
</modal>
@@ -46,8 +46,12 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date`
if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return
}
} else {
@@ -36,13 +36,7 @@ module.exports = class {
return
}
for (const key in data) {
if (!data.hasOwnProperty(key)) {
continue
}
this.state.story.chapters[index][key] = data[key]
}
this.state.story.chapters[index] = Object.assign({}, this.state.story.chapters[index], data);
this.refreshStory()
}
@@ -8,18 +8,22 @@
<a on-click("open", "story.edit") class="color-menu">Edit</a>
</if-permitted>
<if-permitted user=input.user author=state.story.author permission="story.remove">
<a on-click("open", "story.remove") class="color-menu">Remove</a>
<a on-click("open", "story.remove") class="color-menu">Remove</a>
</if-permitted>
</if>
</div>
<story-tags tags=state.story.tags author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/>
<story-tags tags=state.story.tags open=state.story.open author=state.story.author user=(!state.removed ? input.user : null) on-select("open", "story.tags")/>
<annotation if(state.removed) level="error">
<p>
This story has been removed. Your browser has not refreshed the page yet,
so you can still read it (or back it up) before leaving the page.
</p>
</annotation>
<chapter for(chapter in state.story.chapters) key=chapter.id chapter=chapter user=(!state.removed ? input.user : null) on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) />
<chapter for(chapter in state.story.chapters) key=chapter.id
chapter=chapter
user=(!state.removed ? input.user : null)
hideTitle=(state.story.chapters[0] === chapter && chapter.title === state.story.name)
on-edit("updateChapter", chapter.id) on-remove("removeChapter", chapter.id) on-move("moveChapter", chapter) />
</div>
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") />
@@ -0,0 +1,36 @@
const {commentApi} = require("../../../../../rpdata/api/Comment")
module.exports = class {
onCreate(input) {
this.state = {
error: null,
loading: false,
values: {
title: "",
source: "",
fictionalDate: "",
},
}
}
change(key, ev) {
this.state.values[key] = ev.target.value
}
close() {
this.emit("close")
}
doIt() {
commentApi.removeComment({commentId: this.input.comment.id}).then(() => {
this.emit("remove")
this.emit("close")
}).catch(errs => {
console.warn("Failed to delete:", errs)
this.state.error = "Failed to delete: " + errs.message || errs[0].message
}).then(() => {
this.state.loading = false
})
}
}
@@ -0,0 +1,9 @@
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) notification closable on-close("close") >
<h1>Delete Comment</h1>
<p class="color-error">${state.error}</p>
<p class="color-danger">This is irreversible!</p>
<button disabled=state.loading on-click("doIt")>${Math.random() > 0.99 ? "Deleet" : "Delete"}</button>
</modal>
@@ -1,6 +1,14 @@
<div class="story-tags">
<a for(tag in input.tags) href=("/story/by-tag/" + tag.kind + "/" + encodeURIComponent(tag.name)) class=["tag", "color-tag-" + tag.kind.toLowerCase()]>${tag.name}</a>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
</if-permitted>
<if(input.open)>
<if-permitted user=input.user permission="member">
<a on-click("select", "edit") class="option color-menu">Manage Tags (Open)</a>
</if-permitted>
</if>
<else>
<if-permitted user=input.user author=input.author permission="story.edit">
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
</if-permitted>
</else>
</div>
@@ -42,8 +42,12 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) {
this.state.error = `Could not parse ${values.fictionalDate} as date`
if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return
}
} else {
+5
View File
@@ -3405,6 +3405,11 @@
"loose-envify": "^1.0.0"
}
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
"integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo="
},
"ipaddr.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+1
View File
@@ -26,6 +26,7 @@
"express-session": "^1.15.6",
"graphql-query-compress": "^1.1.0",
"http-proxy-middleware": "^0.19.1",
"ip": "^1.1.5",
"isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.3.0",
"lasso": "^3.2.6",
+1 -1
View File
@@ -7,7 +7,7 @@ const changesTemplate = require("../../marko/page/data/changes.marko")
router.get("/", async(req, res) => {
res.markoAsync(changesTemplate, {
changes: changesApi.list(),
changes: changesApi.list({limit: 2000}),
selected: {changes: true},
})
})
+8 -1
View File
@@ -8,19 +8,26 @@ const {channelApi} = require("../../rpdata/api/Channel")
const listTemplate = require("../../marko/page/logs/list.marko")
router.get("/", async(req, res) => {
const filter = {limit: 0}
const filter = {limit: 75}
if (req.query.characters) {
filter.characters = req.query.characters.split(",")
filter.limit = 0
}
if (req.query.channels) {
filter.channels = req.query.channels.split(",")
filter.limit = 0
}
if (req.query.events) {
filter.events = req.query.events.split(",")
filter.limit = 0
}
if (req.query.search) {
filter.search = req.query.search
filter.limit = 0
}
if (req.query.limit) {
filter.limit = parseInt(req.query.limit)
}
try {
+5
View File
@@ -68,6 +68,11 @@ class ChangesAPI {
time
nick
}
...on Comment {
chapterId
characterName
}
}
}
}
+51 -4
View File
@@ -1,4 +1,5 @@
const {query} = require("../client")
const {Comment} = require("./Comment")
class Chapter {
/**
@@ -8,9 +9,13 @@ class Chapter {
* @param {string} source
* @param {string | Date} createdDate
* @param {string | Date} fictionalDate
* @param {string | Date} editedDate
* @param {string | Date} editedDate
* @param {boolean} canComment
* @param {"Disabled"|"Article"|"Chat"|"Message"} commentMode
* @param {boolean} commentsLocked
* @param {any[]} comments
*/
constructor(id, title, author, source, createdDate, fictionalDate, editedDate) {
constructor(id, title, author, source, createdDate, fictionalDate, editedDate, canComment, commentMode, commentsLocked, comments) {
this.id = id
this.title = title
this.author = author
@@ -18,6 +23,10 @@ class Chapter {
this.createdDate = new Date(createdDate)
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
this.editedDate = new Date(editedDate)
this.canComment = canComment
this.commentMode = commentMode
this.commentsLocked = commentsLocked
this.comments = (comments||[]).map(c => Comment.fromData(c))
}
}
@@ -39,12 +48,31 @@ class ChapterAPI {
createdDate
fictionalDate
editedDate
canComment
commentMode
commentsLocked
comments {
id
subject
author
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
}
`, {input}, {permissions: ["member", "story.add"]}).then((data) => {
const ac = data.addChapter
return new Chapter(ac.id, ac.title, ac.author, ac.source, ac.createdDate, ac.fictionalDate, ac.editedDate)
return new Chapter(ac.id, ac.title, ac.author, ac.source, ac.createdDate, ac.fictionalDate, ac.editedDate, ac.canComment, ac.commentMode, ac.commentsLocked, ac.comments)
})
}
@@ -61,6 +89,25 @@ class ChapterAPI {
title
fictionalDate
source
canComment
commentMode
commentsLocked
comments {
id
subject
author
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
}
`, {input}, {permissions: ["member", "story.edit"]}).then(({editChapter}) => {
@@ -68,7 +115,7 @@ class ChapterAPI {
editChapter.fictionalDate = new Date(editChapter.fictionalDate)
}
return editChapter
return Object.assign({comments:[]}, editChapter)
})
}
+169
View File
@@ -0,0 +1,169 @@
const {query} = require("../client")
class Comment {
/**
* @param {string} id
* @param {string} subject
* @param {string} author
* @param {string} characterName
* @param {CommentCharacter} character
* @param {string | number | Date} fictionalDate
* @param {string | number | Date} createdDate
* @param {string | number | Date} editedDate
* @param {string} source
*/
constructor(id, subject, author, characterName, character, fictionalDate, createdDate, editedDate, source) {
this.id = id
this.subject = subject
this.author = author
this.characterName = characterName
this.character = character != null ? CommentCharacter.fromData(character) : null
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
this.createdDate = new Date(createdDate)
this.editedDate = new Date(editedDate)
this.source = source
}
static fromData(data) {
return new Comment(data.id, data.subject, data.author, data.characterName, data.character, data.fictionalDate, data.createdDate, data.editedDate, data.source)
}
}
class CommentCharacter {
/**
* @param {string} id
* @param {string} name
* @param {string} author
* @param {string} description
*/
constructor(id, name, author, description) {
this.id = id
this.name = name
this.author = author
this.description = description
}
static fromData(data) {
return new CommentCharacter(data.id, data.name, data.author, data.description)
}
}
class CommentAPI {
/**
* @param {string} id
* @returns {Promise<Comment>}
*/
find(id) {
return query(`
query FindComment($id: String!) {
comment(id: $id) {
id
subject
author
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
`, {id}).then(({comment}) => {
return new Comment(
comment.id, comment.subject, comment.author,
comment.characterName, comment.character,
comment.fictionalDate, comment.createdDate,
comment.editedDate, comment.source,
)
})
}
/**
* @param {any} input
* @returns {Promise<Comment>}
*/
addComment(input) {
return query(`
mutation AddComment($input: CommentAddInput!) {
addComment(input: $input) {
id
subject
author
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
`, {input}, {permissions:["member", "story.edit"]}).then(({addComment: comment}) => {
return new Comment(
comment.id, comment.subject, comment.author,
comment.characterName, comment.character,
comment.fictionalDate, comment.createdDate,
comment.editedDate, comment.source,
)
})
}
/**
* @param {any} input
* @returns {Promise<Comment>}
*/
editComment(input) {
return query(`
mutation EditComment($input: CommentEditInput!) {
editComment(input: $input) {
id
subject
author
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
`, {input}, {permissions:["member", "story.edit"]}).then(({editComment: comment}) => {
return new Comment(
comment.id, comment.subject, comment.author,
comment.characterName, comment.character,
comment.fictionalDate, comment.createdDate,
comment.editedDate, comment.source,
)
})
}
/**
* @param {{commentId:string}} input
* @returns {Promise<{commentId:string}>}
*/
removeComment(input) {
return query(`
mutation RemoveComment($input: CommentRemoveInput!) {
removeComment(input: $input) {
id
}
}
`, {input}, {permissions:["member", "story.edit"]})
}
}
module.exports = {Comment, CommentCharacter, commentApi: new CommentAPI}
+35 -16
View File
@@ -29,7 +29,7 @@ class Story {
this.updatedDate = new Date(updatedDate)
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
this.tags = tags.map(dt => new Tag(dt.kind, dt.name))
this.chapters = chapters != null ? chapters.map(c => new Chapter(c.id, c.title, c.author, c.source, c.createdDate, c.fictionalDate, c.editedDate)) : null
this.chapters = chapters != null ? chapters.map(c => new Chapter(c.id, c.title, c.author, c.source, c.createdDate, c.fictionalDate, c.editedDate, c.canComment, c.commentMode, c.commentsLocked, c.comments)) : null
}
}
@@ -91,32 +91,51 @@ const storyApi = {
*/
find(id) {
return query(`
query FindStory($id: String!) {
story(id: $id) {
id
query FindStory($id: String!) {
story(id: $id) {
id
name
author
category
tags {
kind
name
}
listed
open
createdDate
fictionalDate
updatedDate
chapters {
id
title
author
category
tags {
kind
name
}
listed
open
source
createdDate
fictionalDate
updatedDate
chapters {
editedDate
canComment
commentMode
commentsLocked
comments {
id
title
subject
author
source
createdDate
characterName
character {
id
name
author
description
}
fictionalDate
createdDate
editedDate
source
}
}
}
}
`, {id}).then(({story}) => {
return new Story(
story.id, story.name, story.author, story.category,
+35 -1
View File
@@ -9,6 +9,8 @@ const config = require("./config")
const express = require("express")
const proxy = require("express-http-proxy")
const proxy2 = require("http-proxy-middleware")
const ip = require("ip")
const dns = require("dns")
const lasso = require("lasso")
const lassoMiddleware = require("lasso/middleware")
const markoExpress = require("marko/express")
@@ -26,7 +28,39 @@ lasso.configure(require("./marko-config"))
// Set trusted proxy
if (config.trustedProxy != null) {
app.set('trust proxy', config.trustedProxy)
if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
app.set('trust proxy', config.trustedProxy)
console.log("Trusting proxy", config.trustedProxy);
} else {
let trusted = null
setInterval(() => {
dns.resolve4(config.trustedProxy, (err, res) => {
if (err != null) {
if (trusted === null) {
console.error("Failed to resolve trustedProxy hostname")
}
return
}
if (res.length === 0) {
if (trusted === null) {
console.error("No results for trustedProxy hostname")
}
return
}
if (trusted !== res[0]) {
console.log("Trusting proxy", res[0], `(${config.trustedProxy})`);
app.set('trust proxy', res[0])
trusted = res[0]
}
})
}, 1000)
}
}
// Apply middleware