Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a057f6f24 | ||
|
|
853076fd5e | ||
|
|
06bf2b67a2 | ||
|
|
40259b8839 | ||
|
|
3fc9d5de7f | ||
|
|
c69e528105 | ||
|
|
6ea9632623 | ||
|
|
490725ecd2 | ||
|
|
494d0404ed | ||
|
|
624f7046af | ||
|
|
8bfc0dad14 | ||
|
|
3ed3e50486 | ||
|
|
5260d96f37 | ||
|
|
8b3d642104 | ||
|
|
46369f84bd | ||
|
|
68f3effa8d | ||
|
|
4657ae2aec | ||
|
|
f134d57b79 | ||
|
|
9b4da98ac4 | ||
|
|
599ca83f0f | ||
|
|
b952007f3b | ||
|
|
c0e74f4756 | ||
|
|
68a8e7ef97 | ||
|
|
5b81f7ff0d | ||
|
|
78a14ae86b | ||
|
|
c28f17abdd | ||
|
|
de3bae3353 | ||
|
|
fcf2361fbf | ||
|
|
7c1623493a | ||
|
|
013c50d4f4 | ||
|
|
f236d35974 | ||
|
|
55379fcfe3 | ||
|
|
c4636cf843 | ||
|
|
8fd9b19bd0 | ||
|
|
5a5dfea774 | ||
|
|
c157f5f592 |
@@ -0,0 +1,4 @@
|
|||||||
|
.git
|
||||||
|
**/*.marko.js
|
||||||
|
.cache
|
||||||
|
.static
|
||||||
@@ -11,6 +11,7 @@ prebuild.run({
|
|||||||
"./marko/page/logs/list.marko",
|
"./marko/page/logs/list.marko",
|
||||||
"./marko/page/data/channels.marko",
|
"./marko/page/data/channels.marko",
|
||||||
"./marko/page/data/characters.marko",
|
"./marko/page/data/characters.marko",
|
||||||
|
"./marko/page/data/changes.marko",
|
||||||
"./marko/page/story-content/view.marko",
|
"./marko/page/story-content/view.marko",
|
||||||
"./marko/page/logs-content/view.marko",
|
"./marko/page/logs-content/view.marko",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
<if(input.value != null)>
|
||||||
|
<span class=["date", input.class]>${(input.utc ? moment.utc(input.value) : moment(input.value)).format(input.format || "MMM D, YYYY")}</span>
|
||||||
|
</if>
|
||||||
@@ -1,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>
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
const {charactersApi} = require("../../../rpdata/api/Character")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
nicks: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
charactersApi.unknownNicks(100).then(unknowns => {
|
||||||
|
this.state.nicks = unknowns.map(u => u.nick)
|
||||||
|
}).catch(err => {
|
||||||
|
console.warn("Failed to get unknown nicks:", err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<div class="unknown-nick-list">
|
||||||
|
<div for(nick in state.nicks) on-click("emit", "select", nick) key=nick class="nick">${nick}</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
div.unknown-nick-list {
|
||||||
|
padding: 0.5em 0;
|
||||||
|
|
||||||
|
div.nick {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25em 0.5ch;
|
||||||
|
margin: 2px;
|
||||||
|
background-color: #111;
|
||||||
|
color: #881;
|
||||||
|
}
|
||||||
|
div.nick:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #222;
|
||||||
|
color: #CC2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<include("../layout", {title: "Data", site: "data"})>
|
||||||
|
<@body>
|
||||||
|
<background src="/assets/images/bg.png" opacity=0.25 />
|
||||||
|
<changes-page user=input.user changes=input.changes selected=input.selected />
|
||||||
|
</@body>
|
||||||
|
</include>
|
||||||
@@ -18,7 +18,5 @@
|
|||||||
offDesc="The logbot will not monitor this channel."
|
offDesc="The logbot will not monitor this channel."
|
||||||
on-change("change", "logged") />
|
on-change("change", "logged") />
|
||||||
|
|
||||||
<p class="color-danger" if(state.values.logged)>The logbot is not yet available, so this does nothing right now.</p>
|
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -33,6 +33,14 @@ module.exports = class {
|
|||||||
this.emit("close")
|
this.emit("close")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectUnknown(nick) {
|
||||||
|
this.state.values = Object.assign({}, this.state.values, {
|
||||||
|
nick: nick,
|
||||||
|
name: nick.replace(/_/g, " ").replace(/`/g, "'"),
|
||||||
|
shortName: (!nick.includes("_") && nick.includes("`")) ? nick.split("`")[0] : "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,4 +21,12 @@
|
|||||||
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
<textarea key="description" placeholder="Description" on-change("change", "description") value=state.values.description />
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
|
|
||||||
|
<h2>Unknown Nicks</h2>
|
||||||
|
<p>After you're done adding the one you're here for, why not take a look at the unknown nicks discovered in the last log character updates?</p>
|
||||||
|
<p>Clicking one will change the nick in the form above, and add a guess for what the full name is.</p>
|
||||||
|
<if-permitted not user=input.user permission="character.add">
|
||||||
|
<p class="color-danger">You don't have permission to set a different author on characters, so only add your own.</p>
|
||||||
|
</if-permitted>
|
||||||
|
<unknown-nick-list on-select("selectUnknown") />
|
||||||
</modal>
|
</modal>
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
const moment = require("moment")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate() {
|
||||||
|
this.state = {
|
||||||
|
operation: "",
|
||||||
|
links: [],
|
||||||
|
parentLink: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onInput(input) {
|
||||||
|
if (input.data) {
|
||||||
|
this.refresh(input.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh(data) {
|
||||||
|
this.state.links = []
|
||||||
|
this.state.operation = opText[data.op]
|
||||||
|
this.state.parentLink = null
|
||||||
|
|
||||||
|
switch (data.model) {
|
||||||
|
case "Channel": {
|
||||||
|
const key = data.keys.find(k => k.id !== "*")
|
||||||
|
|
||||||
|
this.state.operation += " channel"
|
||||||
|
this.state.links = [{
|
||||||
|
text: key.id,
|
||||||
|
href: `/data/channels/`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Character": {
|
||||||
|
const character = data.objects.find(o => o.type === "Character")
|
||||||
|
|
||||||
|
this.state.operation += " character"
|
||||||
|
this.state.links = [{
|
||||||
|
text: `${character.name} (${character.id})`,
|
||||||
|
href: `/data/characters/`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Story": {
|
||||||
|
const key = data.keys.find(k => k.model === "Story" && k.id !== "*")
|
||||||
|
const story = data.objects.find(o => o.type === "Story")
|
||||||
|
|
||||||
|
this.state.operation += " story"
|
||||||
|
this.state.links = [{
|
||||||
|
text: story.name,
|
||||||
|
href: `/story/${key.id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Chapter": {
|
||||||
|
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")
|
||||||
|
|
||||||
|
this.state.operation += " chapter"
|
||||||
|
this.state.links = [{
|
||||||
|
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}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log": {
|
||||||
|
const key = data.keys.find(k => k.id !== "*")
|
||||||
|
|
||||||
|
this.state.operation += " log"
|
||||||
|
this.state.links = [{
|
||||||
|
text: key.id,
|
||||||
|
href: `/logs/${key.id}`,
|
||||||
|
}]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Post": {
|
||||||
|
const logKey = data.keys.find(k => k.model === "Log")
|
||||||
|
const postKeys = data.keys.filter(k => k.model === "Post")
|
||||||
|
const postObjs = data.objects.filter(o => o.type === "Post")
|
||||||
|
|
||||||
|
this.state.operation += (postKeys.length > 1) ? " posts" : " post"
|
||||||
|
this.state.links = postObjs.map(po => ({
|
||||||
|
text: `[${moment(po.time).format("HH:mm")}] ${po.nick}`,
|
||||||
|
href: `/logs/${logKey.id}#${po.id}`,
|
||||||
|
}))
|
||||||
|
this.state.parentLink = {
|
||||||
|
prefix: "in log",
|
||||||
|
text: `${logKey.id}`,
|
||||||
|
href: `/logs/${logKey.id}`,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const opText = {
|
||||||
|
add: "added",
|
||||||
|
remove: "removed",
|
||||||
|
move: "moved",
|
||||||
|
edit: "edited",
|
||||||
|
import: "imported",
|
||||||
|
tag: "tagged",
|
||||||
|
untag: "untagged",
|
||||||
|
"move-in": "moved in",
|
||||||
|
"move-out": "moved out",
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
<div class="change">
|
||||||
|
<span class="change-id color-menu">[${moment(input.data.date).format("MMM D HH:mm:ss")}] * </span>
|
||||||
|
<span class="author color-text">${input.data.author} </span>
|
||||||
|
<span class="text color-text">${state.operation} </span>
|
||||||
|
<span for(link in state.links | status-var=loop) key=link.href class="link">
|
||||||
|
<a class="color-primary" href=link.href>${link.text}</a>
|
||||||
|
<span if(loop.getIndex() < state.links.length - 1) class="comma">, </span>
|
||||||
|
</span>
|
||||||
|
<if (state.parentLink != null)>
|
||||||
|
<span class="text"> ${state.parentLink.prefix} </span>
|
||||||
|
<a class="color-primary" href=state.parentLink.href>${state.parentLink.text}</a>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
div.change {
|
||||||
|
text-indent: -2ch;
|
||||||
|
margin-left: 2ch;
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
characters: input.characters,
|
||||||
|
modal: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open(modal) {
|
||||||
|
this.state.modal = modal
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.state.modal = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<data-menu categories=input.categories selected=(input.selected || {}) user=input.user on-open("open") />
|
||||||
|
<main>
|
||||||
|
<div class="changes-page">
|
||||||
|
<h1 class="color-primary">Changes</h1>
|
||||||
|
<p>All changes to <em>listed</em> resources is stored for 90 days for transparency reasons. The current state of an object after each change exists, but is only available through the GraphQL API at this time.</p>
|
||||||
|
<for (change in input.changes)>
|
||||||
|
<change data=change />
|
||||||
|
</for>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<add-character-modal enabled=(state.modal === "character.add") user=input.user on-added("characterAdded") on-close("close") />
|
||||||
|
<add-channel-modal enabled=(state.modal === "channel.add") user=input.user on-close("close") />
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
.changes-page {
|
||||||
|
width: 90%;
|
||||||
|
max-width: 95ch;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-weight: 200;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5em 0.5ch 0.5em 0.5ch;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,7 +39,24 @@ module.exports = class {
|
|||||||
}).catch(errs => {
|
}).catch(errs => {
|
||||||
console.warn("Failed to add nick:", errs)
|
console.warn("Failed to add nick:", errs)
|
||||||
|
|
||||||
this.state.error = "Failed to add nick: " + errs[0].message
|
this.state.error = errs[0].message
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
selectUnknown(nick) {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {id: this.input.character.id, nick}
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
charactersApi.addNick(input).then((res) => {
|
||||||
|
this.emit("nicks", res.nicks)
|
||||||
|
}).catch(errs => {
|
||||||
|
this.state.error = errs[0].message
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
this.state.loading = false
|
this.state.loading = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,9 +10,12 @@
|
|||||||
</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>
|
||||||
|
|
||||||
|
<h2>Unknown Nicks</h2>
|
||||||
|
<p>These are the 100 most common nicks in the logs, maybe one of these belongs to <strong>${input.character.name}</strong>?</p>
|
||||||
|
<unknown-nick-list key="un-list" on-select("selectUnknown") />
|
||||||
</modal>
|
</modal>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
<menu-header>List</menu-header>
|
<menu-header>List</menu-header>
|
||||||
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
<menu-link key="characters" selected=input.selected.characters icon="C" href="/data/characters/">Characters</menu-link>
|
||||||
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
|
<menu-link key="channels" selected=input.selected.channels icon="#" href="/data/channels/">Channels</menu-link>
|
||||||
|
<menu-link key="history" selected=input.selected.changes icon="H" href="/data/changes/">History</menu-link>
|
||||||
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
<if-permitted user=input.user permission=["member", "character.add", "channel.add"]>
|
||||||
<menu-gap />
|
<menu-gap />
|
||||||
<menu-header>Add</menu-header>
|
<menu-header>Add</menu-header>
|
||||||
|
|||||||
@@ -15,7 +15,5 @@
|
|||||||
offDesc="The logbot will not monitor this channel."
|
offDesc="The logbot will not monitor this channel."
|
||||||
on-change("change", "logged") />
|
on-change("change", "logged") />
|
||||||
|
|
||||||
<p class="color-danger" if(state.values.logged)>The logbot is not yet available, so this does nothing right now.</p>
|
|
||||||
|
|
||||||
<button disabled=state.loading on-click("save")>Save</button>
|
<button disabled=state.loading on-click("save")>Save</button>
|
||||||
</modal>
|
</modal>
|
||||||
@@ -19,16 +19,12 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInput(input) {
|
onInput(input) {
|
||||||
if (input.log && !this.first) {
|
|
||||||
this.state.values = {
|
this.state.values = {
|
||||||
title: input.log.title,
|
title: input.log.title,
|
||||||
event: input.log.eventName,
|
event: input.log.eventName,
|
||||||
description: input.log.description,
|
description: input.log.description,
|
||||||
open: input.log.open,
|
open: input.log.open,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.first = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
change(key, ev) {
|
change(key, ev) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
const moment = require("moment")
|
const moment = require("moment")
|
||||||
|
|
||||||
const {postApi} = require("../../../../../rpdata/api/Post")
|
const {postApi} = require("../../../../../rpdata/api/Post")
|
||||||
|
const {logsApi} = require("../../../../../rpdata/api/Log")
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
@@ -11,6 +12,22 @@ module.exports = class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
this.sub = logsApi.subscribeChanges(this.state.log.id)
|
||||||
|
|
||||||
|
this.sub.on("data", data => {
|
||||||
|
this.handleChange(data.changes)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.sub.connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmount() {
|
||||||
|
if (this.sub != null) {
|
||||||
|
this.sub.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
open(modal) {
|
open(modal) {
|
||||||
this.state.modal = modal
|
this.state.modal = modal
|
||||||
}
|
}
|
||||||
@@ -30,8 +47,6 @@ module.exports = class {
|
|||||||
for (const patch of patches) {
|
for (const patch of patches) {
|
||||||
const index = posts.findIndex(p => p.id === patch.id)
|
const index = posts.findIndex(p => p.id === patch.id)
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
console.warn("Post not found for patch:", patch)
|
|
||||||
// TODO: Force full refresh
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,8 +57,7 @@ module.exports = class {
|
|||||||
posts.sort((a, b) => a.position - b.position)
|
posts.sort((a, b) => a.position - b.position)
|
||||||
for (let i = 1; i < posts.length; ++i) {
|
for (let i = 1; i < posts.length; ++i) {
|
||||||
if (posts[i].position !== posts[i - 1].position + 1) {
|
if (posts[i].position !== posts[i - 1].position + 1) {
|
||||||
console.warn("Discontinuity detected!")
|
console.warn("Discontinuity detected:", i, posts)
|
||||||
// TODO: Force full refresh
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,15 +79,7 @@ module.exports = class {
|
|||||||
|
|
||||||
removePost(post) {
|
removePost(post) {
|
||||||
postApi.remove({id: post.id}).then(() => {
|
postApi.remove({id: post.id}).then(() => {
|
||||||
this.state.log.posts = this.state.log.posts.filter(p => p.id !== post.id)
|
this.postRemoved(post)
|
||||||
|
|
||||||
for (const p of this.state.log.posts) {
|
|
||||||
if (p.position > post.position) {
|
|
||||||
p.position--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state.log = Object.assign({}, this.state.log)
|
|
||||||
}).catch(errs => {
|
}).catch(errs => {
|
||||||
console.warn("Failed to delete:", errs)
|
console.warn("Failed to delete:", errs)
|
||||||
this.state.error = "Failed to delete: " + (errs.message || errs[0].message)
|
this.state.error = "Failed to delete: " + (errs.message || errs[0].message)
|
||||||
@@ -88,11 +94,69 @@ module.exports = class {
|
|||||||
this.state.log = Object.assign(Object.assign({}, this.state.log), patch)
|
this.state.log = Object.assign(Object.assign({}, this.state.log), patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postRemoved(post) {
|
||||||
|
if (this.state.log.posts.find(p => p.id === post.id) == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = this.state.log.posts.filter(p => p.id !== post.id).map(p => {
|
||||||
|
if (p.position > post.position) {
|
||||||
|
return Object.assign({}, p, {position: p.position - 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
return p
|
||||||
|
})
|
||||||
|
|
||||||
|
this.state.log = Object.assign({}, this.state.log, {posts: posts})
|
||||||
|
}
|
||||||
|
|
||||||
postAdded(post) {
|
postAdded(post) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleChange(change) {
|
||||||
|
switch (`${change.model}.${change.op}`) {
|
||||||
|
case "Post.add": {
|
||||||
|
for (const post of change.objects.filter(o => o.type === "Post")) {
|
||||||
|
this.postAdded(post)
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Post.move":
|
||||||
|
case "Post.edit": {
|
||||||
|
this.patch(change.objects.filter(o => o.type === "Post"))
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Post.remove": {
|
||||||
|
for (const post of change.objects.filter(o => o.type === "Post")) {
|
||||||
|
this.postRemoved(post)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log.edit": {
|
||||||
|
const patch = change.objects.find(o => o.type === "Log")
|
||||||
|
if (patch == null) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logEdited(patch)
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case "Log.remove": {
|
||||||
|
this.logRemoved()
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logRemoved() {
|
logRemoved() {
|
||||||
this.state.removed = true
|
this.state.removed = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<if(state.text != null && state.text != "")>
|
<if(state.text != null && state.text != "")>
|
||||||
<div class=["post-meta", input.kind, state.color, input.weak ? "weak" : null] title=state.tooltip>
|
<div class=["post-meta", input.kind, state.color, input.weak ? "weak" : null, input.class] 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>${state.text}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,3 +18,13 @@ div.post-meta {
|
|||||||
div.post-meta.weak {
|
div.post-meta.weak {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.post-meta.nick.pk-text {
|
||||||
|
color: #AAA;
|
||||||
|
}
|
||||||
|
div.post-meta.nick.pk-action {
|
||||||
|
color: #A64;
|
||||||
|
}
|
||||||
|
div.post-meta.nick.pk-scene {
|
||||||
|
color: #68A;
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
const EventEmitter = require("events");
|
||||||
|
|
||||||
|
let linkSignal = null;
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -8,8 +12,12 @@ module.exports = class {
|
|||||||
modal: null,
|
modal: null,
|
||||||
removed: false,
|
removed: false,
|
||||||
multipart: false,
|
multipart: false,
|
||||||
|
anchored: false,
|
||||||
|
prevWasText: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.mounted = false;
|
||||||
|
|
||||||
this.updatePost(input)
|
this.updatePost(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,20 +49,71 @@ 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.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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMount() {
|
||||||
|
this.linkSignalCallback = () => {
|
||||||
|
this.state.anchored = false
|
||||||
|
}
|
||||||
|
if (linkSignal == null) {
|
||||||
|
linkSignal = new EventEmitter()
|
||||||
|
linkSignal.setMaxListeners(10000)
|
||||||
|
}
|
||||||
|
linkSignal.on("signal", this.linkSignalCallback)
|
||||||
|
|
||||||
|
this.state.anchored = window.location.hash.slice(1) === this.input.post.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnmount() {
|
||||||
|
linkSignal.removeListener("signal", this.linkSignalCallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
link() {
|
||||||
|
linkSignal.emit("signal")
|
||||||
|
this.state.anchored = true
|
||||||
|
}
|
||||||
|
|
||||||
updatePost(input) {
|
updatePost(input) {
|
||||||
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, "_")
|
this.state.text = input.post.text.replace(/\x02/g, "**").replace(/\x1D/g, "*").replace(/\`/g, "\\`").trim();
|
||||||
|
|
||||||
if (input.post.kind === "text" && !this.state.text.includes("\"")) {
|
// Fix Ctrl-Bs
|
||||||
|
let left = false
|
||||||
|
this.state.text = this.state.text.replace(/\s*\*\*\s*/g, str => {
|
||||||
|
left = !left
|
||||||
|
|
||||||
|
if (left) {
|
||||||
|
return " **"
|
||||||
|
} else if (!left) {
|
||||||
|
return "** "
|
||||||
|
}
|
||||||
|
|
||||||
|
return str
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.state.text.startsWith("|")) {
|
||||||
|
this.state.name = ""
|
||||||
|
this.state.text = this.state.text.slice(1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.post.kind === "text" && !this.state.text.includes("\"") && !this.state.text.includes("\''") && !this.state.text.includes("|")) {
|
||||||
|
const colonIndex = this.state.text.indexOf(": ");
|
||||||
|
if (colonIndex != -1 && colonIndex < this.state.text.indexOf(" ")) {
|
||||||
|
this.state.text = this.state.text.replace(": ", ": \"") + "\"";
|
||||||
|
} else {
|
||||||
this.state.text = '"' + this.state.text + '"'
|
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("=")) {
|
||||||
const postNick = input.post.nick.replace("'s", "").replace("s'", "s")
|
const postNick = input.post.nick.replace("'s", "").replace("s'", "s")
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<div if(!state.removed) class=["post", component.kindClass("post-type"), state.multipart ? "multipart" : null]>
|
<div if(!state.removed) class=["post", component.kindClass("pk-"), state.multipart ? "multipart" : null, state.anchored ? "anchored" : null]>
|
||||||
|
<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">
|
||||||
|
<a on-click("link") href=("#" + input.post.id) class="color-menu">Link</a>
|
||||||
<if-permitted user=input.user permission="post.move">
|
<if-permitted user=input.user permission="post.move">
|
||||||
<a on-click("move", -1, true)>Up</a>
|
<a on-click("move", -1, true)>Up</a>
|
||||||
<a on-click("move", +1, true)>Down</a>
|
<a on-click("move", +1, true)>Down</a>
|
||||||
@@ -13,7 +15,7 @@
|
|||||||
</if-permitted>
|
</if-permitted>
|
||||||
</div>
|
</div>
|
||||||
<post-meta kind="timestamp" value=input.post.time />
|
<post-meta kind="timestamp" value=input.post.time />
|
||||||
<post-meta kind="nick" value=input.post.nick />
|
<post-meta kind="nick" class=("pk-"+input.post.kind) value=input.post.nick />
|
||||||
</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))>
|
||||||
@@ -24,7 +26,7 @@
|
|||||||
<markdown class="post-content post-scene" source=state.text />
|
<markdown 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) />
|
<markdown 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">
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
div.post {
|
div.post {
|
||||||
|
margin-top: 1em;
|
||||||
margin-bottom: 1ch;
|
margin-bottom: 1ch;
|
||||||
|
margin-left: -12px;
|
||||||
|
padding-left: 12px;
|
||||||
|
padding-right: 0.5ch;
|
||||||
|
|
||||||
|
transition: background-color 0.333s linear;
|
||||||
|
|
||||||
div.post-meta-row {
|
div.post-meta-row {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.anchor {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
top: -4em;
|
||||||
|
}
|
||||||
|
|
||||||
div.options {
|
div.options {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
@@ -24,6 +36,10 @@ div.post {
|
|||||||
a:hover {
|
a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
width: calc(100% - 6.5ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.post-body {
|
div.post-body {
|
||||||
@@ -48,15 +64,34 @@ div.post {
|
|||||||
div.annotation {
|
div.annotation {
|
||||||
margin-top: 0.33em;
|
margin-top: 0.33em;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
div.post-body p:first-of-type {
|
p:first-of-type {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
p:last-of-type {
|
||||||
|
padding-bottom: 0.1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.pk-scene {
|
||||||
|
margin-left: -14px;
|
||||||
|
border-left: 2px solid #68A;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.post.pk-scene.anchored {
|
||||||
|
background-color: rgba(102, 136, 170, 0.25);
|
||||||
|
}
|
||||||
|
div.post.pk-action.anchored {
|
||||||
|
background-color: rgba(170, 102, 68, 0.25);
|
||||||
|
}
|
||||||
|
div.post.pk-text.anchored {
|
||||||
|
background-color: rgba(170, 170, 170, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
div.post.multipart {
|
div.post.multipart {
|
||||||
margin-top: -1.25em;
|
margin-top: -0.9em;
|
||||||
|
|
||||||
div.post-meta {
|
div.post-meta {
|
||||||
span {
|
span {
|
||||||
|
|||||||
@@ -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}) {
|
||||||
@@ -127,3 +127,28 @@ 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
const {tz} = require("moment-timezone")
|
||||||
|
|
||||||
|
const {logsApi} = require("../../../../../rpdata/api/Log")
|
||||||
|
|
||||||
|
module.exports = class {
|
||||||
|
onCreate(input) {
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
loading: false,
|
||||||
|
imported: null,
|
||||||
|
values: {
|
||||||
|
importer: "MircLike",
|
||||||
|
timezone: tz.guess() || "UTC",
|
||||||
|
channelName: "",
|
||||||
|
date: "",
|
||||||
|
data: "",
|
||||||
|
},
|
||||||
|
timezones: tz.names(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.first = false
|
||||||
|
}
|
||||||
|
|
||||||
|
change(key, ev) {
|
||||||
|
this.state.values[key] = ev.target.value
|
||||||
|
this.state.values = Object.assign({}, this.state.values)
|
||||||
|
}
|
||||||
|
|
||||||
|
open() {
|
||||||
|
this.state.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
this.emit("close")
|
||||||
|
|
||||||
|
this.state.values = {
|
||||||
|
importer: "MircLike",
|
||||||
|
timezone: tz.guess() || "UTC",
|
||||||
|
channelName: "",
|
||||||
|
date: "",
|
||||||
|
data: "",
|
||||||
|
}
|
||||||
|
this.state.imported = null
|
||||||
|
this.state.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
save() {
|
||||||
|
if (this.state.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.state.error = null;
|
||||||
|
|
||||||
|
const input = Object.assign({}, this.state.values)
|
||||||
|
if (input.date != "" && input.importer !== "ForumLog") {
|
||||||
|
input.date = new Date(input.date)
|
||||||
|
if (Number.isNaN(input.date)) {
|
||||||
|
this.state.error = "Invalid date"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (input.importer === "MircLike") {
|
||||||
|
this.state.error = "Date is required for mIRC-like logs"
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
input.date = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.channelName.length < 2 || input.channelName.includes(" ") || input.channelName.includes(" ") || input.channelName.charAt(0) !== "#") {
|
||||||
|
this.state.error = "A valid channelName name is required"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.loading = true
|
||||||
|
logsApi.import(input).then(logs => {
|
||||||
|
this.state.imported = logs
|
||||||
|
}).catch(errs => {
|
||||||
|
console.warn("Import failed:", errs)
|
||||||
|
|
||||||
|
this.state.error = "Import failed: " + errs[0].message
|
||||||
|
}).then(() => {
|
||||||
|
this.state.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
<modal class="modal color-text nolabel" key="modal" enabled=(input.enabled) closable on-close("close") on-open("open") >
|
||||||
|
<if(state.imported == null)>
|
||||||
|
<h1>Import Log</h1>
|
||||||
|
|
||||||
|
<p key="error" class="color-error">${state.error}</p>
|
||||||
|
|
||||||
|
<label>Type</label>
|
||||||
|
<select key="importer" autofocus class="big" placeholder="Kind" on-change("change", "importer") value=state.values.importer>
|
||||||
|
<option value="MircLike" selected=(state.values.importer === "MircLike")>mIRC-Like log</option>
|
||||||
|
<option value="ForumLog" selected=(state.values.importer === "ForumLog")>Forum Log</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Timezone</label>
|
||||||
|
<select key="timezone" class="big" placeholder="Kind" on-change("change", "timezone") value=state.values.timezone>
|
||||||
|
<option for(tz in state.timezones) value=tz selected=(state.values.timezone === tz)>${tz}</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>Channel</label>
|
||||||
|
<input key="channelName" placeholder="(Required)" class="big" on-change("change", "channelName") value=state.values.channelName />
|
||||||
|
|
||||||
|
<if(state.values.importer === "MircLike")>
|
||||||
|
<label>Date</label>
|
||||||
|
<input key="date" class="big" placeholder="(Required)" on-change("change", "date") value=state.values.date />
|
||||||
|
</if>
|
||||||
|
|
||||||
|
<label>Content</label>
|
||||||
|
<textarea key="data" placeholder="(Required)" class="tall" on-change("change", "data") value=state.values.data />
|
||||||
|
|
||||||
|
<button disabled=state.loading on-click("save")>Import</button>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
|
<h1>Import Successful</h1>
|
||||||
|
|
||||||
|
<p>The following logs were imported.</p>
|
||||||
|
|
||||||
|
<ol class="imported-log-list">
|
||||||
|
<for (log in state.imported)>
|
||||||
|
<li><a class="color-primary" href=("/logs/" + log.id)>${log.id} (${moment(log.date).format("MMMM D, YYYY")})</a></li>
|
||||||
|
</for>
|
||||||
|
</ol>
|
||||||
|
</else>
|
||||||
|
</modal>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
ol.imported-log-list {
|
||||||
|
color: #777;
|
||||||
|
|
||||||
|
li {
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,5 +3,6 @@
|
|||||||
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
|
<menu-link key="logs" selected=(input.selected.index && !state.filtered) icon="L" href="/logs/">All</menu-link>
|
||||||
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
<if-permitted key="if-permitted" user=input.user permission="log.add">
|
||||||
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
<menu-link key="add_log" dark on-click("select", "log.add") icon="+">Add Log</menu-link>
|
||||||
|
<menu-link key="import_log" dark on-click("select", "log.import") icon="+">Import Log</menu-link>
|
||||||
</if-permitted>
|
</if-permitted>
|
||||||
</menu>
|
</menu>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
const {logHeaderApi} = require("../../../../../rpdata/api/LogHeader")
|
const { logHeaderApi } = require("../../../../../rpdata/api/LogHeader")
|
||||||
|
|
||||||
module.exports = class {
|
module.exports = class {
|
||||||
onCreate(input) {
|
onCreate(input) {
|
||||||
@@ -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) {
|
||||||
|
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
|
||||||
|
|||||||
@@ -1,7 +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") />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,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"
|
||||||
this.state.text = value
|
this.state.text = value
|
||||||
|
|||||||
@@ -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,25 @@
|
|||||||
<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 author=input.chapter.author permission="chapter.edit">
|
||||||
|
<div class="add-comment color-menu" on-click("open", "addComment") >Add ${state.commentLabels[input.chapter.commentMode] || "Comment"}</div>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
<edit-chapter-modal enabled=(state.modal === "edit") chapter=input.chapter on-close("close") on-edit("updateChapter") />
|
<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>
|
||||||
@@ -40,9 +40,39 @@
|
|||||||
a:hover {
|
a:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 700px) {
|
||||||
|
width: calc(100% - 6.5ch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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,119 @@
|
|||||||
|
import moment from "moment"
|
||||||
|
|
||||||
|
<div class=["comment", "mode-" + input.mode.toLowerCase()]>
|
||||||
|
<if (input.mode === "Message")>
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<chapter-meta weak kind="date" value=input.comment.createdDate />
|
||||||
|
<chapter-meta weak kind="author" value=input.comment.author />
|
||||||
|
</div>
|
||||||
|
<div class="content color-blockquote">
|
||||||
|
<table class="header">
|
||||||
|
<tbody>
|
||||||
|
<tr if(input.comment.subject != "")>
|
||||||
|
<th>Subject:</th>
|
||||||
|
<td>${input.comment.subject}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>From:</th>
|
||||||
|
<td>
|
||||||
|
<div if(input.comment.character != null) class="tooltip" style="display: inline-block">
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark">
|
||||||
|
<h1>${input.comment.character.name}</h1>
|
||||||
|
<h2>${input.comment.character.author}</h2>
|
||||||
|
<markdown source=input.comment.character.description />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div else>
|
||||||
|
<span class="tooltip color-primary">
|
||||||
|
<span>${input.comment.characterName}</span>
|
||||||
|
|
||||||
|
<div class="tooltip-content color-highlight-dark color-primary">
|
||||||
|
<h1>(Unknown Character)</h1>
|
||||||
|
<h2>${input.comment.author}</h2>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr if(input.comment.fictionalDate != null)>
|
||||||
|
<th>Date:</th>
|
||||||
|
<td>${moment(input.comment.fictionalDate).format("MMM D, YYYY")}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<markdown class="body" source=input.comment.source />
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<if(input.mode === "Chat")>
|
||||||
|
<div class="content">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<for (paragraph in state.paragraphs)>
|
||||||
|
<div class="message-text color-text">
|
||||||
|
<markdown class="body" source=paragraph>
|
||||||
|
<if (input.comment.fictionalDate)>
|
||||||
|
<span class="decoration color-menu">[</span>
|
||||||
|
<date class="color-menu" value=input.comment.fictionalDate></date>
|
||||||
|
<span class="decoration color-menu">] </span>
|
||||||
|
</if>
|
||||||
|
<span class="decoration color-menu"><</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">> </span>
|
||||||
|
</markdown>
|
||||||
|
</div>
|
||||||
|
</for>
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
<if(input.mode === "Article")>
|
||||||
|
<div class="metadata">
|
||||||
|
<div class="options color-menu">
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "editComment", input.comment) >Edit</a>
|
||||||
|
</if-permitted>
|
||||||
|
<if-permitted user=input.user author=input.comment.author permission="story.edit">
|
||||||
|
<a on-click("emit", "open", "removeComment", input.comment) >Remove</a>
|
||||||
|
</if-permitted>
|
||||||
|
</div>
|
||||||
|
<chapter-meta kind="title" value=input.comment.subject />
|
||||||
|
<chapter-meta kind="name" value=input.comment.characterName character=(input.comment.character) ukAuthor=input.comment.author />
|
||||||
|
<chapter-meta kind="date" value=input.comment.fictionalDate />
|
||||||
|
<chapter-meta weak kind="date" value=input.comment.createdDate />
|
||||||
|
<chapter-meta weak kind="author" value=input.comment.author />
|
||||||
|
</div>
|
||||||
|
<div class="content color-text">
|
||||||
|
<markdown class="body" source=input.comment.source />
|
||||||
|
</div>
|
||||||
|
</if>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
div.comment {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
div.options {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 95ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-message {
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
font-size: 1em;
|
||||||
|
|
||||||
|
.metadata {
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
outline: 1px solid;
|
||||||
|
padding: 0.25em 1ch;
|
||||||
|
|
||||||
|
font-size: 1.25em;
|
||||||
|
|
||||||
|
table.header {
|
||||||
|
border-bottom: 0.5px solid;
|
||||||
|
margin: 0;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
tr {
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
th {
|
||||||
|
width: 12ch;
|
||||||
|
padding-right: 1ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
padding-top: 0.5em;
|
||||||
|
|
||||||
|
p:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-chat {
|
||||||
|
div.options {
|
||||||
|
display: none;
|
||||||
|
bottom: -0.25em;
|
||||||
|
|
||||||
|
a {
|
||||||
|
background: black;
|
||||||
|
opacity: 0.666;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.content {
|
||||||
|
line-height: 2em;
|
||||||
|
font-size: 1.125em;
|
||||||
|
|
||||||
|
div.tooltip-content {
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.message-text {
|
||||||
|
vertical-align: top;
|
||||||
|
padding-left: 2ch;
|
||||||
|
text-indent: -2ch;
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
p:first-of-type {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
p:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
line-height: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-article {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
font-size: 1.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.comment.mode-chat:hover {
|
||||||
|
div.options {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ module.exports = class {
|
|||||||
title: "",
|
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>
|
||||||
@@ -46,8 +46,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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,18 @@
|
|||||||
</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(input.open)>
|
||||||
|
<if-permitted user=input.user permission="member">
|
||||||
|
<a on-click("select", "edit") class="option color-menu">Manage Tags (Open)</a>
|
||||||
|
</if-permitted>
|
||||||
|
</if>
|
||||||
|
<else>
|
||||||
<if-permitted user=input.user author=input.author permission="story.edit">
|
<if-permitted user=input.user author=input.author permission="story.edit">
|
||||||
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
|
<a on-click("select", "edit") class="option color-menu">Manage Tags</a>
|
||||||
</if-permitted>
|
</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 {
|
||||||
|
|||||||
Generated
+1196
-48
File diff suppressed because it is too large
Load Diff
@@ -25,6 +25,8 @@
|
|||||||
"express-http-proxy": "^1.2.0",
|
"express-http-proxy": "^1.2.0",
|
||||||
"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",
|
||||||
|
"ip": "^1.1.5",
|
||||||
"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",
|
||||||
@@ -37,8 +39,12 @@
|
|||||||
"markdown-it-regexp": "^0.4.0",
|
"markdown-it-regexp": "^0.4.0",
|
||||||
"marko": "^4.13.8",
|
"marko": "^4.13.8",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
|
"moment-timezone": "^0.5.23",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-auth0": "^1.0.0",
|
"passport-auth0": "^1.0.0",
|
||||||
"pluralize": "^7.0.0"
|
"pluralize": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/http-proxy-middleware": "^0.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
const express = require("express")
|
||||||
|
const router = express.Router()
|
||||||
|
|
||||||
|
const {changesApi} = require("../../rpdata/api/Change")
|
||||||
|
|
||||||
|
const changesTemplate = require("../../marko/page/data/changes.marko")
|
||||||
|
|
||||||
|
router.get("/", async(req, res) => {
|
||||||
|
res.markoAsync(changesTemplate, {
|
||||||
|
changes: changesApi.list({limit: 2000}),
|
||||||
|
selected: {changes: true},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
module.exports = router
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const proxy = require("http-proxy-middleware")
|
||||||
const express = require("express")
|
const express = require("express")
|
||||||
const jwt = require("jsonwebtoken")
|
const jwt = require("jsonwebtoken")
|
||||||
|
|
||||||
@@ -44,6 +45,7 @@ router.post("/", (req, res) => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
router.use("/", proxy(config.graphqlEndpoint, {ws: true}))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} user
|
* @param {string} user
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const {query} = require("../client")
|
||||||
|
|
||||||
|
class Change {
|
||||||
|
/**
|
||||||
|
* @param {string} id
|
||||||
|
* @param {string} model
|
||||||
|
* @param {string} op
|
||||||
|
* @param {string} author
|
||||||
|
* @param {Date | string | number} date
|
||||||
|
* @param {{model:string,id:string}[]} keys '
|
||||||
|
* @param {{type:string, [x:string]:any}[]} objects
|
||||||
|
*/
|
||||||
|
constructor(id, model, op, author, date, keys, objects) {
|
||||||
|
this.id = id
|
||||||
|
this.model = model
|
||||||
|
this.op = op
|
||||||
|
this.author = author
|
||||||
|
this.date = date
|
||||||
|
this.keys = keys
|
||||||
|
this.objects = objects
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromData(data) {
|
||||||
|
return new Change(data.id, data.model, data.op, data.author, data.date, data.keys, data.objects)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangesAPI {
|
||||||
|
/**
|
||||||
|
* Call `channels(filter)` query
|
||||||
|
*
|
||||||
|
* @param {{limit:int, keys: {model:string,id:string}[]}} filter
|
||||||
|
* @returns {Promise<Change[]>}
|
||||||
|
*/
|
||||||
|
list(filter = null) {
|
||||||
|
return query(`
|
||||||
|
query ListChanges($filter:ChangesFilter) {
|
||||||
|
changes(filter:$filter) {
|
||||||
|
id
|
||||||
|
model
|
||||||
|
op
|
||||||
|
author
|
||||||
|
date
|
||||||
|
keys {
|
||||||
|
model
|
||||||
|
id
|
||||||
|
}
|
||||||
|
objects {
|
||||||
|
type: __typename
|
||||||
|
|
||||||
|
...on Character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
...on Chapter {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
|
||||||
|
...on Story {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
...on Post {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
nick
|
||||||
|
}
|
||||||
|
|
||||||
|
...on Comment {
|
||||||
|
chapterId
|
||||||
|
characterName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {filter}).then(({changes}) => {
|
||||||
|
return changes.map(d => Change.fromData(d))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { Change, changesApi: new ChangesAPI }
|
||||||
|
|
||||||
+50
-3
@@ -1,4 +1,5 @@
|
|||||||
const {query} = require("../client")
|
const {query} = require("../client")
|
||||||
|
const {Comment} = require("./Comment")
|
||||||
|
|
||||||
class Chapter {
|
class Chapter {
|
||||||
/**
|
/**
|
||||||
@@ -9,8 +10,12 @@ class Chapter {
|
|||||||
* @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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,6 +190,19 @@ class ChracterAPI {
|
|||||||
return removeCharacter
|
return removeCharacter
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unknownNicks(limit = 100) {
|
||||||
|
return query(`
|
||||||
|
query UnknownNicks($limit: Int!) {
|
||||||
|
unknownNicks(filter: {limit: $limit}) {
|
||||||
|
nick
|
||||||
|
score
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {limit}).then(({unknownNicks}) => {
|
||||||
|
return unknownNicks
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Character, charactersApi: new ChracterAPI }
|
module.exports = { Character, charactersApi: new ChracterAPI }
|
||||||
@@ -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}
|
||||||
+90
-2
@@ -1,4 +1,4 @@
|
|||||||
const {query} = require("../client")
|
const {query, Subscription} = require("../client")
|
||||||
|
|
||||||
const {Character} = require("./Character")
|
const {Character} = require("./Character")
|
||||||
const {Channel} = require("./Channel")
|
const {Channel} = require("./Channel")
|
||||||
@@ -124,6 +124,53 @@ class LogAPI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call `importLog(input)` mutation, returns multpile logs.
|
||||||
|
*
|
||||||
|
* @param {{importer:"MircLike"|"ForumLog", date?:Date|string|number, channel:string, timezone:string, data:string}} input
|
||||||
|
* @returns {Promise<Log[]>}
|
||||||
|
*/
|
||||||
|
import(input) {
|
||||||
|
return query(`
|
||||||
|
mutation ImportLog($input: LogImportInput!) {
|
||||||
|
importLog(input: $input) {
|
||||||
|
id
|
||||||
|
shortId
|
||||||
|
date
|
||||||
|
title
|
||||||
|
eventName
|
||||||
|
description
|
||||||
|
open
|
||||||
|
channel {
|
||||||
|
name
|
||||||
|
logged
|
||||||
|
hub
|
||||||
|
eventName
|
||||||
|
locationName
|
||||||
|
}
|
||||||
|
characters {
|
||||||
|
id
|
||||||
|
nicks
|
||||||
|
author
|
||||||
|
name
|
||||||
|
shortName
|
||||||
|
description
|
||||||
|
}
|
||||||
|
posts {
|
||||||
|
id
|
||||||
|
position
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {input}, {permissions: ["log.add"]}).then(({importLog}) => {
|
||||||
|
return importLog.map(d => Log.fromData(d))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call `editLog(input)` mutation, returns the id and all editable fields.
|
* Call `editLog(input)` mutation, returns the id and all editable fields.
|
||||||
*
|
*
|
||||||
@@ -146,7 +193,6 @@ class LogAPI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call `removeLog(input)` mutation, returns the id
|
* Call `removeLog(input)` mutation, returns the id
|
||||||
*
|
*
|
||||||
@@ -164,6 +210,48 @@ class LogAPI {
|
|||||||
return editLog
|
return editLog
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribe to the changes.
|
||||||
|
*
|
||||||
|
* @param {string} logId The long logId, the short one won't do.
|
||||||
|
*/
|
||||||
|
subscribeChanges(logId) {
|
||||||
|
return new Subscription(`
|
||||||
|
subscription Changes($keys: [ChangeKeyInput!]!) {
|
||||||
|
changes(keys: $keys) {
|
||||||
|
id
|
||||||
|
model
|
||||||
|
op
|
||||||
|
author
|
||||||
|
listed
|
||||||
|
keys {
|
||||||
|
model
|
||||||
|
id
|
||||||
|
}
|
||||||
|
date
|
||||||
|
objects {
|
||||||
|
type: __typename
|
||||||
|
...on Log {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
eventName
|
||||||
|
description
|
||||||
|
open
|
||||||
|
}
|
||||||
|
...on Post {
|
||||||
|
id
|
||||||
|
time
|
||||||
|
kind
|
||||||
|
nick
|
||||||
|
text
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, {keys: [{model: "Log", id: logId}]})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { Log, logsApi: new LogAPI }
|
module.exports = { Log, logsApi: new LogAPI }
|
||||||
+20
-1
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,25 @@ const storyApi = {
|
|||||||
createdDate
|
createdDate
|
||||||
fictionalDate
|
fictionalDate
|
||||||
editedDate
|
editedDate
|
||||||
|
canComment
|
||||||
|
commentMode
|
||||||
|
commentsLocked
|
||||||
|
comments {
|
||||||
|
id
|
||||||
|
subject
|
||||||
|
author
|
||||||
|
characterName
|
||||||
|
character {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
author
|
||||||
|
description
|
||||||
|
}
|
||||||
|
fictionalDate
|
||||||
|
createdDate
|
||||||
|
editedDate
|
||||||
|
source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+86
-1
@@ -1,3 +1,4 @@
|
|||||||
|
const EventEmitter = require("events")
|
||||||
const fetch = require("isomorphic-fetch")
|
const fetch = require("isomorphic-fetch")
|
||||||
const config = require("../config")
|
const config = require("../config")
|
||||||
const compressQuery = require("graphql-query-compress")
|
const compressQuery = require("graphql-query-compress")
|
||||||
@@ -30,4 +31,88 @@ function query(query, variables = {}, options = {}) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { query }
|
class Subscription extends EventEmitter {
|
||||||
|
constructor(query, variables) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
this.query = query
|
||||||
|
this.variables = Object.assign({}, variables)
|
||||||
|
this.websocket = null
|
||||||
|
this.verboseLogging = false
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
const baseUrl = (window.location.protocol === 'https:' ? 'wss://' : 'ws://') + window.location.host
|
||||||
|
let id = 1
|
||||||
|
|
||||||
|
this.websocket = new WebSocket(baseUrl + config.graphqlEndpoint, "graphql-ws")
|
||||||
|
|
||||||
|
this.websocket.onopen = () => {
|
||||||
|
this.websocket.send(JSON.stringify({type: "connection_init", payload: {}}));
|
||||||
|
this.websocket.send(JSON.stringify({id: (id++).toFixed(0), type: "start", payload: {
|
||||||
|
query: this.query,
|
||||||
|
variables: this.variables,
|
||||||
|
extensions: {},
|
||||||
|
}}));
|
||||||
|
|
||||||
|
console.log("WS Open")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websocket.onmessage = (ev) => {
|
||||||
|
let data = {}
|
||||||
|
try {
|
||||||
|
data = JSON.parse(ev.data)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Error", err, ev)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.verboseLogging) {
|
||||||
|
console.log("WS Data:", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (data.type) {
|
||||||
|
case "connection_ack": {
|
||||||
|
this.emit("connect")
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "connection_error": {
|
||||||
|
console.warn("WS Connection Error", data.payload.message)
|
||||||
|
this.websocket.close()
|
||||||
|
|
||||||
|
this.emit("error", new Error(data.payload.message))
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "data": {
|
||||||
|
if (data.payload.errors != null && data.payload.errors.length > 0) {
|
||||||
|
this.emit("error", data.payload.errors)
|
||||||
|
} else {
|
||||||
|
this.emit("data", data.payload.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websocket.onerror = (err) => {
|
||||||
|
console.warn("WS Error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.websocket.onclose = () => {
|
||||||
|
this.emit("disconnect")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this.websocket != null) {
|
||||||
|
this.websocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { query, Subscription }
|
||||||
@@ -8,6 +8,9 @@ const config = require("./config")
|
|||||||
// Express depedencies
|
// Express depedencies
|
||||||
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 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")
|
||||||
@@ -25,7 +28,39 @@ lasso.configure(require("./marko-config"))
|
|||||||
|
|
||||||
// Set trusted proxy
|
// Set trusted proxy
|
||||||
if (config.trustedProxy != null) {
|
if (config.trustedProxy != null) {
|
||||||
|
if (ip.isV4Format(config.trustedProxy) || ip.isV6Format(config.trustedProxy)) {
|
||||||
app.set('trust proxy', 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
|
||||||
@@ -72,6 +107,7 @@ 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"))
|
||||||
app.use("/data/channels/", require("./routes/data/channels"))
|
app.use("/data/channels/", require("./routes/data/channels"))
|
||||||
|
app.use("/data/changes/", require("./routes/data/changes"))
|
||||||
|
|
||||||
// Entry point
|
// Entry point
|
||||||
app.get("/", function(req, res) {
|
app.get("/", function(req, res) {
|
||||||
|
|||||||
Reference in New Issue
Block a user