2020-01-22 00:22:19 +00:00
"use strict" ;
2020-01-08 20:56:49 +00:00
2020-01-22 00:22:19 +00:00
const debug = require ( "debug" ) ( "oasis" ) ;
const highlightJs = require ( "highlight.js" ) ;
2020-01-08 20:56:49 +00:00
2020-03-04 00:13:56 +00:00
const MarkdownIt = require ( "markdown-it" ) ;
2020-01-08 20:56:49 +00:00
const {
a ,
article ,
2020-03-24 00:49:03 +00:00
br ,
2020-02-01 21:20:22 +00:00
body ,
2020-01-08 20:56:49 +00:00
button ,
2020-01-31 22:39:18 +00:00
details ,
2020-01-08 20:56:49 +00:00
div ,
2020-02-04 21:27:41 +00:00
em ,
2020-01-08 20:56:49 +00:00
footer ,
form ,
h1 ,
h2 ,
2020-02-01 21:20:22 +00:00
head ,
2020-01-08 20:56:49 +00:00
header ,
2020-02-01 21:20:22 +00:00
html ,
2020-01-08 20:56:49 +00:00
img ,
input ,
label ,
li ,
2020-02-01 21:20:22 +00:00
link ,
main ,
meta ,
nav ,
2020-01-08 20:56:49 +00:00
option ,
p ,
pre ,
progress ,
section ,
select ,
span ,
2020-01-31 22:39:18 +00:00
summary ,
2020-01-08 20:56:49 +00:00
textarea ,
2020-02-01 21:20:22 +00:00
title ,
2020-03-23 22:54:28 +00:00
ul ,
2020-01-22 00:22:19 +00:00
} = require ( "hyperaxe" ) ;
2020-01-08 20:56:49 +00:00
2020-02-01 22:08:37 +00:00
const lodash = require ( "lodash" ) ;
2020-01-31 22:39:18 +00:00
const markdown = require ( "./markdown" ) ;
2020-03-04 00:13:56 +00:00
const md = new MarkdownIt ( ) ;
2020-02-01 22:08:37 +00:00
const i18nBase = require ( "./i18n" ) ;
2020-03-24 16:21:59 +00:00
let selectedLanguage = "en" ;
let i18n = i18nBase [ selectedLanguage ] ;
2020-02-01 22:08:37 +00:00
2020-03-23 22:54:28 +00:00
exports . setLanguage = ( language ) => {
2020-02-02 17:31:43 +00:00
selectedLanguage = language ;
2020-02-01 22:08:37 +00:00
i18n = Object . assign ( { } , i18nBase . en , i18nBase [ language ] ) ;
} ;
2020-02-01 21:20:22 +00:00
const markdownUrl = "https://commonmark.org/help/" ;
const doctypeString = "<!DOCTYPE html>" ;
2020-02-16 19:14:23 +00:00
const THREAD _PREVIEW _LENGTH = 3 ;
2020-03-23 22:54:28 +00:00
const toAttributes = ( obj ) =>
2020-02-01 21:20:22 +00:00
Object . entries ( obj )
. map ( ( [ key , val ] ) => ` ${ key } = ${ val } ` )
. join ( ", " ) ;
2020-02-05 22:33:44 +00:00
// non-breaking space
const nbsp = "\xa0" ;
2020-03-27 15:21:40 +00:00
/ * *
* @ param { { href : string , emoji : string , text : string } } input
* /
2020-02-05 22:33:44 +00:00
const navLink = ( { href , emoji , text } ) =>
li ( a ( { href } , span ( { class : "emoji" } , emoji ) , nbsp , text ) ) ;
2020-02-01 21:20:22 +00:00
const template = ( ... elements ) => {
const nodes = html (
{ lang : "en" } ,
head (
title ( "Oasis" ) ,
link ( { rel : "stylesheet" , href : "/theme.css" } ) ,
link ( { rel : "stylesheet" , href : "/assets/style.css" } ) ,
link ( { rel : "stylesheet" , href : "/assets/highlight.css" } ) ,
link ( { rel : "icon" , type : "image/svg+xml" , href : "/assets/favicon.svg" } ) ,
meta ( { charset : "utf-8" } ) ,
meta ( {
name : "description" ,
2020-03-23 22:54:28 +00:00
content : i18n . oasisDescription ,
2020-02-01 21:20:22 +00:00
} ) ,
meta ( {
name : "viewport" ,
2020-03-23 22:54:28 +00:00
content : toAttributes ( { width : "device-width" , "initial-scale" : 1 } ) ,
2020-02-01 21:20:22 +00:00
} )
) ,
body (
nav (
ul (
2020-02-06 19:44:37 +00:00
navLink ( {
href : "/publish" ,
emoji : "📝" ,
2020-03-23 22:54:28 +00:00
text : i18n . publish ,
2020-02-06 19:44:37 +00:00
} ) ,
2020-02-05 22:33:44 +00:00
navLink ( {
href : "/public/latest/extended" ,
emoji : "🗺️" ,
2020-03-23 22:54:28 +00:00
text : i18n . extended ,
2020-02-05 22:33:44 +00:00
} ) ,
2020-03-11 10:30:49 +00:00
navLink ( {
href : "/public/popular/day" ,
emoji : "📣" ,
2020-03-23 22:54:28 +00:00
text : i18n . popular ,
2020-03-11 10:30:49 +00:00
} ) ,
2020-02-05 22:33:44 +00:00
navLink ( { href : "/public/latest" , emoji : "🐇" , text : i18n . latest } ) ,
navLink ( {
href : "/public/latest/topics" ,
emoji : "📖" ,
2020-03-23 22:54:28 +00:00
text : i18n . topics ,
2020-02-05 22:33:44 +00:00
} ) ,
2020-02-17 20:08:03 +00:00
navLink ( {
href : "/public/latest/summaries" ,
emoji : "🗒️" ,
2020-03-23 22:54:28 +00:00
text : i18n . summaries ,
2020-02-17 20:08:03 +00:00
} ) ,
2020-03-10 12:42:02 +00:00
navLink ( {
href : "/public/latest/threads" ,
emoji : "🧵" ,
2020-03-25 13:05:53 +00:00
text : i18n . threads ,
2020-02-17 20:08:03 +00:00
} ) ,
2020-02-05 22:33:44 +00:00
navLink ( { href : "/profile" , emoji : "🐱" , text : i18n . profile } ) ,
navLink ( { href : "/mentions" , emoji : "💬" , text : i18n . mentions } ) ,
navLink ( { href : "/inbox" , emoji : "✉️" , text : i18n . private } ) ,
navLink ( { href : "/search" , emoji : "🔍" , text : i18n . search } ) ,
2020-02-14 20:28:05 +00:00
navLink ( { href : "/settings" , emoji : "⚙" , text : i18n . settings } )
2020-02-01 21:20:22 +00:00
)
) ,
main ( { id : "content" } , elements )
)
) ;
2020-01-31 22:39:18 +00:00
2020-02-01 21:20:22 +00:00
const result = doctypeString + nodes . outerHTML ;
return result ;
} ;
2020-02-16 19:14:23 +00:00
2020-03-23 22:54:28 +00:00
const postInAside = ( msg ) => {
2020-02-16 19:14:23 +00:00
const encoded = {
key : encodeURIComponent ( msg . key ) ,
author : encodeURIComponent ( msg . value . author ) ,
2020-03-23 22:54:28 +00:00
parent : encodeURIComponent ( msg . value . content . root ) ,
2020-02-17 11:57:01 +00:00
} ;
2020-02-16 19:14:23 +00:00
const url = {
author : ` /author/ ${ encoded . author } ` ,
likeForm : ` /like/ ${ encoded . key } ` ,
link : ` /thread/ ${ encoded . parent } # ${ encoded . key } ` ,
parent : ` /thread/ ${ encoded . parent } # ${ encoded . parent } ` ,
avatar : msg . value . meta . author . avatar . url ,
json : ` /json/ ${ encoded . key } ` ,
reply : ` /reply/ ${ encoded . key } ` ,
2020-03-23 22:54:28 +00:00
comment : ` /comment/ ${ encoded . key } ` ,
2020-02-17 11:57:01 +00:00
} ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
const isPrivate = Boolean ( msg . value . meta . private ) ;
const isRoot = msg . value . content . root == null ;
const isFork = msg . value . meta . postType === "reply" ;
const hasContentWarning =
typeof msg . value . content . contentWarning === "string" ;
2020-02-16 19:14:23 +00:00
const isThreadTarget = Boolean (
lodash . get ( msg , "value.meta.thread.target" , false )
2020-02-17 11:57:01 +00:00
) ;
2020-02-16 19:14:23 +00:00
// TODO: I think this is actually true for both replies and comments.
2020-02-17 11:57:01 +00:00
const isReply = Boolean ( lodash . get ( msg , "value.meta.thread.reply" , false ) ) ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
const timeAgo = msg . value . meta . timestamp . received . since . replace ( "~" , "" ) ;
2020-02-16 19:14:23 +00:00
const markdownContent = markdown (
msg . value . content . text ,
msg . value . content . mentions
2020-02-17 11:57:01 +00:00
) ;
2020-02-16 19:14:23 +00:00
const likeButton = msg . value . meta . voted
? { value : 0 , class : "liked" }
2020-02-17 11:57:01 +00:00
: { value : 1 , class : null } ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
const likeCount = msg . value . meta . votes . length ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
const messageClasses = [ ] ;
2020-02-16 19:14:23 +00:00
if ( isPrivate ) {
2020-02-17 11:57:01 +00:00
messageClasses . push ( "private" ) ;
2020-02-16 19:14:23 +00:00
}
if ( isThreadTarget ) {
2020-02-17 11:57:01 +00:00
messageClasses . push ( "thread-target" ) ;
2020-02-16 19:14:23 +00:00
}
if ( isReply ) {
// True for comments too, I think
2020-02-17 11:57:01 +00:00
messageClasses . push ( "reply" ) ;
2020-02-16 19:14:23 +00:00
}
const postOptions = {
post : null ,
comment : i18n . commentDescription ( { parentUrl : url . parent } ) ,
reply : i18n . replyDescription ( { parentUrl : url . parent } ) ,
2020-03-23 22:54:28 +00:00
mystery : i18n . mysteryDescription ,
2020-02-17 11:57:01 +00:00
} ;
2020-02-16 19:14:23 +00:00
2020-03-23 22:54:28 +00:00
const isMarkdownEmpty = ( md ) => md === "<p>undefined</p>\n" ;
2020-02-16 19:14:23 +00:00
const articleElement = isMarkdownEmpty ( markdownContent )
? article (
{ class : "content" } ,
pre ( {
innerHTML : highlightJs . highlight ( "json" , JSON . stringify ( msg , null , 2 ) )
2020-03-23 22:54:28 +00:00
. value ,
2020-02-16 19:14:23 +00:00
} )
)
2020-02-17 11:57:01 +00:00
: article ( { class : "content" , innerHTML : markdownContent } ) ;
2020-02-16 19:14:23 +00:00
const articleContent = hasContentWarning
? details ( summary ( msg . value . content . contentWarning ) , articleElement )
2020-02-17 11:57:01 +00:00
: articleElement ;
2020-02-16 19:14:23 +00:00
return section (
{
2020-03-23 22:54:28 +00:00
class : messageClasses . join ( " " ) ,
2020-02-16 19:14:23 +00:00
} ,
header (
2020-03-24 00:49:03 +00:00
div (
span (
{ class : "author" } ,
a (
{ href : url . author } ,
img ( { class : "avatar" , src : url . avatar , alt : "" } ) ,
msg . value . meta . author . name
) ,
postOptions [ msg . value . meta . postType ]
2020-02-16 19:14:23 +00:00
) ,
2020-03-24 00:49:03 +00:00
span (
{ class : "time" } ,
isPrivate ? "🔒" : null ,
a ( { href : url . link } , nbsp , timeAgo )
)
2020-02-16 19:14:23 +00:00
)
) ,
articleContent ,
footer (
2020-03-24 00:49:03 +00:00
div (
form (
{ action : url . likeForm , method : "post" } ,
button (
{
name : "voteValue" ,
type : "submit" ,
value : likeButton . value ,
2020-03-24 23:57:22 +00:00
class : likeButton . class ,
2020-03-24 00:49:03 +00:00
} ,
` ❤ ${ likeCount } `
)
) ,
a ( { href : url . comment } , i18n . comment ) ,
isPrivate || isRoot || isFork
? null
: a ( { href : url . reply } , nbsp , i18n . reply ) ,
a ( { href : url . json } , nbsp , i18n . json )
2020-02-16 19:14:23 +00:00
) ,
2020-03-24 00:49:03 +00:00
br ( )
2020-02-16 19:14:23 +00:00
)
2020-02-17 11:57:01 +00:00
) ;
} ;
2020-02-16 19:14:23 +00:00
2020-03-25 20:31:23 +00:00
const thread = ( messages ) => {
2020-03-25 20:09:04 +00:00
// this first loop is preprocessing to enable auto-expansion of forks when a
// message in the fork is linked to
let lookingForTarget = true ;
let shallowest = Infinity ;
for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
const msg = messages [ i ] ;
const depth = lodash . get ( msg , "value.meta.thread.depth" , 0 ) ;
if ( lookingForTarget ) {
const isThreadTarget = Boolean (
lodash . get ( msg , "value.meta.thread.target" , false )
) ;
if ( isThreadTarget ) {
lookingForTarget = false ;
}
} else {
if ( depth < shallowest ) {
lodash . set ( msg , "value.meta.thread.ancestorOfTarget" , true ) ;
shallowest = depth ;
}
}
}
2020-03-22 21:22:35 +00:00
const msgList = [ ] ;
2020-03-22 20:36:26 +00:00
for ( let i = 0 ; i < messages . length ; i ++ ) {
const j = i + 1 ;
2020-03-06 15:33:31 +00:00
2020-03-22 20:36:26 +00:00
const currentMsg = messages [ i ] ;
const nextMsg = messages [ j ] ;
2020-03-06 15:33:31 +00:00
2020-03-25 20:31:23 +00:00
const depth = ( msg ) => {
2020-03-22 20:48:59 +00:00
// will be undefined when checking depth(nextMsg) when currentMsg is the
// last message in the thread
2020-03-22 20:36:26 +00:00
if ( msg === undefined ) return 0 ;
return lodash . get ( msg , "value.meta.thread.depth" , 0 ) ;
} ;
2020-03-06 15:33:31 +00:00
2020-03-22 21:22:35 +00:00
msgList . push ( post ( { msg : currentMsg } ) . outerHTML ) ;
2020-03-06 15:33:31 +00:00
2020-03-22 20:48:59 +00:00
if ( depth ( currentMsg ) < depth ( nextMsg ) ) {
2020-03-25 20:09:04 +00:00
const isAncestor = Boolean (
lodash . get ( currentMsg , "value.meta.thread.ancestorOfTarget" , false )
) ;
2020-03-25 22:39:14 +00:00
msgList . push ( ` <div class="indent"><details ${ isAncestor ? "open" : "" } > ` ) ;
2020-03-22 22:43:25 +00:00
const nextAuthor = lodash . get ( nextMsg , "value.meta.author.name" ) ;
const nextSnippet = postSnippet (
lodash . get ( nextMsg , "value.content.text" )
) ;
msgList . push ( summary ( ` ${ nextAuthor } : ${ nextSnippet } ` ) . outerHTML ) ;
2020-03-22 20:48:59 +00:00
} else if ( depth ( currentMsg ) > depth ( nextMsg ) ) {
2020-03-22 20:36:26 +00:00
// getting more shallow
const diffDepth = depth ( currentMsg ) - depth ( nextMsg ) ;
2020-03-22 20:20:30 +00:00
2020-03-22 21:22:35 +00:00
const shallowList = [ ] ;
2020-03-22 20:36:26 +00:00
for ( let d = 0 ; d < diffDepth ; d ++ ) {
// on the way up it might go several depths at once
2020-03-25 22:39:14 +00:00
shallowList . push ( "</details></div>" ) ;
2020-03-06 15:33:31 +00:00
}
2020-03-22 21:22:35 +00:00
msgList . push ( shallowList ) ;
2020-03-22 20:36:26 +00:00
}
}
2020-03-06 15:33:31 +00:00
2020-03-22 21:22:35 +00:00
const htmlStrings = lodash . flatten ( msgList ) ;
2020-03-22 20:36:26 +00:00
return div ( { } , { innerHTML : htmlStrings . join ( "" ) } ) ;
2020-03-06 15:33:31 +00:00
} ;
2020-03-25 20:31:23 +00:00
const postSnippet = ( text ) => {
2020-03-22 22:43:25 +00:00
const max = 40 ;
2020-03-25 20:31:23 +00:00
text = text . trim ( ) . split ( "\n" , 3 ) . join ( "\n" ) ;
2020-03-22 22:43:25 +00:00
// this is taken directly from patchwork. i'm not entirely sure what this
// regex is doing
text = text . replace ( /_|`|\*|#|^\[@.*?]|\[|]|\(\S*?\)/g , "" ) . trim ( ) ;
text = text . replace ( /:$/ , "" ) ;
2020-03-25 20:31:23 +00:00
text = text . trim ( ) . split ( "\n" , 1 ) [ 0 ] . trim ( ) ;
2020-03-22 22:43:25 +00:00
if ( text . length > max ) {
text = text . substring ( 0 , max - 1 ) + "…" ;
}
return text ;
} ;
2020-02-16 19:14:23 +00:00
/ * *
2020-02-17 11:57:01 +00:00
* Render a section containing a link that takes users to the context for a
2020-02-16 19:14:23 +00:00
* thread preview .
2020-02-17 11:57:01 +00:00
*
2020-02-16 19:14:23 +00:00
* @ param { Array } thread with SSB message objects
* @ param { Boolean } isComment true if this is shown in the context of a comment
* instead of a post
* /
const continueThreadComponent = ( thread , isComment ) => {
const encoded = {
next : encodeURIComponent ( thread [ THREAD _PREVIEW _LENGTH + 1 ] . key ) ,
2020-03-23 22:54:28 +00:00
parent : encodeURIComponent ( thread [ 0 ] . key ) ,
2020-02-17 11:57:01 +00:00
} ;
const left = thread . length - ( THREAD _PREVIEW _LENGTH + 1 ) ;
let continueLink ;
2020-02-16 19:14:23 +00:00
if ( isComment == false ) {
2020-02-17 11:57:01 +00:00
continueLink = ` /thread/ ${ encoded . parent } # ${ encoded . next } ` ;
2020-02-16 19:14:23 +00:00
return a (
{ href : continueLink } ,
` continue reading ${ left } more comment ${ left === 1 ? "" : "s" } `
2020-02-17 11:57:01 +00:00
) ;
2020-02-16 19:14:23 +00:00
} else {
2020-02-17 11:57:01 +00:00
continueLink = ` /thread/ ${ encoded . parent } ` ;
return a ( { href : continueLink } , "read the rest of the thread" ) ;
2020-02-16 19:14:23 +00:00
}
2020-02-17 11:57:01 +00:00
} ;
2020-02-16 19:14:23 +00:00
/ * *
* Render an aside with a preview of comments on a message
2020-02-17 11:57:01 +00:00
*
2020-02-16 19:14:23 +00:00
* For posts , up to three comments are shown , for comments , up to 3 messages
* directly following this one in the thread are displayed . If there are more
* messages in the thread , a link is rendered that links to the rest of the
* context .
2020-02-17 11:57:01 +00:00
*
2020-02-16 19:14:23 +00:00
* @ param { Object } post for which to display the aside
* /
2020-02-17 20:08:03 +00:00
const postAside = ( { key , value } ) => {
const thread = value . meta . thread ;
2020-02-17 11:57:01 +00:00
if ( thread == null ) return null ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
const isComment = value . meta . postType === "comment" ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
let postsToShow ;
2020-02-16 19:14:23 +00:00
if ( isComment ) {
2020-03-23 22:54:28 +00:00
const commentPosition = thread . findIndex ( ( msg ) => msg . key === key ) ;
2020-02-16 19:14:23 +00:00
postsToShow = thread . slice (
commentPosition + 1 ,
Math . min ( commentPosition + ( THREAD _PREVIEW _LENGTH + 1 ) , thread . length )
2020-02-17 11:57:01 +00:00
) ;
2020-02-16 19:14:23 +00:00
} else {
2020-02-17 11:57:01 +00:00
postsToShow = thread . slice (
1 ,
Math . min ( thread . length , THREAD _PREVIEW _LENGTH + 1 )
) ;
2020-02-16 19:14:23 +00:00
}
2020-02-17 11:57:01 +00:00
const fragments = postsToShow . map ( postInAside ) ;
2020-02-16 19:14:23 +00:00
2020-02-17 11:57:01 +00:00
if ( thread . length > THREAD _PREVIEW _LENGTH + 1 ) {
2020-03-25 22:39:14 +00:00
fragments . push ( section ( continueThreadComponent ( thread , isComment ) ) ) ;
2020-02-16 19:14:23 +00:00
}
2020-02-17 20:08:03 +00:00
return div ( { class : "indent" } , fragments ) ;
2020-02-17 11:57:01 +00:00
} ;
2020-02-16 19:14:23 +00:00
2020-02-17 20:08:03 +00:00
const post = ( { msg , aside = false } ) => {
2020-01-31 22:39:18 +00:00
const encoded = {
key : encodeURIComponent ( msg . key ) ,
author : encodeURIComponent ( msg . value . author ) ,
2020-03-23 22:54:28 +00:00
parent : encodeURIComponent ( msg . value . content . root ) ,
2020-01-31 22:39:18 +00:00
} ;
const url = {
author : ` /author/ ${ encoded . author } ` ,
likeForm : ` /like/ ${ encoded . key } ` ,
link : ` /thread/ ${ encoded . key } # ${ encoded . key } ` ,
parent : ` /thread/ ${ encoded . parent } # ${ encoded . parent } ` ,
avatar : msg . value . meta . author . avatar . url ,
json : ` /json/ ${ encoded . key } ` ,
reply : ` /reply/ ${ encoded . key } ` ,
2020-03-23 22:54:28 +00:00
comment : ` /comment/ ${ encoded . key } ` ,
2020-01-31 22:39:18 +00:00
} ;
const isPrivate = Boolean ( msg . value . meta . private ) ;
const isRoot = msg . value . content . root == null ;
const isThreadTarget = Boolean (
lodash . get ( msg , "value.meta.thread.target" , false )
) ;
// TODO: I think this is actually true for both replies and comments.
const isReply = Boolean ( lodash . get ( msg , "value.meta.thread.reply" , false ) ) ;
const { name } = msg . value . meta . author ;
const timeAgo = msg . value . meta . timestamp . received . since . replace ( "~" , "" ) ;
const markdownContent = markdown (
msg . value . content . text ,
msg . value . content . mentions
) ;
const hasContentWarning =
typeof msg . value . content . contentWarning === "string" ;
const likeButton = msg . value . meta . voted
2020-03-31 17:50:00 +00:00
? { value : 0 , class : "liked" }
: { value : 1 , class : null } ;
2020-01-31 22:39:18 +00:00
const likeCount = msg . value . meta . votes . length ;
2020-03-31 06:53:27 +00:00
const maxLikedNameLength = 16 ;
2020-03-31 17:50:00 +00:00
const maxLikedNames = 16 ;
2020-03-31 07:19:58 +00:00
2020-03-31 17:50:00 +00:00
let likedBy = msg . value . meta . votes
2020-03-31 06:53:27 +00:00
. slice ( 0 , maxLikedNames )
. map ( ( name ) => name . slice ( 0 , maxLikedNameLength ) )
. join ( ", " ) ;
2020-03-31 17:50:00 +00:00
if ( likeCount > maxLikedNames ) {
const extraLikes = likeCount - maxLikedNames ;
likedBy += ` + ${ extraLikes } more ` ;
}
2020-02-16 19:14:23 +00:00
const messageClasses = [ "post" ] ;
2020-01-31 22:39:18 +00:00
if ( isPrivate ) {
messageClasses . push ( "private" ) ;
}
if ( isThreadTarget ) {
messageClasses . push ( "thread-target" ) ;
}
if ( isReply ) {
// True for comments too, I think
messageClasses . push ( "reply" ) ;
}
const isFork = msg . value . meta . postType === "reply" ;
2020-02-01 21:20:22 +00:00
// TODO: Refactor to stop using strings and use constants/symbols.
2020-01-31 22:39:18 +00:00
const postOptions = {
post : null ,
2020-02-01 21:20:22 +00:00
comment : i18n . commentDescription ( { parentUrl : url . parent } ) ,
reply : i18n . replyDescription ( { parentUrl : url . parent } ) ,
2020-03-23 22:54:28 +00:00
mystery : i18n . mysteryDescription ,
2020-01-31 22:39:18 +00:00
} ;
const emptyContent = "<p>undefined</p>\n" ;
const articleElement =
markdownContent === emptyContent
? article (
{ class : "content" } ,
pre ( {
innerHTML : highlightJs . highlight (
"json" ,
JSON . stringify ( msg , null , 2 )
2020-03-23 22:54:28 +00:00
) . value ,
2020-01-31 22:39:18 +00:00
} )
)
: article ( { class : "content" , innerHTML : markdownContent } ) ;
const articleContent = hasContentWarning
? details ( summary ( msg . value . content . contentWarning ) , articleElement )
: articleElement ;
const fragment = section (
{
id : msg . key ,
class : messageClasses . join ( " " ) ,
} ,
header (
2020-03-24 00:49:03 +00:00
div (
span (
{ class : "author" } ,
a (
{ href : url . author } ,
img ( { class : "avatar" , src : url . avatar , alt : "" } ) ,
name
) ,
postOptions [ msg . value . meta . postType ]
2020-01-31 22:39:18 +00:00
) ,
2020-03-24 00:49:03 +00:00
span (
{ class : "time" } ,
isPrivate ? "🔒" : null ,
a ( { href : url . link } , nbsp , timeAgo )
)
2020-01-31 22:39:18 +00:00
)
) ,
articleContent ,
// HACK: centered-footer
//
// Here we create an empty div with an anchor tag that can be linked to.
// In our CSS we ensure that this gets centered on the screen when we
// link to this anchor tag.
//
// This is used for redirecting users after they like a post, when we
// want the like button that they just clicked to remain close-ish to
// where it was before they clicked the button.
div ( { id : ` centered-footer- ${ encoded . key } ` , class : "centered-footer" } ) ,
footer (
2020-03-24 00:49:03 +00:00
div (
form (
{ action : url . likeForm , method : "post" } ,
button (
{
name : "voteValue" ,
type : "submit" ,
value : likeButton . value ,
2020-03-24 23:57:22 +00:00
class : likeButton . class ,
2020-03-31 17:50:00 +00:00
title : ` Liked by ${ likedBy } ` ,
2020-03-24 00:49:03 +00:00
} ,
2020-03-31 17:50:00 +00:00
` ❤ ${ likeCount } `
2020-03-31 07:19:58 +00:00
)
2020-03-24 00:49:03 +00:00
) ,
a ( { href : url . comment } , i18n . comment ) ,
isPrivate || isRoot || isFork
? null
: a ( { href : url . reply } , nbsp , i18n . reply ) ,
a ( { href : url . json } , nbsp , i18n . json )
2020-01-31 22:39:18 +00:00
) ,
2020-03-24 00:49:03 +00:00
br ( )
2020-01-31 22:39:18 +00:00
)
2020-02-17 11:57:01 +00:00
) ;
2020-01-31 22:39:18 +00:00
2020-02-17 20:08:03 +00:00
if ( aside ) {
return [ fragment , postAside ( msg ) ] ;
} else {
return fragment ;
}
2020-01-31 22:39:18 +00:00
} ;
2020-01-08 20:56:49 +00:00
2020-02-26 21:38:55 +00:00
exports . editProfileView = ( { name , description } ) =>
template (
section (
h1 ( i18n . editProfile ) ,
p ( i18n . editProfileDescription ) ,
form (
2020-03-01 19:11:09 +00:00
{
action : "/profile/edit" ,
method : "POST" ,
2020-03-23 22:54:28 +00:00
enctype : "multipart/form-data" ,
2020-03-01 19:11:09 +00:00
} ,
2020-02-26 21:38:55 +00:00
label (
2020-03-01 19:11:09 +00:00
i18n . profileImage ,
input ( { type : "file" , name : "image" , accept : "image/*" } )
2020-02-26 21:38:55 +00:00
) ,
2020-03-01 19:11:09 +00:00
label ( i18n . profileName , input ( { name : "name" , value : name } ) ) ,
2020-02-26 21:38:55 +00:00
label (
i18n . profileDescription ,
textarea (
{
autofocus : true ,
2020-03-23 22:54:28 +00:00
name : "description" ,
2020-02-26 21:38:55 +00:00
} ,
description
)
) ,
button (
{
2020-03-23 22:54:28 +00:00
type : "submit" ,
2020-02-26 21:38:55 +00:00
} ,
i18n . submit
)
)
)
) ;
2020-03-28 20:32:02 +00:00
/ * *
* @ param { { avatarUrl : string , description : string , feedId : string , messages : any [ ] , name : string , relationship : object } } input
* /
2020-01-08 20:56:49 +00:00
exports . authorView = ( {
avatarUrl ,
description ,
feedId ,
messages ,
name ,
2020-03-23 22:54:28 +00:00
relationship ,
2020-01-08 20:56:49 +00:00
} ) => {
2020-01-22 00:22:19 +00:00
const mention = ` [@ ${ name } ]( ${ feedId } ) ` ;
const markdownMention = highlightJs . highlight ( "markdown" , mention ) . value ;
2020-01-08 20:56:49 +00:00
2020-03-28 16:44:46 +00:00
const contactForms = [ ] ;
const addForm = ( { action } ) =>
contactForms . push (
form (
{
action : ` / ${ action } / ${ encodeURIComponent ( feedId ) } ` ,
method : "post" ,
} ,
button (
2020-01-22 00:22:19 +00:00
{
2020-03-28 16:44:46 +00:00
type : "submit" ,
2020-01-22 00:22:19 +00:00
} ,
2020-03-28 16:44:46 +00:00
i18n [ action ]
)
)
) ;
2020-03-28 20:32:02 +00:00
if ( relationship . me === false ) {
2020-03-28 16:44:46 +00:00
if ( relationship . following ) {
addForm ( { action : "unfollow" } ) ;
} else if ( relationship . blocking ) {
addForm ( { action : "unblock" } ) ;
} else {
addForm ( { action : "follow" } ) ;
addForm ( { action : "block" } ) ;
}
}
2020-02-01 21:20:22 +00:00
const relationshipText = ( ( ) => {
2020-03-28 20:32:02 +00:00
if ( relationship . me === true ) {
2020-02-01 21:20:22 +00:00
return i18n . relationshipYou ;
} else if (
relationship . following === true &&
relationship . blocking === false
) {
return i18n . relationshipFollowing ;
} else if (
relationship . following === false &&
relationship . blocking === true
) {
return i18n . relationshipBlocking ;
} else if (
relationship . following === false &&
relationship . blocking === false
) {
return i18n . relationshipNone ;
} else if (
relationship . following === true &&
relationship . blocking === true
) {
return i18n . relationshipConflict ;
} else {
throw new Error ( ` Unknown relationship ${ JSON . stringify ( relationship ) } ` ) ;
}
} ) ( ) ;
2020-01-22 00:22:19 +00:00
const prefix = section (
{ class : "message" } ,
2020-03-24 00:49:03 +00:00
div (
2020-01-22 00:22:19 +00:00
{ class : "profile" } ,
img ( { class : "avatar" , src : avatarUrl } ) ,
h1 ( name )
) ,
2020-01-08 20:56:49 +00:00
pre ( {
2020-01-22 00:22:19 +00:00
class : "md-mention" ,
2020-03-23 22:54:28 +00:00
innerHTML : markdownMention ,
2020-01-08 20:56:49 +00:00
} ) ,
2020-01-31 22:39:18 +00:00
description !== "" ? article ( { innerHTML : markdown ( description ) } ) : null ,
2020-01-08 20:56:49 +00:00
footer (
2020-03-24 00:49:03 +00:00
div (
a ( { href : ` /likes/ ${ encodeURIComponent ( feedId ) } ` } , i18n . viewLikes ) ,
span ( nbsp , relationshipText ) ,
2020-03-28 16:44:46 +00:00
... contactForms ,
2020-03-28 20:32:02 +00:00
relationship . me
2020-03-24 00:49:03 +00:00
? a ( { href : ` /profile/edit ` } , nbsp , i18n . editProfile )
: null
) ,
br ( )
2020-01-08 20:56:49 +00:00
)
2020-01-22 00:22:19 +00:00
) ;
2020-01-08 20:56:49 +00:00
return template (
prefix ,
2020-03-23 22:54:28 +00:00
messages . map ( ( msg ) => post ( { msg } ) )
2020-01-22 00:22:19 +00:00
) ;
} ;
2020-01-08 20:56:49 +00:00
exports . commentView = async ( { messages , myFeedId , parentMessage } ) => {
2020-01-22 00:22:19 +00:00
let markdownMention ;
2020-01-08 20:56:49 +00:00
const messageElements = await Promise . all (
2020-03-23 22:54:28 +00:00
messages . reverse ( ) . map ( ( message ) => {
2020-01-22 00:22:19 +00:00
debug ( "%O" , message ) ;
const authorName = message . value . meta . author . name ;
const authorFeedId = message . value . author ;
2020-01-08 20:56:49 +00:00
if ( authorFeedId !== myFeedId ) {
if ( message . key === parentMessage . key ) {
2020-01-22 00:22:19 +00:00
const x = ` [@ ${ authorName } ]( ${ authorFeedId } ) \n \n ` ;
markdownMention = x ;
2020-01-08 20:56:49 +00:00
}
}
2020-01-22 00:22:19 +00:00
return post ( { msg : message } ) ;
2020-01-08 20:56:49 +00:00
} )
2020-01-22 00:22:19 +00:00
) ;
2020-01-08 20:56:49 +00:00
2020-01-22 00:22:19 +00:00
const action = ` /comment/ ${ encodeURIComponent ( messages [ 0 ] . key ) } ` ;
const method = "post" ;
2020-01-08 20:56:49 +00:00
2020-01-22 00:22:19 +00:00
const isPrivate = parentMessage . value . meta . private ;
2020-01-08 20:56:49 +00:00
2020-03-09 11:19:01 +00:00
const publicOrPrivate = isPrivate ? i18n . commentPrivate : i18n . commentPublic ;
2020-02-01 21:20:22 +00:00
const maybeReplyText = isPrivate ? [ null ] : i18n . commentWarning ;
2020-01-08 20:56:49 +00:00
return template (
messageElements ,
2020-01-22 00:22:19 +00:00
p (
2020-02-01 21:20:22 +00:00
... i18n . commentLabel ( { publicOrPrivate , markdownUrl } ) ,
... maybeReplyText
2020-01-08 20:56:49 +00:00
) ,
2020-01-22 00:22:19 +00:00
form (
{ action , method } ,
textarea (
{
autofocus : true ,
required : true ,
2020-03-23 22:54:28 +00:00
name : "text" ,
2020-01-22 00:22:19 +00:00
} ,
2020-02-12 18:55:02 +00:00
isPrivate ? null : markdownMention
2020-01-22 00:22:19 +00:00
) ,
button (
{
2020-03-23 22:54:28 +00:00
type : "submit" ,
2020-01-22 00:22:19 +00:00
} ,
2020-02-01 21:20:22 +00:00
i18n . comment
2020-01-22 00:22:19 +00:00
)
)
) ;
} ;
2020-02-05 01:52:50 +00:00
exports . mentionsView = ( { messages } ) => {
return messageListView ( {
messages ,
viewTitle : i18n . mentions ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . mentionsDescription ,
2020-02-05 01:52:50 +00:00
} ) ;
} ;
exports . privateView = ( { messages } ) => {
return messageListView ( {
messages ,
viewTitle : i18n . private ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . privateDescription ,
2020-02-05 01:52:50 +00:00
} ) ;
} ;
2020-02-14 20:28:05 +00:00
exports . publishCustomView = async ( ) => {
const action = "/publish/custom" ;
2020-02-14 19:50:36 +00:00
const method = "post" ;
return template (
2020-02-14 20:28:05 +00:00
section (
h1 ( i18n . publishCustom ) ,
p ( i18n . publishCustomDescription ) ,
form (
{ action , method } ,
textarea (
{
autofocus : true ,
required : true ,
2020-03-23 22:54:28 +00:00
name : "text" ,
2020-02-14 20:28:05 +00:00
} ,
"{\n" ,
' "type": "test",\n' ,
' "hello": "world"\n' ,
"}"
) ,
button (
{
2020-03-23 22:54:28 +00:00
type : "submit" ,
2020-02-14 20:28:05 +00:00
} ,
i18n . submit
)
2020-02-14 19:50:36 +00:00
)
2020-02-14 20:28:05 +00:00
) ,
p ( i18n . publishBasicInfo ( { href : "/publish" } ) )
2020-02-14 19:50:36 +00:00
) ;
} ;
2020-03-06 15:33:31 +00:00
exports . threadView = ( { messages } ) => template ( thread ( messages ) ) ;
2020-01-08 20:56:49 +00:00
exports . markdownView = ( { text } ) => {
2020-03-04 00:13:56 +00:00
const rawHtml = md . render ( text ) ;
2020-01-08 20:56:49 +00:00
2020-01-22 00:22:19 +00:00
return template ( section ( { class : "message" } , { innerHTML : rawHtml } ) ) ;
} ;
2020-01-08 20:56:49 +00:00
2020-02-05 01:52:50 +00:00
exports . publishView = ( ) => {
const publishForm = "/publish/" ;
return template (
section (
h1 ( i18n . publish ) ,
form (
{ action : publishForm , method : "post" } ,
label (
2020-02-26 21:38:55 +00:00
i18n . publishLabel ( { markdownUrl , linkTarget : "_blank" } ) ,
textarea ( { required : true , name : "text" } )
) ,
label (
i18n . contentWarningLabel ,
input ( {
name : "contentWarning" ,
type : "text" ,
class : "contentWarning" ,
2020-03-23 22:54:28 +00:00
placeholder : i18n . contentWarningPlaceholder ,
2020-02-26 21:38:55 +00:00
} )
2020-02-05 01:52:50 +00:00
) ,
button ( { type : "submit" } , i18n . submit )
)
2020-02-14 20:28:05 +00:00
) ,
p ( i18n . publishCustomInfo ( { href : "/publish/custom" } ) )
2020-02-05 01:52:50 +00:00
) ;
} ;
2020-03-27 15:21:40 +00:00
/ * *
* @ param { { status : object , peers : any [ ] , theme : string , themeNames : string [ ] , version : string } } input
* /
2020-02-18 18:44:36 +00:00
exports . settingsView = ( { status , peers , theme , themeNames , version } ) => {
2020-01-22 00:22:19 +00:00
const max = status . sync . since ;
2020-01-08 20:56:49 +00:00
2020-03-23 22:54:28 +00:00
const progressElements = Object . entries ( status . sync . plugins ) . map ( ( e ) => {
2020-01-22 00:22:19 +00:00
const [ key , val ] = e ;
const id = ` progress- ${ key } ` ;
2020-02-26 21:38:55 +00:00
return div ( label ( key , progress ( { id , value : val , max } , val ) ) ) ;
2020-01-22 00:22:19 +00:00
} ) ;
2020-01-27 00:55:48 +00:00
const startButton = form (
2020-02-11 02:02:16 +00:00
{ action : "/settings/conn/start" , method : "post" } ,
2020-02-01 21:20:22 +00:00
button ( { type : "submit" } , i18n . startNetworking )
2020-01-27 00:55:48 +00:00
) ;
const restartButton = form (
2020-02-11 02:02:16 +00:00
{ action : "/settings/conn/restart" , method : "post" } ,
2020-02-01 21:20:22 +00:00
button ( { type : "submit" } , i18n . restartNetworking )
2020-01-27 00:55:48 +00:00
) ;
const stopButton = form (
2020-02-11 02:02:16 +00:00
{ action : "/settings/conn/stop" , method : "post" } ,
2020-02-01 21:20:22 +00:00
button ( { type : "submit" } , i18n . stopNetworking )
2020-01-27 00:55:48 +00:00
) ;
const connButtons = div ( { class : "form-button-group" } , [
startButton ,
restartButton ,
2020-03-23 22:54:28 +00:00
stopButton ,
2020-01-27 00:55:48 +00:00
] ) ;
2020-03-24 16:46:23 +00:00
const peerList = ( peers || [ ] )
. filter ( ( [ , data ] ) => data . state === "connected" )
. map ( ( [ , data ] ) => {
return li (
a (
{ href : ` /author/ ${ encodeURIComponent ( data . key ) } ` } ,
data . name || data . host || data . key
)
) ;
} ) ;
2020-01-22 00:22:19 +00:00
2020-03-23 22:54:28 +00:00
const themeElements = themeNames . map ( ( cur ) => {
2020-01-22 00:22:19 +00:00
const isCurrentTheme = cur === theme ;
2020-01-08 20:56:49 +00:00
if ( isCurrentTheme ) {
2020-01-22 00:22:19 +00:00
return option ( { value : cur , selected : true } , cur ) ;
2020-01-08 20:56:49 +00:00
} else {
2020-01-22 00:22:19 +00:00
return option ( { value : cur } , cur ) ;
2020-01-08 20:56:49 +00:00
}
2020-01-22 00:22:19 +00:00
} ) ;
2020-01-08 20:56:49 +00:00
const base16 = [
// '00', removed because this is the background
2020-01-22 00:22:19 +00:00
"01" ,
"02" ,
"03" ,
"04" ,
"05" ,
"06" ,
"07" ,
"08" ,
"09" ,
"0A" ,
"0B" ,
"0C" ,
"0D" ,
"0E" ,
2020-03-23 22:54:28 +00:00
"0F" ,
2020-01-22 00:22:19 +00:00
] ;
2020-03-23 22:54:28 +00:00
const base16Elements = base16 . map ( ( base ) =>
2020-01-08 20:56:49 +00:00
div ( {
2020-03-25 22:39:14 +00:00
class : ` theme-preview theme-preview- ${ base } ` ,
2020-01-08 20:56:49 +00:00
} )
2020-01-22 00:22:19 +00:00
) ;
2020-01-08 20:56:49 +00:00
2020-02-02 17:31:43 +00:00
const languageOption = ( shortName , longName ) =>
shortName === selectedLanguage
? option ( { value : shortName , selected : true } , longName )
: option ( { value : shortName } , longName ) ;
2020-01-08 20:56:49 +00:00
return template (
2020-01-22 00:22:19 +00:00
section (
{ class : "message" } ,
2020-02-01 21:20:22 +00:00
h1 ( i18n . settings ) ,
2020-02-18 18:44:36 +00:00
p ( i18n . settingsIntro ( { readmeUrl : "/settings/readme" , version } ) ) ,
2020-03-11 09:26:02 +00:00
h2 ( i18n . peerConnections ) ,
p ( i18n . connectionsIntro ) ,
peerList . length > 0 ? ul ( peerList ) : i18n . noConnections ,
p ( i18n . connectionActionIntro ) ,
connButtons ,
2020-03-11 09:14:14 +00:00
h2 ( i18n . invites ) ,
p ( i18n . invitesDescription ) ,
form (
{ action : "/settings/invite/accept" , method : "post" } ,
input ( { name : "invite" , type : "text" } ) ,
button ( { type : "submit" } , i18n . acceptInvite )
) ,
2020-02-01 21:20:22 +00:00
h2 ( i18n . theme ) ,
p ( i18n . themeIntro ) ,
2020-01-22 00:22:19 +00:00
form (
{ action : "/theme.css" , method : "post" } ,
select ( { name : "theme" } , ... themeElements ) ,
2020-02-01 21:20:22 +00:00
button ( { type : "submit" } , i18n . setTheme )
2020-01-08 20:56:49 +00:00
) ,
base16Elements ,
2020-02-01 22:08:37 +00:00
h2 ( i18n . language ) ,
p ( i18n . languageDescription ) ,
form (
{ action : "/language" , method : "post" } ,
2020-02-02 17:31:43 +00:00
select ( { name : "language" } , [
languageOption ( "en" , "English" ) ,
2020-02-12 15:47:09 +00:00
languageOption ( "es" , "Español" ) ,
/* cspell:disable-next-line */
2020-03-23 22:54:28 +00:00
languageOption ( "de" , "Deutsch" ) ,
2020-02-02 17:31:43 +00:00
] ) ,
2020-02-01 22:08:37 +00:00
button ( { type : "submit" } , i18n . setLanguage )
) ,
2020-03-11 09:14:14 +00:00
h2 ( i18n . indexes ) ,
2020-01-22 23:19:00 +00:00
progressElements
2020-01-08 20:56:49 +00:00
)
2020-01-22 00:22:19 +00:00
) ;
} ;
2020-01-08 20:56:49 +00:00
2020-03-27 15:21:40 +00:00
/** @param {{ viewTitle: string, viewDescription: string }} input */
2020-02-04 21:59:54 +00:00
const viewInfoBox = ( { viewTitle = null , viewDescription = null } ) => {
if ( ! viewTitle && ! viewDescription ) {
return null ;
}
return section (
{ class : "viewInfo" } ,
viewTitle ? h1 ( viewTitle ) : null ,
viewDescription ? em ( viewDescription ) : null
) ;
} ;
exports . likesView = async ( { messages , feed , name } ) => {
const authorLink = a (
{ href : ` /author/ ${ encodeURIComponent ( feed ) } ` } ,
"@" + name
) ;
return template (
viewInfoBox ( {
2020-03-23 22:54:28 +00:00
viewTitle : span ( authorLink , i18n . likedBy ) ,
2020-03-27 15:21:40 +00:00
// TODO: i18n
viewDescription : "List of messages liked by this author." ,
2020-02-04 21:59:54 +00:00
} ) ,
2020-03-23 22:54:28 +00:00
messages . map ( ( msg ) => post ( { msg } ) )
2020-02-04 21:59:54 +00:00
) ;
} ;
2020-02-05 01:52:50 +00:00
const messageListView = ( {
2020-02-04 21:27:41 +00:00
messages ,
viewTitle = null ,
2020-02-05 01:52:50 +00:00
viewDescription = null ,
2020-02-17 20:08:03 +00:00
viewElements = null ,
// If `aside = true`, it will show a few comments in the thread.
2020-03-23 22:54:28 +00:00
aside = null ,
2020-02-04 21:27:41 +00:00
} ) => {
2020-01-08 20:56:49 +00:00
return template (
2020-02-05 01:52:50 +00:00
section ( h1 ( viewTitle ) , p ( viewDescription ) , viewElements ) ,
2020-03-23 22:54:28 +00:00
messages . map ( ( msg ) => post ( { msg , aside } ) )
2020-01-22 00:22:19 +00:00
) ;
} ;
2020-01-08 20:56:49 +00:00
2020-02-05 01:52:50 +00:00
exports . popularView = ( { messages , prefix } ) => {
return messageListView ( {
2020-02-04 21:27:41 +00:00
messages ,
2020-02-05 01:52:50 +00:00
viewElements : prefix ,
2020-02-04 21:27:41 +00:00
viewTitle : i18n . popular ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . popularDescription ,
2020-02-04 21:27:41 +00:00
} ) ;
} ;
2020-02-05 01:52:50 +00:00
exports . extendedView = ( { messages } ) => {
return messageListView ( {
2020-02-04 21:27:41 +00:00
messages ,
viewTitle : i18n . extended ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . extendedDescription ,
2020-02-04 21:27:41 +00:00
} ) ;
} ;
2020-02-05 01:52:50 +00:00
exports . latestView = ( { messages } ) => {
return messageListView ( {
2020-02-04 21:27:41 +00:00
messages ,
viewTitle : i18n . latest ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . latestDescription ,
2020-02-04 21:27:41 +00:00
} ) ;
} ;
2020-02-05 01:52:50 +00:00
exports . topicsView = ( { messages } ) => {
return messageListView ( {
2020-02-04 21:27:41 +00:00
messages ,
viewTitle : i18n . topics ,
2020-03-23 22:54:28 +00:00
viewDescription : i18n . topicsDescription ,
2020-02-04 21:27:41 +00:00
} ) ;
} ;
2020-02-17 20:08:03 +00:00
exports . summaryView = ( { messages } ) => {
return messageListView ( {
messages ,
viewTitle : i18n . summaries ,
viewDescription : i18n . summariesDescription ,
2020-03-23 22:54:28 +00:00
aside : true ,
2020-02-17 20:08:03 +00:00
} ) ;
} ;
2020-03-10 12:42:02 +00:00
exports . threadsView = ( { messages } ) => {
return messageListView ( {
messages ,
viewTitle : i18n . threads ,
viewDescription : i18n . threadsDescription ,
2020-03-25 13:05:53 +00:00
aside : true ,
2020-02-17 20:08:03 +00:00
} ) ;
} ;
2020-01-08 20:56:49 +00:00
exports . replyView = async ( { messages , myFeedId } ) => {
2020-01-22 00:22:19 +00:00
const replyForm = ` /reply/ ${ encodeURIComponent (
messages [ messages . length - 1 ] . key
) } ` ;
2020-01-08 20:56:49 +00:00
2020-01-22 00:22:19 +00:00
let markdownMention ;
2020-01-08 20:56:49 +00:00
const messageElements = await Promise . all (
2020-03-23 22:54:28 +00:00
messages . reverse ( ) . map ( ( message ) => {
2020-01-22 00:22:19 +00:00
debug ( "%O" , message ) ;
const authorName = message . value . meta . author . name ;
const authorFeedId = message . value . author ;
2020-01-08 20:56:49 +00:00
if ( authorFeedId !== myFeedId ) {
if ( message . key === messages [ 0 ] . key ) {
2020-01-22 00:22:19 +00:00
const x = ` [@ ${ authorName } ]( ${ authorFeedId } ) \n \n ` ;
markdownMention = x ;
2020-01-08 20:56:49 +00:00
}
}
2020-01-22 00:22:19 +00:00
return post ( { msg : message } ) ;
2020-01-08 20:56:49 +00:00
} )
2020-01-22 00:22:19 +00:00
) ;
2020-01-08 20:56:49 +00:00
return template (
messageElements ,
2020-02-01 21:20:22 +00:00
p ( i18n . replyLabel ( { markdownUrl } ) ) ,
2020-01-22 00:22:19 +00:00
form (
{ action : replyForm , method : "post" } ,
textarea (
{
autofocus : true ,
required : true ,
2020-03-23 22:54:28 +00:00
name : "text" ,
2020-01-22 00:22:19 +00:00
} ,
markdownMention
) ,
button (
{
2020-03-23 22:54:28 +00:00
type : "submit" ,
2020-01-22 00:22:19 +00:00
} ,
2020-02-01 21:20:22 +00:00
i18n . reply
2020-01-22 00:22:19 +00:00
)
)
) ;
} ;
2020-02-02 20:20:47 +00:00
exports . searchView = ( { messages , query } ) => {
const searchInput = input ( {
name : "query" ,
required : false ,
type : "search" ,
2020-03-23 22:54:28 +00:00
value : query ,
2020-02-02 20:20:47 +00:00
} ) ;
// - Minimum length of 3 because otherwise SSB-Search hangs forever. :)
// https://github.com/ssbc/ssb-search/issues/8
// - Using `setAttribute()` because HyperScript (the HyperAxe dependency has
// a bug where the `minlength` property is being ignored. No idea why.
// https://github.com/hyperhype/hyperscript/issues/91
searchInput . setAttribute ( "minlength" , 3 ) ;
return template (
2020-01-22 00:22:19 +00:00
section (
2020-02-05 01:52:50 +00:00
h1 ( i18n . search ) ,
2020-01-22 00:22:19 +00:00
form (
{ action : "/search" , method : "get" } ,
2020-02-26 21:38:55 +00:00
label ( i18n . searchLabel , searchInput ) ,
2020-01-22 00:22:19 +00:00
button (
{
2020-03-23 22:54:28 +00:00
type : "submit" ,
2020-01-22 00:22:19 +00:00
} ,
2020-02-01 21:20:22 +00:00
i18n . submit
2020-01-22 00:22:19 +00:00
)
)
2020-01-08 20:56:49 +00:00
) ,
2020-03-23 22:54:28 +00:00
messages . map ( ( msg ) => post ( { msg } ) )
2020-01-22 00:22:19 +00:00
) ;
2020-02-02 20:20:47 +00:00
} ;
2020-02-27 20:09:37 +00:00
exports . hashtagView = ( { messages , hashtag } ) => {
return template (
section ( h1 ( ` # ${ hashtag } ` ) , p ( i18n . hashtagDescription ) ) ,
2020-03-23 22:54:28 +00:00
messages . map ( ( msg ) => post ( { msg } ) )
2020-02-27 20:09:37 +00:00
) ;
} ;
2020-03-17 14:24:46 +00:00
2020-03-27 15:21:40 +00:00
/** @param {{percent: number}} input */
2020-03-17 14:24:46 +00:00
exports . indexingView = ( { percent } ) => {
2020-03-17 14:33:31 +00:00
// TODO: i18n
const message = ` Oasis has only processed ${ percent } % of the messages and needs to catch up. This page will refresh every 10 seconds. Thanks for your patience! ❤ ` ;
2020-03-17 14:24:46 +00:00
const nodes = html (
{ lang : "en" } ,
head (
2020-03-17 14:33:31 +00:00
title ( "Oasis" ) ,
2020-03-17 14:24:46 +00:00
link ( { rel : "icon" , type : "image/svg+xml" , href : "/assets/favicon.svg" } ) ,
meta ( { charset : "utf-8" } ) ,
meta ( {
name : "description" ,
2020-03-23 22:54:28 +00:00
content : i18n . oasisDescription ,
2020-03-17 14:24:46 +00:00
} ) ,
meta ( {
name : "viewport" ,
2020-03-23 22:54:28 +00:00
content : toAttributes ( { width : "device-width" , "initial-scale" : 1 } ) ,
2020-03-17 14:24:46 +00:00
} ) ,
meta ( { "http-equiv" : "refresh" , content : 10 } )
) ,
2020-03-17 14:33:31 +00:00
body (
main (
{ id : "content" } ,
p ( message ) ,
progress ( { value : percent , max : 100 } )
)
)
2020-03-17 14:24:46 +00:00
) ;
const result = doctypeString + nodes . outerHTML ;
return result ;
} ;