Compare commits

..
41 Commits
Author SHA1 Message Date
gisle 99aec4d2cb Added log suggestions to the bottom of logs. 2019-08-02 17:34:12 +02:00
gisle 3f944ff029 Fixed logs page not remembering show all button.
the build failed
2019-08-02 11:14:30 +02:00
gisle 3cbe42e1ba Fixed ordinal checker crashing edit story form.
the build was successful
2019-08-02 11:10:25 +02:00
gisle 01b501f9e5 Fixed /logs/<lig-id> route not supporting dash in channel name.
the build was successful
2019-07-20 10:18:05 +02:00
gisle 9b7d182955 Removed limit argument in logs query if it's not needed.
the build was successful
2019-05-21 20:45:27 +02:00
gisle 7c55fc03aa Fixed locals middleware doing an auth check for assets, scripts and other static content.
the build was successful
2019-05-19 21:37:01 +02:00
gisle 6fca626ea2 logs-content: Added exception for annotations in multipart post detection.
the build was successful
2019-04-25 21:00:54 +02:00
gisle d92e6bfeb0 Merge branch 'master' of ssh+git://git.aiterp.net/rpdata/frontend
the build was successful
2019-04-25 20:54:10 +02:00
gisle 2d89096430 logs-content: Fixed post being added twice on add due to race condition between websocket and mutation return. 2019-04-25 20:53:59 +02:00
gisle 3988028256 story-content: Fixed comment fictional dates being affected by browser timezone.
the build was successful
2019-04-19 16:21:59 +02:00
gisle c51733ef3b logs-content: Fixed event link filtering by channel. 2019-04-19 16:17:05 +02:00
gisle f81f05d274 logs-content: Added character highlighter feature. 2019-04-19 16:15:07 +02:00
gisle 25d4aa73d1 logs-content: Replaced old post rendering with actual irc caret notation parsing. 2019-04-19 14:57:05 +02:00
gisle e1a5968341 package: npm audit fix 2019-04-18 12:14:09 +02:00
gisle 5c4446f161 Fixed story chapter comment permission check not allowing other users to comment.
the build was successful
2019-03-20 21:02:28 +01:00
gisle 3a057f6f24 Fixed story chapter fictionalDate field weirdness, especially when it's not provided.
the build was successful
2019-03-17 21:59:50 +01:00
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
gisle c28f17abdd data: Added op text for move-in, move-out and tag, added fallback to ID for untitled chapters.
the build was successful
2018-12-08 16:30:54 +01:00
61 changed files with 1726 additions and 168 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>
@@ -0,0 +1,23 @@
const {parse, BOLD} = require("irc-caret-notation")
module.exports = class {
onCreate() {
this.state = {
paragraphs: [],
prev: "",
}
}
onInput(input) {
if (this.state == null) {
return
}
if (input.source != this.state.prev) {
this.state.prev = input.source
this.state.paragraphs = input.source.split("\n").filter(l => l.length > 0).map(l => parse(l, {
"*": (!(input.source.startsWith("(")) ? BOLD : null),
}))
}
}
}
@@ -0,0 +1,5 @@
<div class=["irc-caret-notation", input.class]>
<p for(p in state.paragraphs)>
<span for(chunk in p) class=chunk.cssClasses("icn-")>${chunk.text}</span>
</p>
</div>
+1
View File
@@ -1,3 +1,4 @@
<div class=["markdown-content", input.class]> <div class=["markdown-content", input.class]>
<include(input.renderBody) />
<include(state.render) /> <include(state.render) />
</div> </div>
+2 -2
View File
@@ -1,6 +1,6 @@
<a on-click("onClick") href=input.href> <a on-click("onClick") href=input.href>
<div class=state.classes> <div class=[state.classes, input.weak ? "dark" : null]>
<div class="icon color-primary">${input.icon}</div> <div class=["icon", "color-primary"]>${input.icon}</div>
<div class=["text", input.selected ? "color-primary" : input.textClass]> <div class=["text", input.selected ? "color-primary" : input.textClass]>
<include(input.renderBody) /> <include(input.renderBody) />
</div> </div>
+3
View File
@@ -47,6 +47,9 @@ module.exports = class {
if (this.input.enabled) { if (this.input.enabled) {
window.removeEventListener("keydown", this.onPress) window.removeEventListener("keydown", this.onPress)
} }
this.mounted = false
this.opened = false
} }
/** /**
+68
View File
@@ -51,3 +51,71 @@ body {
background-color: #555; background-color: #555;
color: #000; 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); background-color: rgba(221, 170, 17, 0.125);
color: #DA1; color: #DA1;
@@ -100,7 +100,7 @@ div.markdown-content .orange, body.theme-logs div.markdown-content blockquote, b
color: #DA1; 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); background-color: rgba(34, 187, 255, 0.125);
color: #4BF; color: #4BF;
@@ -108,7 +108,7 @@ div.markdown-content .blue, body.theme-story div.markdown-content blockquote, bo
color: #4BF; // 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); background-color: rgba(17,255,136, 0.125);
color: #1F8; color: #1F8;
+20 -1
View File
@@ -65,7 +65,22 @@ module.exports = class {
this.state.operation += " chapter" this.state.operation += " chapter"
this.state.links = [{ this.state.links = [{
text: chapter.title, text: chapter.title || `Untitled chapter (${chapterKey.id})`,
href: `/story/${storyKey.id}#${chapterKey.id}`,
}]
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}`, href: `/story/${storyKey.id}#${chapterKey.id}`,
}] }]
@@ -112,4 +127,8 @@ const opText = {
move: "moved", move: "moved",
edit: "edited", edit: "edited",
import: "imported", import: "imported",
tag: "tagged",
untag: "untagged",
"move-in": "moved in",
"move-out": "moved out",
} }
@@ -1,7 +1,7 @@
import moment from "moment"; import moment from "moment";
<div class="change"> <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="author color-text">${input.data.author} </span>
<span class="text color-text">${state.operation} </span> <span class="text color-text">${state.operation} </span>
<span for(link in state.links | status-var=loop) key=link.href class="link"> <span for(link in state.links | status-var=loop) key=link.href class="link">
@@ -10,7 +10,7 @@
</div> </div>
<label>Current Nicks</label> <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> <div class="content color-text">${nick}</div>
<button disabled=state.loading on-click("removeNick", nick)>Remove</button> <button disabled=state.loading on-click("removeNick", nick)>Remove</button>
</div> </div>
@@ -0,0 +1,93 @@
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
module.exports = class {
onCreate(input) {
this.state = {
id: input.log.id,
channelName: input.log.channelName,
eventName: input.log.eventName,
nexts: [],
prevs: [],
loaded: false,
error: null,
}
}
onMount() {
this.timeout = setTimeout(() => {
logHeaderApi.list().then(logs => {
const index = logs.findIndex(l => l.id === this.state.id);
if (index === -1) {
return Promise.reject("This log not found in list.")
}
const log = logs[index]
const characterIds = log.characters.map(c => c.id);
const nexts = []
const prevs = []
const foundGroups = {}
let foundChannel = false
for (let i = index - 1; i >= 0; --i) {
const log2 = logs[i]
const hasEvent = (log.eventName ? log.eventName === log2.eventName : false)
const hasChannel = (log.channelName === log2.channelName)
const characters = log2.findCharacters(...characterIds)
const characterIdsKey = characters.map(c => c.id).sort().join(",")
const suggestion = {log: log2, characters, hasEvent, hasChannel}
if (hasChannel && !foundChannel) {
foundChannel = true
foundGroups[characterIdsKey] = true
nexts.push(suggestion)
} else if (hasEvent) {
foundGroups[characterIdsKey] = true
nexts.push(suggestion)
} else if (nexts.length < 8 && !hasEvent && characters.length > 1 && !foundGroups[characterIdsKey]) {
foundGroups[characterIdsKey] = true
nexts.push(suggestion)
}
}
for (let i = index + 1; i < logs.length; ++i) {
const log2 = logs[i]
const hasEvent = (log.eventName ? log.eventName === log2.eventName : false)
const hasChannel = (log.channelName === log2.channelName)
const characters = log2.findCharacters(...characterIds)
const characterIdsKey = characters.map(c => c.id).sort().join(",")
const suggestion = {log: log2, characters, hasEvent, hasChannel}
if (hasChannel && !foundChannel) {
foundChannel = true
foundGroups[characterIdsKey] = true
prevs.push(suggestion)
} else if (hasEvent) {
foundGroups[characterIdsKey] = true
prevs.push(suggestion)
} else if (prevs.length < 8 && !hasEvent && characters.length > 1 && !foundGroups[characterIdsKey]) {
foundGroups[characterIdsKey] = true
prevs.push(suggestion)
}
}
this.state.nexts = nexts;
this.state.prevs = prevs;
this.state.loaded = true;
}).catch(err => {
console.warn("Suggestions load error:", err)
this.suggestionsError = "Failed to load suggestions: " + err.toString()
})
}, 1000)
}
onUnmount() {
clearTimeout(this.timeout)
}
}
@@ -0,0 +1,41 @@
import moment from "moment"
<div class="log-suggestions">
<if (state.loaded && state.error == null)>
<if (state.nexts.length > 0 || state.prevs.length > 0)>
<if (state.nexts.length > 0)>
<h2 class="color-primary">Next Logs</h2>
<div for(s in state.nexts) class="suggestion color-primary">
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.name || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
<div class="suggestion-tags">
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${s.log.eventName}</div>
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
</div>
</div>
</if>
<if (state.prevs.length > 0)>
<h2 class="color-primary">Previous Logs</h2>
<div for(s in state.prevs) class="suggestion color-primary">
<div class="suggestion-header color-primary"><a href=`/logs/${s.log.id}`>${s.log.name || `${s.log.channelName} - ${moment(s.log.date).format("MMMM D, YYYY")}`}</a></div>
<div for(paragraph in (s.log.description||"").split("\n").filter(p => p)) class="suggestion-description color-text">${paragraph}</div>
<div class="suggestion-tags">
<div if(s.hasEvent) class="suggestion-tag color-tag-event">${s.log.eventName}</div>
<div if(s.hasChannel) class="suggestion-tag color-tag-location">${s.log.channelName}</div>
<div for(c in s.characters) class="suggestion-tag color-tag-character">${c.name}</div>
</div>
</div>
</if>
</if>
<else>
<div class="empty color-menu">(No suggestions)</div>
</else>
</if>
<else-if(state.error != null)>
<div class="error color-danger">{state.error}</div>
</else-if>
<else>
<div class="loading color-menu">Loading suggestions...</div>
</else>
</div>
@@ -0,0 +1,46 @@
div.log-suggestions {
padding: 0 0.5ch;
margin-top: 1em;
margin-bottom: 2em;
h2 {
text-align: center;
margin: 0;
margin-bottom: 0.25em;
}
div.empty, div.loading {
text-align: center;
}
div.suggestion {
margin-bottom: 0.5em;
padding: 0.5em;
div.suggestion-header {
margin: 0.125em 0.5ch;
a {
color: inherit;
}
a:hover {
color: white;
}
}
div.suggestion-description {
margin: 0.125em 0.5ch;
}
div.suggestion-tags {
margin-top: 0.25em;
div.suggestion-tag {
display: inline-block;
padding: 0 0.5ch;
margin: 0.125em 0.5ch;
border: 0.25px solid;
}
}
}
}
@@ -1,5 +1,21 @@
module.exports = class { module.exports = class {
onCreate() {
this.state = {
highlights: [],
}
}
select(value) { select(value) {
this.emit("select", value) this.emit("select", value)
} }
toggleHighlight(id) {
if (this.state.highlights.includes(id)) {
this.state.highlights = this.state.highlights.filter(id2 => id2 !== id)
} else {
this.state.highlights = this.state.highlights.concat(id)
}
this.emit("hightlights", this.state.highlights);
}
} }
@@ -4,10 +4,23 @@
<menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link> <menu-link dark key="_create" on-click("select", "post.add", null) icon="+">Add Post</menu-link>
<menu-gap /> <menu-gap />
</if-permitted> </if-permitted>
<menu-header>Links</menu-header> <menu-header>Channel</menu-header>
<menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link> <menu-link href=("/logs/?channels="+encodeURIComponent(input.log.channel.name)) icon="#">${input.log.channel.name}</menu-link>
<menu-link if(input.log.channel.eventName) href=("/logs/?channels="+input.log.channel.eventName) icon="E">${input.log.channel.eventName}</menu-link> <menu-gap />
<for(character in input.log.characters)> <if(input.log.eventName)>
<menu-link href=("/logs/?characters="+character.id) icon="C">${character.name} <span class="weak">(${character.author})</span></menu-link> <menu-header>Event</menu-header>
</for> <menu-link href=("/logs/?events="+input.log.eventName) icon="E">${input.log.eventName}</menu-link>
<menu-gap />
</if>
<if(input.log.characters.length > 0)>
<menu-header>Characters</menu-header>
<for(character in input.log.characters)>
<menu-link weak=(state.highlights.length > 0 && !state.highlights.includes(character.id))
icon="C"
on-click("toggleHighlight", character.id)>
${character.name} <span class="weak">(${character.author})</span>
</menu-link>
</for>
<menu-gap />
</if>
</menu> </menu>
@@ -9,6 +9,7 @@ module.exports = class {
log: input.log, log: input.log,
modal: null, modal: null,
removed: false, removed: false,
highlights: [],
} }
} }
@@ -111,6 +112,11 @@ module.exports = class {
} }
postAdded(post) { postAdded(post) {
// Stop duplicates in case of race condition.
if (this.state.log.posts.find(p => p.id === post.id)) {
return
}
this.state.log.posts = this.state.log.posts.concat([post]) this.state.log.posts = this.state.log.posts.concat([post])
this.state.log = Object.assign({}, this.state.log) this.state.log = Object.assign({}, this.state.log)
} }
@@ -161,6 +167,10 @@ module.exports = class {
this.state.removed = true this.state.removed = true
} }
highlightsChanged(highlights) {
this.state.highlights = highlights.slice();
}
get title() { get title() {
if (this.state.log.title) { if (this.state.log.title) {
return this.state.log.title return this.state.log.title
@@ -1,8 +1,9 @@
<logs-content-menu user=input.user log=state.log on-select("open") /> <logs-content-menu on-hightlights("highlightsChanged") user=input.user log=state.log on-select("open") />
<main> <main>
<div class="logs-content"> <div class="logs-content">
<div class="header"> <div class="header">
<h1 class="color-primary">${component.title}</h1> <h1 class="color-primary">${component.title}</h1>
<p for(paragraph in state.log.description.split("\n").filter(p => p)) class="color-text">${paragraph}</p>
<if-permitted user=input.user permission="log.edit"> <if-permitted user=input.user permission="log.edit">
<a on-click("open", "log.edit") class="color-menu">Edit</a> <a on-click("open", "log.edit") class="color-menu">Edit</a>
</if-permitted> </if-permitted>
@@ -25,8 +26,10 @@
prev=(state.log.posts[loop.getIndex() - 1] || {}) prev=(state.log.posts[loop.getIndex() - 1] || {})
post=post post=post
characters=state.log.characters characters=state.log.characters
highlights=state.highlights
last=(post.position === state.log.posts.length) last=(post.position === state.log.posts.length)
/> />
<log-suggestions log=state.log />
</div> </div>
<add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") /> <add-post-modal enabled=(state.modal === "post.add") logId=state.log.id on-close("close") on-added("postAdded") />
@@ -15,6 +15,10 @@ div.logs-content {
margin-bottom: 0; margin-bottom: 0;
} }
p {
margin: 0.25em 0 0 0;
}
a { a {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
@@ -13,6 +13,8 @@ module.exports = class {
removed: false, removed: false,
multipart: false, multipart: false,
anchored: false, anchored: false,
prevWasText: false,
dark: false,
} }
this.mounted = false; this.mounted = false;
@@ -47,7 +49,8 @@ module.exports = class {
} }
const timediff = Date.parse(input.post.time) - Date.parse(input.prev.time) 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.multipart = (!input.post.kind.startsWith("annotation.") && input.post.nick == input.prev.nick && input.prev.multipart !== false) && (Math.abs(timediff) < 60000);
this.state.prevWasText = (input.prev.kind === "text");
this.updatePost(input) this.updatePost(input)
} }
@@ -78,25 +81,13 @@ module.exports = class {
this.state.shortName = input.post.nick.split("_").shift() this.state.shortName = input.post.nick.split("_").shift()
this.state.name = input.post.nick this.state.name = input.post.nick
this.state.nameSuffix = "" this.state.nameSuffix = ""
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "_").trim();
if (this.state.text.startsWith("|")) { this.state.text = input.post.text
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.highlights.length > 0 && (input.post.kind === "action" || input.post.kind === "text")) {
const colonIndex = this.state.text.indexOf(": "); this.state.dark = true
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) { } else {
this.state.text = this.state.text.replace(": ", ": \"") + "\""; this.state.dark = false
} else {
this.state.text = '"' + this.state.text + '"'
}
}
if (this.state.text.charAt(0) == this.state.text.charAt(0).toUpperCase()) {
this.state.name = ""
} }
if (!input.post.nick.startsWith("=")) { if (!input.post.nick.startsWith("=")) {
@@ -104,13 +95,19 @@ module.exports = class {
for (const character of input.characters) { for (const character of input.characters) {
for (const nick of character.nicks) { for (const nick of character.nicks) {
if (nick === postNick) { if (nick === postNick) {
this.state.name = character.name this.state.name = character.name + character.id
this.state.shortName = character.shortName this.state.shortName = character.shortName
if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) { if (input.post.nick.endsWith("'s") || input.post.nick.endsWith("'")) {
this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s" this.state.nameSuffix = (character.shortName.endsWith("s") || character.shortName.endsWith("z")) ? "'" : "'s"
} }
if (input.highlights.length > 0) {
this.state.dark = !(input.highlights.includes(character.id))
} else {
this.state.dark = false
}
return return
} }
} }
@@ -1,4 +1,4 @@
<div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null]> <div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null, state.dark ? "dark" : null]>
<a class="anchor" id=input.post.id /> <a class="anchor" id=input.post.id />
<div class="post-meta-row"> <div class="post-meta-row">
<div class="options color-menu"> <div class="options color-menu">
@@ -19,18 +19,18 @@
</div> </div>
<div if(input.post.kind.startsWith("annotation.")) class="post-body" > <div if(input.post.kind.startsWith("annotation.")) class="post-body" >
<annotation level=(input.post.kind.substring(11))> <annotation level=(input.post.kind.substring(11))>
<markdown class="post-content" source=state.text /> <irc-caret-notation class="post-content" source=state.text />
</annotation> </annotation>
</div> </div>
<div if(input.post.kind === "scene") class="post-body color-text"> <div if(input.post.kind === "scene") class="post-body color-text">
<markdown class="post-content post-scene" source=state.text /> <irc-caret-notation class="post-content post-scene" source=state.text />
</div> </div>
<div if(input.post.kind === "action") class="post-body color-text"> <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) /> <irc-caret-notation class="post-content post-action" source=(prefix + state.text) />
</div> </div>
<div if(input.post.kind === "text") class="post-body color-text"> <div if(input.post.kind === "text") class="post-body color-text">
<markdown class="post-content post-text" source=state.text /> <irc-caret-notation class="post-content post-text" source=state.text />
</div> </div>
<remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") /> <remove-post-modal enabled=(state.modal === "remove") post=input.post on-close("close") on-remove("remove") />
<edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") /> <edit-post-modal enabled=(state.modal === "edit") post=input.post on-close("close") on-edited("edited") />
@@ -1,6 +1,6 @@
div.post { div.post {
margin-top: 1em; margin-top: 1em;
margin-bottom: 1ch; margin-bottom: 1.25em;
margin-left: -12px; margin-left: -12px;
padding-left: 12px; padding-left: 12px;
padding-right: 0.5ch; padding-right: 0.5ch;
@@ -65,12 +65,54 @@ div.post {
margin-top: 0.33em; margin-top: 0.33em;
} }
p {
line-height: 1.33em;
margin-bottom: -0.25em;
span.icn-bold { font-weight: 800; }
span.icn-underline { text-decoration: underline; }
span.icn-italic { font-style: italic; }
span.icn-fg-0 { color: #fff; }
span.icn-fg-1 { color: #111; }
span.icn-fg-2 { color: #13d; }
span.icn-fg-3 { color: #173; }
span.icn-fg-4 { color: #d21; }
span.icn-fg-5 { color: #721; }
span.icn-fg-6 { color: #727; }
span.icn-fg-7 { color: #d71; }
span.icn-fg-8 { color: #aa1; }
span.icn-fg-9 { color: #1d3; }
span.icn-fg-10 { color: #177; }
span.icn-fg-11 { color: #1DD; }
span.icn-fg-12 { color: #127; }
span.icn-fg-13 { color: #c1a; }
span.icn-fg-14 { color: #444; }
span.icn-fg-15 { color: #aaa; }
span.icn-bg-0 { background-color: #fff; }
span.icn-bg-1 { background-color: #111; }
span.icn-bg-2 { background-color: #13d; }
span.icn-bg-3 { background-color: #173; }
span.icn-bg-4 { background-color: #d21; }
span.icn-bg-5 { background-color: #721; }
span.icn-bg-6 { background-color: #727; }
span.icn-bg-7 { background-color: #d71; }
span.icn-bg-8 { background-color: #aa1; }
span.icn-bg-9 { background-color: #1d3; }
span.icn-bg-10 { background-color: #177; }
span.icn-bg-11 { background-color: #1DD; }
span.icn-bg-12 { background-color: #127; }
span.icn-bg-13 { background-color: #c1a; }
span.icn-bg-14 { background-color: #444; }
span.icn-bg-15 { background-color: #aaa; }
}
p:first-of-type { p:first-of-type {
margin-top: 0; margin-top: 0;
padding-top: 0; padding-top: 0;
} }
p:last-of-type { p:last-of-type {
padding-bottom: 0.1em; padding-bottom: 0.1em;
margin-bottom: 0em;
} }
} }
} }
@@ -91,11 +133,15 @@ div.post.pk-text.anchored {
} }
div.post.multipart { div.post.multipart {
margin-top: -0.9em; margin-top: -1.5em;
div.post-meta { div.post-meta {
span { span {
display: none; display: none;
} }
} }
}
div.post.dark {
opacity: 0.25;
} }
@@ -49,9 +49,9 @@ module.exports = class {
const search = this.state.search.toLocaleLowerCase() 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.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) => 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) => 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) => a.localeCompare(b)) 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}) { updateActive({filter, characters, channels, eventNames}) {
@@ -126,4 +126,29 @@ module.exports = class {
return str.toLowerCase().indexOf(search) !== -1 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 { } else {
this.state.filter = Object.assign({}, this.state.filter, {search: null}) this.state.filter = Object.assign({}, this.state.filter, {search: null})
} }
} else { } 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, {[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.updateQuery(this.state.filter)
this.refresh() this.refresh()
@@ -67,6 +68,9 @@ module.exports = class {
if (filter.search) { if (filter.search) {
queries.push("search=" + encodeURIComponent(filter.search)) queries.push("search=" + encodeURIComponent(filter.search))
} }
if (filter.limit != null) {
queries.push("limit=" + encodeURIComponent(filter.limit.toString()))
}
if (queries.length > 0) { if (queries.length > 0) {
history.replaceState("", "", `/logs/?${queries.join("&")}`) 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() { refresh() {
logHeaderApi.list(this.state.filter).then(logs => { logHeaderApi.list(this.state.filter).then(logs => {
this.state.logs = logs this.state.logs = logs
+6 -1
View File
@@ -1,8 +1,13 @@
<background src="/assets/images/bg.png" opacity=0.25 /> <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 /> <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 /> <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") /> <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> </main>
<add-log-modal enabled=(state.modal === "log.add") on-close("close") /> <add-log-modal enabled=(state.modal === "log.add") on-close("close") />
<import-log-modal enabled=(state.modal === "log.import") 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 break
} }
case "title": {
this.state.color = "color-primary"
this.state.text = value
break
}
default: { default: {
this.state.color = "color-menu" this.state.color = "color-menu"
@@ -1,6 +1,18 @@
<if(state.text != null && state.text != "")> <if(state.text != null && state.text != "")>
<div class=["chapter-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip> <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> <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> </div>
</if> </if>
@@ -13,6 +13,10 @@ div.chapter-meta {
a:hover { a:hover {
border-bottom: 0.5px solid; border-bottom: 0.5px solid;
} }
.tooltip {
border-bottom: 0.25px dotted;
}
} }
div.chapter-meta.weak { div.chapter-meta.weak {
@@ -3,9 +3,42 @@ module.exports = class {
this.state = { this.state = {
modal: null, modal: null,
removed: false, 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) { updateChapter(data) {
this.emit("edit", data) this.emit("edit", data)
} }
@@ -1,6 +1,6 @@
<div class="story-chapter"> <div class="story-chapter">
<a class="anchor" id=input.chapter.id /> <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="metadata">
<div class="options color-menu"> <div class="options color-menu">
<if-permitted user=input.user author=input.chapter.author permission="chapter.edit"> <if-permitted user=input.user author=input.chapter.author permission="chapter.edit">
@@ -16,10 +16,26 @@
<chapter-meta kind="date" utc=true value=input.chapter.fictionalDate /> <chapter-meta kind="date" utc=true value=input.chapter.fictionalDate />
<chapter-meta weak kind="date" value=input.chapter.createdDate /> <chapter-meta weak kind="date" value=input.chapter.createdDate />
<chapter-meta weak kind="author" value=input.chapter.author /> <chapter-meta weak kind="author" value=input.chapter.author />
<chapter-meta weak kind="message" value=input.chapter.commentMode />
</div> </div>
<markdown class="chapter-content color-text" source=input.chapter.source /> <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 permission=["member", "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") /> <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") /> <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") /> <remove-chapter-modal enabled=(state.modal === "remove") chapter=input.chapter on-close("close") on-remove("removeChapter") />
</div> </div>
@@ -49,4 +49,30 @@
div.chapter-content { div.chapter-content {
font-size: 1.25em; 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,121 @@
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>
<date utc value=input.comment.fictionalDate></date>
</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>
<if (input.comment.fictionalDate)>
<span class="decoration color-menu">[</span>
<date utc class="color-menu" value=input.comment.fictionalDate></date>
<span class="decoration color-menu">]&nbsp;</span>
</if>
<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 utc 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: "", title: "",
source: "", source: "",
fictionalDate: "", fictionalDate: "",
commentMode: "Disabled",
}, },
} }
@@ -19,18 +20,18 @@ module.exports = class {
onInput(input) { onInput(input) {
if (input.chapter && !this.first) { if (input.chapter && !this.first) {
let {fictionalDate, title, source} = input.chapter let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
if (fictionalDate != null) { if (fictionalDate != null) {
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY") 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) { change(key, ev) {
this.state.values[key] = ev.target.value this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
} }
open() { open() {
@@ -52,15 +53,19 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC") let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") { if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) { if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date!` this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return return
} }
} else { } else {
fictionalDate = null 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 => { chapterApi.addChapter(input).then(chapter => {
this.emit("add", chapter) this.emit("add", chapter)
@@ -70,11 +75,12 @@ module.exports = class {
title: "", title: "",
source: "", source: "",
fictionalDate: "", fictionalDate: "",
commentMode: "Disabled",
} }
}).catch(errs => { }).catch(errs => {
console.warn("Failed to edit:", 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(() => { }).then(() => {
this.state.loading = false this.state.loading = false
}) })
@@ -12,5 +12,13 @@
<label>Content</label> <label>Content</label>
<textarea key="source" placeholder="" class="tall" on-change("change", "source") value=state.values.source /> <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> <button disabled=state.loading on-click("save")>Save</button>
</modal> </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: "", title: "",
source: "", source: "",
fictionalDate: "", fictionalDate: "",
commentMode: "Article",
commentsLocked: false,
}, },
} }
@@ -19,18 +21,20 @@ module.exports = class {
onInput(input) { onInput(input) {
if (input.chapter && !this.first) { if (input.chapter && !this.first) {
let {fictionalDate, title, source} = input.chapter let {fictionalDate, title, source, commentMode, commentsLocked} = input.chapter
if (fictionalDate != null) { if (fictionalDate != null) {
fictionalDate = moment.utc(fictionalDate).format("MMM D, YYYY") 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) { change(key, ev) {
this.state.values[key] = ev.target.value this.state.values = Object.assign({}, this.state.values, {[key]: ev.target.value})
} }
open() { open() {
@@ -47,15 +51,19 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC") let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") { if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) { if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date` this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return return
} }
} else { } else {
fictionalDate = null 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) { if (fictionalDate != null) {
input.fictionalDate = fictionalDate input.fictionalDate = fictionalDate
@@ -12,5 +12,17 @@
<label>Content</label> <label>Content</label>
<textarea key="source" placeholder="Content" class="tall" on-change("change", "source") value=state.values.source /> <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> <button disabled=state.loading on-click("save")>Save</button>
</modal> </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)",
}
}
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
let fictionalDate = new Date(comment.fictionalDate)
if (comment.fictionalDate && !Number.isNaN(fictionalDate.getTime()) && fictionalDate.getTime() !== 0) {
fictionalDate = moment(fictionalDate).format("MMM D, YYYY")
} else {
fictionalDate = ""
}
this.state.values = {
subject: comment.subject,
characterId: comment.characterId,
characterName: comment.characterName,
fictionalDate: fictionalDate,
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>
@@ -2,6 +2,8 @@ const moment = require("moment")
const {storyApi} = require("../../../../../rpdata/api/Story") const {storyApi} = require("../../../../../rpdata/api/Story")
const ORDINAL_REGEX = /[0-9]{1,2}(st|nd|rd|th)/g
module.exports = class { module.exports = class {
onCreate() { onCreate() {
this.state = { this.state = {
@@ -45,9 +47,15 @@ module.exports = class {
const values = this.state.values const values = this.state.values
let fictionalDate = new Date(values.fictionalDate + " UTC") let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") { if (values.fictionalDate) {
if (Number.isNaN(fictionalDate)) { if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date` this.state.error = `Could not parse ${values.fictionalDate} as date.`
const ordinal = values.fictionalDate.match(ORDINAL_REGEX)[0]
if (ordinal != null) {
this.state.error += " Try to remove the '"+ordinal.slice(-2)+"'."
}
return return
} }
} else { } else {
@@ -36,13 +36,7 @@ module.exports = class {
return return
} }
for (const key in data) { this.state.story.chapters[index] = Object.assign({}, this.state.story.chapters[index], data);
if (!data.hasOwnProperty(key)) {
continue
}
this.state.story.chapters[index][key] = data[key]
}
this.refreshStory() this.refreshStory()
} }
@@ -8,18 +8,22 @@
<a on-click("open", "story.edit") class="color-menu">Edit</a> <a on-click("open", "story.edit") class="color-menu">Edit</a>
</if-permitted> </if-permitted>
<if-permitted user=input.user author=state.story.author permission="story.remove"> <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-permitted>
</if> </if>
</div> </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"> <annotation if(state.removed) level="error">
<p> <p>
This story has been removed. Your browser has not refreshed the page yet, 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. so you can still read it (or back it up) before leaving the page.
</p> </p>
</annotation> </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> </div>
<create-chapter-modal storyId=state.story.id enabled=(state.modal === "chapter.add") chapter=input.chapter on-close("close") on-add("addChapter") /> <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"> <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> <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(input.open)>
</if-permitted> <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> </div>
@@ -42,8 +42,12 @@ module.exports = class {
let fictionalDate = new Date(values.fictionalDate + " UTC") let fictionalDate = new Date(values.fictionalDate + " UTC")
if (values.fictionalDate != "") { if (values.fictionalDate != "") {
if (Number.isNaN(fictionalDate)) { if (Number.isNaN(fictionalDate.getTime())) {
this.state.error = `Could not parse ${values.fictionalDate} as date` this.state.error = `Could not parse ${values.fictionalDate} as date.`
if (values.fictionalDate.includes("th")) {
this.state.error += " Try to remove the 'th'."
}
return return
} }
} else { } else {
+11 -9
View File
@@ -28,15 +28,17 @@ module.exports = (req, res, next) => {
} }
} }
if (req.user) { if (["/static/", "/hax/", "/assets/"].find(p => req.path.startsWith(p)) == null) {
res.locals.user = { if (req.user) {
loggedIn: true, res.locals.user = {
name: req.user._json.name, loggedIn: true,
permissions: getPermissions(req.user._json.name), name: req.user._json.name,
} permissions: getPermissions(req.user._json.name),
} else { }
res.locals.user = { } else {
loggedIn: false, res.locals.user = {
loggedIn: false,
}
} }
} }
+84 -55
View File
@@ -2308,13 +2308,13 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
}, },
"fsevents": { "fsevents": {
"version": "1.2.4", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.8.tgz",
"integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", "integrity": "sha512-tPvHgPGB7m40CZ68xqFGkKuzN+RnpGmSV+hgeKxhRpbxdqKXUFJGC3yonBOLzQBcJyGpdZFDfCsdOC2KFsXzeA==",
"optional": true, "optional": true,
"requires": { "requires": {
"nan": "^2.9.2", "nan": "^2.12.1",
"node-pre-gyp": "^0.10.0" "node-pre-gyp": "^0.12.0"
}, },
"dependencies": { "dependencies": {
"abbrev": { "abbrev": {
@@ -2324,7 +2324,8 @@
}, },
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"bundled": true "bundled": true,
"optional": true
}, },
"aproba": { "aproba": {
"version": "1.2.0", "version": "1.2.0",
@@ -2332,7 +2333,7 @@
"optional": true "optional": true
}, },
"are-we-there-yet": { "are-we-there-yet": {
"version": "1.1.4", "version": "1.1.5",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2342,32 +2343,37 @@
}, },
"balanced-match": { "balanced-match": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true "bundled": true,
"optional": true
}, },
"brace-expansion": { "brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"chownr": { "chownr": {
"version": "1.0.1", "version": "1.1.1",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"code-point-at": { "code-point-at": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"console-control-strings": { "console-control-strings": {
"version": "1.1.0", "version": "1.1.0",
"bundled": true "bundled": true,
"optional": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@@ -2375,15 +2381,15 @@
"optional": true "optional": true
}, },
"debug": { "debug": {
"version": "2.6.9", "version": "4.1.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"ms": "2.0.0" "ms": "^2.1.1"
} }
}, },
"deep-extend": { "deep-extend": {
"version": "0.5.1", "version": "0.6.0",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
@@ -2426,7 +2432,7 @@
} }
}, },
"glob": { "glob": {
"version": "7.1.2", "version": "7.1.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2444,11 +2450,11 @@
"optional": true "optional": true
}, },
"iconv-lite": { "iconv-lite": {
"version": "0.4.21", "version": "0.4.24",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"safer-buffer": "^2.1.0" "safer-buffer": ">= 2.1.2 < 3"
} }
}, },
"ignore-walk": { "ignore-walk": {
@@ -2470,7 +2476,8 @@
}, },
"inherits": { "inherits": {
"version": "2.0.3", "version": "2.0.3",
"bundled": true "bundled": true,
"optional": true
}, },
"ini": { "ini": {
"version": "1.3.5", "version": "1.3.5",
@@ -2480,6 +2487,7 @@
"is-fullwidth-code-point": { "is-fullwidth-code-point": {
"version": "1.0.0", "version": "1.0.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"number-is-nan": "^1.0.0" "number-is-nan": "^1.0.0"
} }
@@ -2492,24 +2500,27 @@
"minimatch": { "minimatch": {
"version": "3.0.4", "version": "3.0.4",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
} }
}, },
"minimist": { "minimist": {
"version": "0.0.8", "version": "0.0.8",
"bundled": true "bundled": true,
"optional": true
}, },
"minipass": { "minipass": {
"version": "2.2.4", "version": "2.3.5",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.2",
"yallist": "^3.0.0" "yallist": "^3.0.0"
} }
}, },
"minizlib": { "minizlib": {
"version": "1.1.0", "version": "1.2.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2519,37 +2530,38 @@
"mkdirp": { "mkdirp": {
"version": "0.5.1", "version": "0.5.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"minimist": "0.0.8" "minimist": "0.0.8"
} }
}, },
"ms": { "ms": {
"version": "2.0.0", "version": "2.1.1",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"needle": { "needle": {
"version": "2.2.0", "version": "2.3.0",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"debug": "^2.1.2", "debug": "^4.1.0",
"iconv-lite": "^0.4.4", "iconv-lite": "^0.4.4",
"sax": "^1.2.4" "sax": "^1.2.4"
} }
}, },
"node-pre-gyp": { "node-pre-gyp": {
"version": "0.10.0", "version": "0.12.0",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"detect-libc": "^1.0.2", "detect-libc": "^1.0.2",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"needle": "^2.2.0", "needle": "^2.2.1",
"nopt": "^4.0.1", "nopt": "^4.0.1",
"npm-packlist": "^1.1.6", "npm-packlist": "^1.1.6",
"npmlog": "^4.0.2", "npmlog": "^4.0.2",
"rc": "^1.1.7", "rc": "^1.2.7",
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"semver": "^5.3.0", "semver": "^5.3.0",
"tar": "^4" "tar": "^4"
@@ -2565,12 +2577,12 @@
} }
}, },
"npm-bundled": { "npm-bundled": {
"version": "1.0.3", "version": "1.0.6",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
"npm-packlist": { "npm-packlist": {
"version": "1.1.10", "version": "1.4.1",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
@@ -2591,7 +2603,8 @@
}, },
"number-is-nan": { "number-is-nan": {
"version": "1.0.1", "version": "1.0.1",
"bundled": true "bundled": true,
"optional": true
}, },
"object-assign": { "object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -2601,6 +2614,7 @@
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"wrappy": "1" "wrappy": "1"
} }
@@ -2635,11 +2649,11 @@
"optional": true "optional": true
}, },
"rc": { "rc": {
"version": "1.2.7", "version": "1.2.8",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"deep-extend": "^0.5.1", "deep-extend": "^0.6.0",
"ini": "~1.3.0", "ini": "~1.3.0",
"minimist": "^1.2.0", "minimist": "^1.2.0",
"strip-json-comments": "~2.0.1" "strip-json-comments": "~2.0.1"
@@ -2667,16 +2681,17 @@
} }
}, },
"rimraf": { "rimraf": {
"version": "2.6.2", "version": "2.6.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"glob": "^7.0.5" "glob": "^7.1.3"
} }
}, },
"safe-buffer": { "safe-buffer": {
"version": "5.1.1", "version": "5.1.2",
"bundled": true "bundled": true,
"optional": true
}, },
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
@@ -2689,7 +2704,7 @@
"optional": true "optional": true
}, },
"semver": { "semver": {
"version": "5.5.0", "version": "5.7.0",
"bundled": true, "bundled": true,
"optional": true "optional": true
}, },
@@ -2706,6 +2721,7 @@
"string-width": { "string-width": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"code-point-at": "^1.0.0", "code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0", "is-fullwidth-code-point": "^1.0.0",
@@ -2723,6 +2739,7 @@
"strip-ansi": { "strip-ansi": {
"version": "3.0.1", "version": "3.0.1",
"bundled": true, "bundled": true,
"optional": true,
"requires": { "requires": {
"ansi-regex": "^2.0.0" "ansi-regex": "^2.0.0"
} }
@@ -2733,16 +2750,16 @@
"optional": true "optional": true
}, },
"tar": { "tar": {
"version": "4.4.1", "version": "4.4.8",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"chownr": "^1.0.1", "chownr": "^1.1.1",
"fs-minipass": "^1.2.5", "fs-minipass": "^1.2.5",
"minipass": "^2.2.4", "minipass": "^2.3.4",
"minizlib": "^1.1.0", "minizlib": "^1.1.1",
"mkdirp": "^0.5.0", "mkdirp": "^0.5.0",
"safe-buffer": "^5.1.1", "safe-buffer": "^5.1.2",
"yallist": "^3.0.2" "yallist": "^3.0.2"
} }
}, },
@@ -2752,20 +2769,22 @@
"optional": true "optional": true
}, },
"wide-align": { "wide-align": {
"version": "1.1.2", "version": "1.1.3",
"bundled": true, "bundled": true,
"optional": true, "optional": true,
"requires": { "requires": {
"string-width": "^1.0.2" "string-width": "^1.0.2 || 2"
} }
}, },
"wrappy": { "wrappy": {
"version": "1.0.2", "version": "1.0.2",
"bundled": true "bundled": true,
"optional": true
}, },
"yallist": { "yallist": {
"version": "3.0.2", "version": "3.0.3",
"bundled": true "bundled": true,
"optional": true
} }
} }
}, },
@@ -3405,11 +3424,21 @@
"loose-envify": "^1.0.0" "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": { "ipaddr.js": {
"version": "1.8.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
"integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4=" "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
}, },
"irc-caret-notation": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/irc-caret-notation/-/irc-caret-notation-1.0.0.tgz",
"integrity": "sha512-V/Dhqb/wentrfxH5U2pUTK1ye53mIEmpWCpT81Qa+/YfEDY7XuFjpCD/J0WRzjDqaecwn4mj0Mz+6T6bAz/BSA=="
},
"is-absolute": { "is-absolute": {
"version": "0.2.6", "version": "0.2.6",
"resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz",
@@ -3972,9 +4001,9 @@
"integrity": "sha1-OWCLQ1wJAfpVECF8FFJyjWvBm18=" "integrity": "sha1-OWCLQ1wJAfpVECF8FFJyjWvBm18="
}, },
"lodash": { "lodash": {
"version": "4.17.10", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
"integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
}, },
"lodash.includes": { "lodash.includes": {
"version": "4.3.0", "version": "4.3.0",
@@ -4284,9 +4313,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}, },
"nan": { "nan": {
"version": "2.10.0", "version": "2.13.2",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
"integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
"optional": true "optional": true
}, },
"nanomatch": { "nanomatch": {
+2
View File
@@ -26,6 +26,8 @@
"express-session": "^1.15.6", "express-session": "^1.15.6",
"graphql-query-compress": "^1.1.0", "graphql-query-compress": "^1.1.0",
"http-proxy-middleware": "^0.19.1", "http-proxy-middleware": "^0.19.1",
"ip": "^1.1.5",
"irc-caret-notation": "^1.0.0",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"jsonwebtoken": "^8.3.0", "jsonwebtoken": "^8.3.0",
"lasso": "^3.2.6", "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) => { router.get("/", async(req, res) => {
res.markoAsync(changesTemplate, { res.markoAsync(changesTemplate, {
changes: changesApi.list(), changes: changesApi.list({limit: 2000}),
selected: {changes: true}, 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") const listTemplate = require("../../marko/page/logs/list.marko")
router.get("/", async(req, res) => { router.get("/", async(req, res) => {
const filter = {limit: 0} const filter = {limit: 75}
if (req.query.characters) { if (req.query.characters) {
filter.characters = req.query.characters.split(",") filter.characters = req.query.characters.split(",")
filter.limit = 0
} }
if (req.query.channels) { if (req.query.channels) {
filter.channels = req.query.channels.split(",") filter.channels = req.query.channels.split(",")
filter.limit = 0
} }
if (req.query.events) { if (req.query.events) {
filter.events = req.query.events.split(",") filter.events = req.query.events.split(",")
filter.limit = 0
} }
if (req.query.search) { if (req.query.search) {
filter.search = req.query.search filter.search = req.query.search
filter.limit = 0
}
if (req.query.limit) {
filter.limit = parseInt(req.query.limit)
} }
try { try {
+5
View File
@@ -68,6 +68,11 @@ class ChangesAPI {
time time
nick nick
} }
...on Comment {
chapterId
characterName
}
} }
} }
} }
+51 -4
View File
@@ -1,4 +1,5 @@
const {query} = require("../client") const {query} = require("../client")
const {Comment} = require("./Comment")
class Chapter { class Chapter {
/** /**
@@ -8,9 +9,13 @@ class Chapter {
* @param {string} source * @param {string} source
* @param {string | Date} createdDate * @param {string | Date} createdDate
* @param {string | Date} fictionalDate * @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.id = id
this.title = title this.title = title
this.author = author this.author = author
@@ -18,6 +23,10 @@ class Chapter {
this.createdDate = new Date(createdDate) this.createdDate = new Date(createdDate)
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
this.editedDate = new Date(editedDate) 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 createdDate
fictionalDate fictionalDate
editedDate 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) => { `, {input}, {permissions: ["member", "story.add"]}).then((data) => {
const ac = data.addChapter 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 title
fictionalDate fictionalDate
source 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}) => { `, {input}, {permissions: ["member", "story.edit"]}).then(({editChapter}) => {
@@ -68,7 +115,7 @@ class ChapterAPI {
editChapter.fictionalDate = new Date(editChapter.fictionalDate) 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}
+14
View File
@@ -27,6 +27,15 @@ class LogHeader {
this.open = open this.open = open
this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author)) this.characters = characters.map(ch => new LogHeaderCharacter(ch.id, ch.name, ch.shortName, ch.author))
} }
/**
* Return all characters in that log.
*
* @param {...string} ids Character IDs
*/
findCharacters(...ids) {
return this.characters.filter(c => ids.includes(c.id))
}
} }
class LogHeaderCharacter { class LogHeaderCharacter {
@@ -89,6 +98,11 @@ const logHeaderApi = {
* @returns {Promise<LogHeader[]>} * @returns {Promise<LogHeader[]>}
*/ */
list(filter = {}) { list(filter = {}) {
filter = Object.assign({}, filter)
if (filter.limit != null && filter.limit <= 0) {
delete filter.limit
}
return query(` return query(`
query LogHeaders($filter: LogsFilter) { query LogHeaders($filter: LogsFilter) {
headers: logs(filter:$filter) { headers: logs(filter:$filter) {
+35 -16
View File
@@ -29,7 +29,7 @@ class Story {
this.updatedDate = new Date(updatedDate) this.updatedDate = new Date(updatedDate)
this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null this.fictionalDate = fictionalDate != null ? new Date(fictionalDate) : null
this.tags = tags.map(dt => new Tag(dt.kind, dt.name)) 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) { find(id) {
return query(` return query(`
query FindStory($id: String!) { query FindStory($id: String!) {
story(id: $id) { story(id: $id) {
id id
name
author
category
tags {
kind
name name
}
listed
open
createdDate
fictionalDate
updatedDate
chapters {
id
title
author author
category source
tags {
kind
name
}
listed
open
createdDate createdDate
fictionalDate fictionalDate
updatedDate editedDate
chapters { canComment
commentMode
commentsLocked
comments {
id id
title subject
author author
source characterName
createdDate character {
id
name
author
description
}
fictionalDate fictionalDate
createdDate
editedDate editedDate
source
} }
} }
} }
}
`, {id}).then(({story}) => { `, {id}).then(({story}) => {
return new Story( return new Story(
story.id, story.name, story.author, story.category, story.id, story.name, story.author, story.category,
+36 -2
View File
@@ -9,6 +9,8 @@ const config = require("./config")
const express = require("express") const express = require("express")
const proxy = require("express-http-proxy") const proxy = require("express-http-proxy")
const proxy2 = require("http-proxy-middleware") const proxy2 = require("http-proxy-middleware")
const ip = require("ip")
const dns = require("dns")
const lasso = require("lasso") const lasso = require("lasso")
const lassoMiddleware = require("lasso/middleware") const lassoMiddleware = require("lasso/middleware")
const markoExpress = require("marko/express") const markoExpress = require("marko/express")
@@ -26,7 +28,39 @@ lasso.configure(require("./marko-config"))
// Set trusted proxy // Set trusted proxy
if (config.trustedProxy != null) { 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 // Apply middleware
@@ -68,7 +102,7 @@ app.use("/story/by-author/", require("./routes/story/by-author"))
app.use("/story/tag-list/", require("./routes/story/tag-list")) app.use("/story/tag-list/", require("./routes/story/tag-list"))
app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content")) app.use("/story/:id(S[0-9a-z]{15})/", require("./routes/story-content"))
app.use("/logs/", require("./routes/logs")) app.use("/logs/", require("./routes/logs"))
app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\']{2,})/", require("./routes/logs-content")) app.use("/logs/:id([0-9]{4}-[0-1][0-9]-[0-3][0-9]_[0-9]{9}_[A-Za-z0-9\,\'\-]{2,})/", require("./routes/logs-content"))
app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content")) app.use("/logs/:id(L[0-9]{0,7})/", require("./routes/logs-content"))
app.use("/data/", require("./routes/data")) app.use("/data/", require("./routes/data"))
app.use("/data/characters/", require("./routes/data/characters")) app.use("/data/characters/", require("./routes/data/characters"))