diff --git a/package.json b/package.json index 33a934f..467abbd 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dev": "nodemon --inspect src/index.js --debug --no-open", "fix": "common-good fix", "start": "node src/index.js", - "test": "tap --timeout 240 && common-good check --dependency-check-suffix '-i changelog-version -i mkdirp -i nodemon -i stylelint-config-recommended'", + "test": "tap --timeout 240 && common-good test --dependency-check-suffix '-i changelog-version -i mkdirp -i nodemon -i stylelint-config-recommended'", "preversion": "npm test", "version": "mv docs/CHANGELOG.md ./ && changelog-version && mv CHANGELOG.md docs/ && git add docs/CHANGELOG.md" }, diff --git a/src/assets/style.css b/src/assets/style.css index 3af4de2..988a3ec 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -113,7 +113,8 @@ a { color: var(--fg-light); } -button, .file-button { +button, +.file-button { cursor: pointer; background: var(--fg); color: var(--bg); @@ -123,19 +124,19 @@ button, .file-button { } .file-button { - width: 58px; - font-size: 8pt; - float: right; - margin: 0; - background: transparent; - color: var(--fg); + width: 58px; + font-size: 8pt; + float: right; + margin: 0; + background: transparent; + color: var(--fg); } #blob { - visibility: hidden; - height: 0; - padding: 0; - margin: 0; + visibility: hidden; + height: 0; + padding: 0; + margin: 0; } section.post-preview { @@ -174,10 +175,9 @@ section > footer > div > form > button { section > footer > div > a:hover, section > footer > div > form > button:hover { - text-decoration: underline; + text-decoration: underline; } - section > footer > div > form > button { display: inline-block; border: 0; @@ -360,7 +360,7 @@ nav > ul > li > a:hover { } nav > ul > li > a.current { - font-weight: bold; + font-weight: bold; } section { @@ -375,9 +375,9 @@ section { .indent section, .thread-container section { - margin: unset; - border-radius: unset; - border-bottom: var(--fg-alt) solid 1px; + margin: unset; + border-radius: unset; + border-bottom: var(--fg-alt) solid 1px; } .indent details[open] { @@ -386,7 +386,7 @@ section { .indent section:last-of-type, .thread-container section:last-of-type { - border-bottom: unset; + border-bottom: unset; } .mentions-container { @@ -453,7 +453,7 @@ section > header { } .author-action > a { - text-decoration: underline; + text-decoration: underline; } section > header > div { @@ -522,7 +522,7 @@ section > footer > div > * { } section > footer > div > form > button:first-of-type { - font-size: 100%; + font-size: 100%; } section > footer > div > form > button.liked { diff --git a/src/index.js b/src/index.js index 194cff5..81922e4 100755 --- a/src/index.js +++ b/src/index.js @@ -146,60 +146,58 @@ const { about, blob, friend, meta, post, vote } = require("./models")({ // enhance the users' input text by expanding @name to [@name](@feedPub.key) // and slurps up blob uploads and appends a markdown link for it to the text (see handleBlobUpload) -const preparePreview = async function(ctx) { +const preparePreview = async function (ctx) { let text = String(ctx.request.body.text); // find all the @mentions that are not inside a link already // stores name:[matches...] // TODO: sort by relationship - // TODO: filter duplicates - const mentions = {} - + const mentions = {}; // This matches for @string followed by a space or other punctuations like ! , or . // The idea here is to match a plain @name but not [@name](...) // also: re.exec is stateful => regex is consumed and thus needs to be re-instantiated for each call - const rex = /(?!\[)@([a-zA-Z0-9-]+)([\s\.,!?)~]{1}|$)/g + const rex = /(?!\[)@([a-zA-Z0-9-]+)([\s\.,!?)~]{1}|$)/g; // ^ sentence ^ // delimiters // find @mentions using rex and use about.named() to get the info for them - let m + let m; while ((m = rex.exec(text)) !== null) { - const [match, name] = m; - let matches = about.named(name) + const name = m[1]; + let matches = about.named(name); for (const feed of matches) { - let found = mentions[name] || [] - found.push(feed) - mentions[name] = found + let found = mentions[name] || []; + found.push(feed); + mentions[name] = found; } } // filter the matches depending on the follow relation - Object.keys(mentions).forEach(name => { - let matches = mentions[name] + Object.keys(mentions).forEach((name) => { + let matches = mentions[name]; // if we find mention matches for a name, and we follow them / they follow us, // then use those matches as suggestions - const meaningfulMatches = matches.filter(m => { + const meaningfulMatches = matches.filter((m) => { return (m.rel.followsMe || m.rel.following) && m.rel.blocking === false; - }) + }); if (meaningfulMatches.length > 0) { - matches = meaningfulMatches + matches = meaningfulMatches; } - mentions[name] = matches - }) + mentions[name] = matches; + }); // replace the text with a markdown link if we have unambiguous match const replacer = (match, name, sign) => { - let matches = mentions[name] + let matches = mentions[name]; if (matches && matches.length === 1) { // we found an exact match, don't send it to frontend as a suggestion - delete mentions[name] + delete mentions[name]; // format markdown link and put the correct sign back at the end - return `[@${matches[0].name}](${matches[0].feed})${sign ? sign : ''}` + return `[@${matches[0].name}](${matches[0].feed})${sign ? sign : ""}`; } - return match - } + return match; + }; text = text.replace(rex, replacer); // add blob new blob to the end of the document. @@ -211,10 +209,10 @@ const preparePreview = async function(ctx) { id: ssb.id, name: await about.name(ssb.id), image: await about.image(ssb.id), - } + }; - return { authorMeta, text , mentions} -} + return { authorMeta, text, mentions }; +}; // handleBlobUpload ingests an uploaded form file. // it takes care of maximum blob size (5meg), exif stripping and mime detection. @@ -242,15 +240,16 @@ const handleBlobUpload = async function (ctx) { // implementation borrowed from ssb-blob-files // (which operates on a slightly different data structure, sadly) // https://github.com/ssbc/ssb-blob-files/blob/master/async/image-process.js - data = Buffer.from(removeExif(dataString), "binary") + data = Buffer.from(removeExif(dataString), "binary"); - function removeExif (fileData) { + function removeExif(fileData) { const exifOrientation = exif.load(fileData); - const orientation = exifOrientation['0th'][exif.ImageIFD.Orientation]; - const clean = exif.remove(fileData); - if (orientation !== undefined) { // preserve img orientation - const exifData = { '0th': {} } - exifData['0th'][exif.ImageIFD.Orientation] = orientation; + const orientation = exifOrientation["0th"][exif.ImageIFD.Orientation]; + const clean = exif.remove(fileData); + if (orientation !== undefined) { + // preserve img orientation + const exifData = { "0th": {} }; + exifData["0th"][exif.ImageIFD.Orientation] = orientation; const exifStr = exif.dump(exifData); return exif.insert(exifStr, clean); } else { @@ -258,8 +257,10 @@ const handleBlobUpload = async function (ctx) { } } } catch (e) { - console.warn(e) - console.warn("blob was likely not a jpeg -- no exif data to remove. proceeding with blob upload"); + console.warn(e); + console.warn( + "blob was likely not a jpeg -- no exif data to remove. proceeding with blob upload" + ); } const addBlob = new Promise((resolve, reject) => { @@ -267,43 +268,42 @@ const handleBlobUpload = async function (ctx) { pull.values([data]), ssb.blobs.add((err, hashedBlobRef) => { if (err) return reject(err); - console.log("added", hashedBlobRef) - resolve(hashedBlobRef) + resolve(hashedBlobRef); }) - ) - }) + ); + }); blob = { id: await addBlob, - name: blobUpload.name - } - const FileType = require('file-type'); + name: blobUpload.name, + }; + const FileType = require("file-type"); try { - let ftype = await FileType.fromBuffer(data) - blob.mime = ftype.mime + let ftype = await FileType.fromBuffer(data); + blob.mime = ftype.mime; } catch (error) { - console.warn(error) - blob.mime = "application/octet-stream" + console.warn(error); + blob.mime = "application/octet-stream"; } } } // append uploaded blob as markdown to the end of the input text if (typeof blob !== "boolean") { if (blob.mime.startsWith("image/")) { - text += `\n![${blob.name}](${blob.id})` + text += `\n![${blob.name}](${blob.id})`; } else if (blob.mime.startsWith("audio/")) { - text += `\n![audio:${blob.name}](${blob.id})` + text += `\n![audio:${blob.name}](${blob.id})`; } else if (blob.mime.startsWith("video/")) { - text += `\n![video:${blob.name}](${blob.id})` + text += `\n![video:${blob.name}](${blob.id})`; } else { - text += `\n[${blob.name}](${blob.id})` + text += `\n[${blob.name}](${blob.id})`; } } - return text -} + return text; +}; const resolveCommentComponents = async function (ctx) { const { message } = ctx.params; - const parentId = message + const parentId = message; const parentMessage = await post.get(parentId); const myFeedId = await meta.myFeedId(); @@ -316,22 +316,21 @@ const resolveCommentComponents = async function (ctx) { const rootMessage = hasRoot ? hasFork - ? parentMessage - : await post.get(parentMessage.value.content.root) + ? parentMessage + : await post.get(parentMessage.value.content.root) : parentMessage; const messages = await post.topicComments(rootMessage.key); messages.push(rootMessage); - let contentWarning + let contentWarning; if (ctx.request.body) { const rawContentWarning = String(ctx.request.body.contentWarning).trim(); contentWarning = rawContentWarning.length > 0 ? rawContentWarning : undefined; } - return { messages, myFeedId, parentMessage, contentWarning } -} - + return { messages, myFeedId, parentMessage, contentWarning }; +}; const { authorView, @@ -779,24 +778,37 @@ router ctx.body = await publishView(); }) .get("/comment/:message", async (ctx) => { - const { messages, myFeedId, parentMessage } = await resolveCommentComponents(ctx) - ctx.body = await commentView({ messages, myFeedId, parentMessage }) + const { + messages, + myFeedId, + parentMessage, + } = await resolveCommentComponents(ctx); + ctx.body = await commentView({ messages, myFeedId, parentMessage }); }) - .post("/subtopic/preview/:message", koaBody({ multipart: true }), async (ctx) => { - const { message } = ctx.params; - const rootMessage = await post.get(message); - const myFeedId = await meta.myFeedId(); + .post( + "/subtopic/preview/:message", + koaBody({ multipart: true }), + async (ctx) => { + const { message } = ctx.params; + const rootMessage = await post.get(message); + const myFeedId = await meta.myFeedId(); - const rawContentWarning = String(ctx.request.body.contentWarning).trim(); - const contentWarning = - rawContentWarning.length > 0 ? rawContentWarning : undefined; + const rawContentWarning = String(ctx.request.body.contentWarning).trim(); + const contentWarning = + rawContentWarning.length > 0 ? rawContentWarning : undefined; - const messages = [rootMessage]; + const messages = [rootMessage]; - const previewData = await preparePreview(ctx); + const previewData = await preparePreview(ctx); - ctx.body = await previewSubtopicView({ messages, myFeedId, previewData, contentWarning }); - }) + ctx.body = await previewSubtopicView({ + messages, + myFeedId, + previewData, + contentWarning, + }); + } + ) .post("/subtopic/:message", koaBody(), async (ctx) => { const { message } = ctx.params; const text = String(ctx.request.body.text); @@ -818,13 +830,28 @@ router ctx.body = await publishSubtopic({ message, text }); ctx.redirect(`/thread/${encodeURIComponent(message)}`); }) - .post("/comment/preview/:message", koaBody({ multipart: true }), async (ctx) => { - const { messages, contentWarning, myFeedId, parentMessage } = await resolveCommentComponents(ctx) - - const previewData = await preparePreview(ctx); + .post( + "/comment/preview/:message", + koaBody({ multipart: true }), + async (ctx) => { + const { + messages, + contentWarning, + myFeedId, + parentMessage, + } = await resolveCommentComponents(ctx); - ctx.body = await previewCommentView({ messages, myFeedId, contentWarning, parentMessage, previewData }); - }) + const previewData = await preparePreview(ctx); + + ctx.body = await previewCommentView({ + messages, + myFeedId, + contentWarning, + parentMessage, + previewData, + }); + } + ) .post("/comment/:message", koaBody(), async (ctx) => { const { message } = ctx.params; const text = String(ctx.request.body.text); @@ -854,7 +881,7 @@ router rawContentWarning.length > 0 ? rawContentWarning : undefined; const previewData = await preparePreview(ctx); - ctx.body = await previewView({previewData, contentWarning}); + ctx.body = await previewView({ previewData, contentWarning }); }) .post("/publish/", koaBody(), async (ctx) => { const text = String(ctx.request.body.text); diff --git a/src/models.js b/src/models.js index 101f3d7..d2471a7 100644 --- a/src/models.js +++ b/src/models.js @@ -121,114 +121,127 @@ module.exports = ({ cooler, isPublic }) => { // TODO: an alternative would be using ssb.names if available and just loading this as a fallback // Two lookup tables to remove old and duplicate names - const feeds_to_name = {} - let all_the_names = {} + const feeds_to_name = {}; + let all_the_names = {}; - let dirty = false // just stop mindless work (nothing changed) could be smarter thou - let running = false // don't run twice + let dirty = false; // just stop mindless work (nothing changed) could be smarter thou + let running = false; // don't run twice // transposeLookupTable flips the lookup around (form feed->name to name->feed) // and also enhances the entries with image and relationship info const transposeLookupTable = () => { - if (!dirty) return - if (running) return - running = true + if (!dirty) return; + if (running) return; + running = true; // invalidate old cache // regenerate a new thing because we don't know which entries will be gone - all_the_names = {} + all_the_names = {}; - const allFeeds = Object.keys(feeds_to_name) - console.log(`updating ${allFeeds.length} feeds`) - console.time('transpose-name-index') + const allFeeds = Object.keys(feeds_to_name); + console.log(`updating ${allFeeds.length} feeds`); + console.time("transpose-name-index"); - const lookups = [] + const lookups = []; for (const feed of allFeeds) { - const e = feeds_to_name[feed] - let pair = { feed, name: e.name } - lookups.push(enhanceFeedInfo(pair)) + const e = feeds_to_name[feed]; + let pair = { feed, name: e.name }; + lookups.push(enhanceFeedInfo(pair)); } // wait for all image and follow lookups - Promise.all(lookups).then(() => { - dirty = false // all updated - running = false - console.timeEnd('transpose-name-index') - }).catch((err) => { - running = false - console.warn('lookup transposition failed:', err) - }) - } + Promise.all(lookups) + .then(() => { + dirty = false; // all updated + running = false; + console.timeEnd("transpose-name-index"); + }) + .catch((err) => { + running = false; + console.warn("lookup transposition failed:", err); + }); + }; // this function adds the avater image and relationship to the all_the_names lookup table - const enhanceFeedInfo = ({feed, name}) => { + const enhanceFeedInfo = ({ feed, name }) => { return new Promise((resolve, reject) => { - getAbout({feedId: feed, key: "image"}).then((img) => { - if (img !== null && typeof img !== "string" && typeof img === "object" && typeof img.link === "string") { - img = img.link - } else if (img === null) { - img = nullImage // default empty image if we dont have one - } + getAbout({ feedId: feed, key: "image" }) + .then((img) => { + if ( + img !== null && + typeof img !== "string" && + typeof img === "object" && + typeof img.link === "string" + ) { + img = img.link; + } else if (img === null) { + img = nullImage; // default empty image if we dont have one + } - models.friend.getRelationship(feed).then((rel) => { - // append and update lookup table - let feeds_named = all_the_names[name] || [] - feeds_named.push({feed, name, rel, img }) - all_the_names[name.toLowerCase()] = feeds_named - resolve() + models.friend + .getRelationship(feed) + .then((rel) => { + // append and update lookup table + let feeds_named = all_the_names[name] || []; + feeds_named.push({ feed, name, rel, img }); + all_the_names[name.toLowerCase()] = feeds_named; + resolve(); - // TODO: append if these fail!? - }).catch(reject) - }).catch(reject) - }) - } + // TODO: append if these fail!? + }) + .catch(reject); + }) + .catch(reject); + }); + }; cooler.open().then((ssb) => { - - console.time('about-name-warmup') // benchmark the time it takes to stream all existing abouts + console.time("about-name-warmup"); // benchmark the time it takes to stream all existing abouts pull( ssb.query.read({ live: true, // keep streaming new messages as they arrive query: [ { - $filter: { // all messages of type:about that have a name field that is typeof string + $filter: { + // all messages of type:about that have a name field that is typeof string value: { content: { type: "about", - name: { $is: "string" } + name: { $is: "string" }, }, }, - } - } - ] + }, + }, + ], }), pull.filter((msg) => { // backlog of data is done, only new values from now on if (msg.sync && msg.sync === true) { - console.timeEnd('about-name-warmup') - transposeLookupTable() // fire once now - setInterval(transposeLookupTable, 1000*60) // and then every 60 seconds - return false + console.timeEnd("about-name-warmup"); + transposeLookupTable(); // fire once now + setInterval(transposeLookupTable, 1000 * 60); // and then every 60 seconds + return false; } // only pick messages about self - return msg.value.author == msg.value.content.about + return msg.value.author == msg.value.content.about; }), pull.drain((msg) => { - const name = msg.value.content.name - const ts = msg.value.timestamp - const feed = msg.value.author + const name = msg.value.content.name; + const ts = msg.value.timestamp; + const feed = msg.value.author; - const newEntry = { name, ts } - const currentEntry = feeds_to_name[feed] - if (typeof currentEntry == 'undefined') { - dirty = true - feeds_to_name[feed] = newEntry - } else if (currentEntry.ts < ts) { // overwrite entry if it's newer - dirty = true - feeds_to_name[feed] = newEntry + const newEntry = { name, ts }; + const currentEntry = feeds_to_name[feed]; + if (typeof currentEntry == "undefined") { + dirty = true; + feeds_to_name[feed] = newEntry; + } else if (currentEntry.ts < ts) { + // overwrite entry if it's newer + dirty = true; + feeds_to_name[feed] = newEntry; } }) - ) + ); }); models.about = { @@ -253,14 +266,14 @@ module.exports = ({ cooler, isPublic }) => { ); // First 8 chars of public key }, named: (name) => { - let found = [] - let matched = Object.keys(all_the_names).filter(n => { - return n.startsWith(name.toLowerCase()) - }) + let found = []; + let matched = Object.keys(all_the_names).filter((n) => { + return n.startsWith(name.toLowerCase()); + }); for (const m of matched) { - found = found.concat(all_the_names[m]) + found = found.concat(all_the_names[m]); } - return found + return found; }, image: async (feedId) => { if (isPublic && (await models.about.publicWebHosting(feedId)) === false) { @@ -321,13 +334,15 @@ module.exports = ({ cooler, isPublic }) => { }, want: async ({ blobId }) => { debug("want blob: %s", blobId); - cooler.open().then(ssb => { - - // This does not wait for the blob. - ssb.blobs.want(blobId); - }).catch(err => { - console.warn(`failed to want blob:${blobId}: ${err}`) - }) + cooler + .open() + .then((ssb) => { + // This does not wait for the blob. + ssb.blobs.want(blobId); + }) + .catch((err) => { + console.warn(`failed to want blob:${blobId}: ${err}`); + }); }, search: async ({ query }) => { debug("blob search: %s", query); @@ -367,7 +382,7 @@ module.exports = ({ cooler, isPublic }) => { following, blocking, }; - transposeLookupTable() // invalidate @mentions table + transposeLookupTable(); // invalidate @mentions table return ssb.publish(content); }, follow: (feedId) => diff --git a/src/views/index.js b/src/views/index.js index 7811bb1..4a9554c 100644 --- a/src/views/index.js +++ b/src/views/index.js @@ -74,8 +74,15 @@ const nbsp = "\xa0"; * @param {{href: string, emoji: string, text: string }} input */ const template = (titlePrefix, ...elements) => { - const navLink = ({ href, emoji, text }, prefix) => - li(a({ href, class: titlePrefix === text ? "current" : "" }, span({ class: "emoji" }, emoji), nbsp, text)); + const navLink = ({ href, emoji, text }) => + li( + a( + { href, class: titlePrefix === text ? "current" : "" }, + span({ class: "emoji" }, emoji), + nbsp, + text + ) + ); const nodes = html( { lang: "en" }, @@ -201,9 +208,9 @@ const thread = (messages) => { const nextAuthor = lodash.get(nextMsg, "value.meta.author.name"); const nextSnippet = postSnippet( - lodash.has(nextMsg, "value.content.contentWarning") ? - lodash.get(nextMsg, "value.content.contentWarning") : - lodash.get(nextMsg, "value.content.text") + lodash.has(nextMsg, "value.content.contentWarning") + ? lodash.get(nextMsg, "value.content.contentWarning") + : lodash.get(nextMsg, "value.content.text") ); msgList.push(summary(`${nextAuthor}: ${nextSnippet}`).outerHTML); @@ -222,7 +229,10 @@ const thread = (messages) => { } const htmlStrings = lodash.flatten(msgList); - return div({}, { class: "thread-container", innerHTML: htmlStrings.join("") }); + return div( + {}, + { class: "thread-container", innerHTML: htmlStrings.join("") } + ); }; const postSnippet = (text) => { @@ -656,14 +666,34 @@ exports.authorView = ({ return template(i18n.profile, prefix, items); }; -exports.previewCommentView = async ({ previewData, messages, myFeedId, parentMessage, contentWarning }) => { +exports.previewCommentView = async ({ + previewData, + messages, + myFeedId, + parentMessage, + contentWarning, +}) => { const publishAction = `/comment/${encodeURIComponent(messages[0].key)}`; - const preview = generatePreview({ previewData, contentWarning, action: publishAction }) - return exports.commentView({ messages, myFeedId, parentMessage }, preview, previewData.text, contentWarning) + const preview = generatePreview({ + previewData, + contentWarning, + action: publishAction, + }); + return exports.commentView( + { messages, myFeedId, parentMessage }, + preview, + previewData.text, + contentWarning + ); }; -exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, text, contentWarning) => { +exports.commentView = async ( + { messages, myFeedId, parentMessage }, + preview, + text, + contentWarning +) => { let markdownMention; const messageElements = await Promise.all( @@ -692,10 +722,8 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex return template( i18n.commentTitle({ authorName }), - div({ class: "thread-container" }, - messageElements, - ), - preview !== undefined ? preview : '', + div({ class: "thread-container" }, messageElements), + preview !== undefined ? preview : "", p( ...i18n.commentLabel({ publicOrPrivate, markdownUrl }), ...maybeSubtopicText @@ -708,8 +736,7 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex required: true, name: "text", }, - text ? text : - isPrivate ? null : markdownMention + text ? text : isPrivate ? null : markdownMention ), label( i18n.contentWarningLabel, @@ -717,12 +744,12 @@ exports.commentView = async ({ messages, myFeedId, parentMessage }, preview, tex name: "contentWarning", type: "text", class: "contentWarning", - value: contentWarning ? contentWarning : '', + value: contentWarning ? contentWarning : "", placeholder: i18n.contentWarningPlaceholder, }) ), - button({ type: "submit" }, i18n.preview), - label({ class: "file-button", for: "blob"}, i18n.attachFiles), + button({ type: "submit" }, i18n.preview), + label({ class: "file-button", for: "blob" }, i18n.attachFiles), input({ type: "file", id: "blob", name: "blob" }) ) ); @@ -805,14 +832,14 @@ exports.publishView = (preview, text, contentWarning) => { section( h1(i18n.publish), form( - { + { action: "/publish/preview", method: "post", enctype: "multipart/form-data", }, label( i18n.publishLabel({ markdownUrl, linkTarget: "_blank" }), - textarea({ required: true, name: "text" }, text ? text : '') + textarea({ required: true, name: "text" }, text ? text : "") ), label( i18n.contentWarningLabel, @@ -820,24 +847,24 @@ exports.publishView = (preview, text, contentWarning) => { name: "contentWarning", type: "text", class: "contentWarning", - value: contentWarning ? contentWarning : '', + value: contentWarning ? contentWarning : "", placeholder: i18n.contentWarningPlaceholder, }) ), button({ type: "submit" }, i18n.preview), - label({ class: "file-button", for: "blob"}, i18n.attachFiles), + label({ class: "file-button", for: "blob" }, i18n.attachFiles), input({ type: "file", id: "blob", name: "blob" }) ) ), - preview ? preview : '', + preview ? preview : "", p(i18n.publishCustomInfo({ href: "/publish/custom" })) ); }; const generatePreview = ({ previewData, contentWarning, action }) => { - const { authorMeta, text, mentions } = previewData + const { authorMeta, text, mentions } = previewData; - // craft message that looks like it came from the db + // craft message that looks like it came from the db // cb: this kinda fragile imo? this is for getting a proper post styling ya? const msg = { key: "%non-existant.preview", @@ -845,8 +872,8 @@ const generatePreview = ({ previewData, contentWarning, action }) => { author: authorMeta.id, // sequence: -1, content: { - type:"post", - text: text + type: "post", + text: text, }, timestamp: Date.now(), meta: { @@ -855,65 +882,82 @@ const generatePreview = ({ previewData, contentWarning, action }) => { author: { name: authorMeta.name, avatar: { - url: `/image/64/${encodeURIComponent(authorMeta.image)}` - } + url: `/image/64/${encodeURIComponent(authorMeta.image)}`, + }, }, - } - } - } - if (contentWarning) msg.value.content.contentWarning = contentWarning + }, + }, + }; + if (contentWarning) msg.value.content.contentWarning = contentWarning; const ts = new Date(msg.value.timestamp); lodash.set(msg, "value.meta.timestamp.received.iso8601", ts.toISOString()); const ago = Date.now() - Number(ts); const prettyAgo = prettyMs(ago, { compact: true }); lodash.set(msg, "value.meta.timestamp.received.since", prettyAgo); return div( - Object.keys(mentions).length === 0 ? "" : - section({ class: "mention-suggestions"}, - h2(i18n.mentionsMatching), - Object.keys(mentions).map((name) => { - let matches = mentions[name] + Object.keys(mentions).length === 0 + ? "" + : section( + { class: "mention-suggestions" }, + h2(i18n.mentionsMatching), + Object.keys(mentions).map((name) => { + let matches = mentions[name]; - return div( - matches.map(m => { - let relationship = { emoji: "", desc: "" } - if (m.rel.followsMe && m.rel.following) { - // mutuals get the handshake emoji - relationship.emoji = "🤝" - relationship.desc = i18n.relationshipMutuals - } else if (m.rel.following) { - // if we're following that's an eyes emoji - relationship.emoji = "👀" - relationship.desc = i18n.relationshipFollowing - } else if (m.rel.followsMe) { - // follower has waving-hand emoji - relationship.emoji = "👋" - relationship.desc = i18n.relationshipTheyFollow - } else { - // no relationship has question mark emoji - relationship.emoji = "❓" - relationship.desc = i18n.relationshipNotFollowing - } - return div({ class: "mentions-container" }, - a({ class: "mentions-image", - href: `/author/${encodeURIComponent(m.feed)}` }, - img({ src: `/image/64/${encodeURIComponent(m.img)}`}) - ), - a( - { class: "mentions-name", href: `/author/${encodeURIComponent(m.feed)}` }, - m.name - ), - div({ class: "emo-rel" }, - span({ class: "emoji", title: relationship.desc }, relationship.emoji), - span({ class: "mentions-listing" }, `[@${m.name}](${m.feed})`) - ) - ) + return div( + matches.map((m) => { + let relationship = { emoji: "", desc: "" }; + if (m.rel.followsMe && m.rel.following) { + // mutuals get the handshake emoji + relationship.emoji = "🤝"; + relationship.desc = i18n.relationshipMutuals; + } else if (m.rel.following) { + // if we're following that's an eyes emoji + relationship.emoji = "👀"; + relationship.desc = i18n.relationshipFollowing; + } else if (m.rel.followsMe) { + // follower has waving-hand emoji + relationship.emoji = "👋"; + relationship.desc = i18n.relationshipTheyFollow; + } else { + // no relationship has question mark emoji + relationship.emoji = "❓"; + relationship.desc = i18n.relationshipNotFollowing; + } + return div( + { class: "mentions-container" }, + a( + { + class: "mentions-image", + href: `/author/${encodeURIComponent(m.feed)}`, + }, + img({ src: `/image/64/${encodeURIComponent(m.img)}` }) + ), + a( + { + class: "mentions-name", + href: `/author/${encodeURIComponent(m.feed)}`, + }, + m.name + ), + div( + { class: "emo-rel" }, + span( + { class: "emoji", title: relationship.desc }, + relationship.emoji + ), + span( + { class: "mentions-listing" }, + `[@${m.name}](${m.feed})` + ) + ) + ); + }) + ); }) - ) - }) - ), - section({ class: "post-preview" }, - post({msg}), + ), + section( + { class: "post-preview" }, + post({ msg }), // doesn't need blobs, preview adds them to the text form( @@ -922,24 +966,28 @@ const generatePreview = ({ previewData, contentWarning, action }) => { name: "contentWarning", type: "hidden", value: contentWarning, - }), - input({ - name: "text", - type: "hidden", - value: text, }), - button({ type: "submit" }, i18n.publish), - ), + input({ + name: "text", + type: "hidden", + value: text, + }), + button({ type: "submit" }, i18n.publish) + ) ) - ) -} + ); +}; exports.previewView = ({ previewData, contentWarning }) => { const publishAction = "/publish"; - const preview = generatePreview({ previewData, contentWarning, action: publishAction }) - return exports.publishView(preview, previewData.text, contentWarning) -} + const preview = generatePreview({ + previewData, + contentWarning, + action: publishAction, + }); + return exports.publishView(preview, previewData.text, contentWarning); +}; /** * @param {{status: object, peers: any[], theme: string, themeNames: string[], version: string }} input @@ -1173,14 +1221,33 @@ exports.threadsView = ({ messages }) => { }); }; -exports.previewSubtopicView = async ({ previewData, messages, myFeedId, contentWarning }) => { +exports.previewSubtopicView = async ({ + previewData, + messages, + myFeedId, + contentWarning, +}) => { const publishAction = `/subtopic/${encodeURIComponent(messages[0].key)}`; - const preview = generatePreview({ previewData, contentWarning, action: publishAction }) - return exports.subtopicView({ messages, myFeedId }, preview, previewData.text, contentWarning) + const preview = generatePreview({ + previewData, + contentWarning, + action: publishAction, + }); + return exports.subtopicView( + { messages, myFeedId }, + preview, + previewData.text, + contentWarning + ); }; -exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarning) => { +exports.subtopicView = async ( + { messages, myFeedId }, + preview, + text, + contentWarning +) => { const subtopicForm = `/subtopic/preview/${encodeURIComponent( messages[messages.length - 1].key )}`; @@ -1206,10 +1273,8 @@ exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarn return template( i18n.subtopicTitle({ authorName }), - div({ class: "thread-container" }, - messageElements, - ), - preview !== undefined ? preview : '', + div({ class: "thread-container" }, messageElements), + preview !== undefined ? preview : "", p(i18n.subtopicLabel({ markdownUrl })), form( { action: subtopicForm, method: "post", enctype: "multipart/form-data" }, @@ -1227,12 +1292,12 @@ exports.subtopicView = async ({ messages, myFeedId }, preview, text, contentWarn name: "contentWarning", type: "text", class: "contentWarning", - value: contentWarning ? contentWarning : '', + value: contentWarning ? contentWarning : "", placeholder: i18n.contentWarningPlaceholder, }) ), - button({ type: "submit" }, i18n.preview), - label({ class: "file-button", for: "blob"}, i18n.attachFiles), + button({ type: "submit" }, i18n.preview), + label({ class: "file-button", for: "blob" }, i18n.attachFiles), input({ type: "file", id: "blob", name: "blob" }) ) ); diff --git a/test/basic.js b/test/basic.js index ef50631..56d547f 100644 --- a/test/basic.js +++ b/test/basic.js @@ -31,38 +31,41 @@ const paths = [ tap.setTimeout(0); tap.test("DNS rebind attack fails", (t) => { - t.plan(1); supertest(app) .get("/inbox") .set("Host", "example.com") .expect(400) - .end(t.error); + .end((err) => { + t.equal(err, null); + t.end(); + }); }); tap.test("CSRF attack should fail with no referer", (t) => { - t.plan(1); - supertest(app).post("/conn/settings/stop").expect(400).end(t.error); + supertest(app).post("/conn/settings/stop").expect(400).end(t.end); }); tap.test("CSRF attack should fail with wrong referer", (t) => { - t.plan(1); supertest(app) .post("/conn/settings/stop") .set("Host", "example.com") .expect(400) - .end(t.error); + .end((err) => { + t.equal(err, null); + t.end(); + }); }); paths.forEach((path) => { tap.test(path, (t) => { - t.plan(1); supertest(app) .get(path) .set("Host", "localhost") .expect(200) .end((err) => { - console.log(path); - t.error(err); + t.equal(err, null); + console.log("done:" + path); + t.end(); }); }); });