2020-01-22 00:22:19 +00:00
"use strict" ;
2020-01-08 20:56:49 +00:00
2020-12-08 17:37:26 +00:00
const path = require ( "path" ) ;
const envPaths = require ( "env-paths" ) ;
const fs = require ( "fs" ) ;
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-10-12 14:14:28 +00:00
const prettyMs = require ( "pretty-ms" ) ;
2020-03-04 00:13:56 +00:00
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-05-13 18:17:35 +00:00
hr ,
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-02-01 22:08:37 +00:00
const i18nBase = require ( "./i18n" ) ;
2020-10-12 14:14:28 +00:00
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-05-11 14:40:51 +00:00
const template = ( titlePrefix , ... elements ) => {
2020-10-19 07:27:10 +00:00
const navLink = ( { href , emoji , text } ) =>
li (
a (
{ href , class : titlePrefix === text ? "current" : "" } ,
span ( { class : "emoji" } , emoji ) ,
nbsp ,
text
)
) ;
2020-10-12 07:55:42 +00:00
2020-12-08 17:37:26 +00:00
const customCSS = ( filename ) => {
const customStyleFile = path . join (
envPaths ( "oasis" , { suffix : "" } ) . config ,
filename
) ;
try {
if ( fs . existsSync ( customStyleFile ) ) {
return link ( { rel : "stylesheet" , href : filename } ) ;
}
} catch ( error ) {
return "" ;
}
} ;
2020-02-01 21:20:22 +00:00
const nodes = html (
{ lang : "en" } ,
head (
2020-05-11 14:40:51 +00:00
title ( titlePrefix , " - Oasis" ) ,
2020-02-01 21:20:22 +00:00
link ( { rel : "stylesheet" , href : "/theme.css" } ) ,
link ( { rel : "stylesheet" , href : "/assets/style.css" } ) ,
link ( { rel : "stylesheet" , href : "/assets/highlight.css" } ) ,
2020-12-08 17:37:26 +00:00
customCSS ( "/custom-style.css" ) ,
2020-02-01 21:20:22 +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-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-05-03 21:14:19 +00:00
navLink ( {
href : "/imageSearch" ,
emoji : "🖼️" ,
text : i18n . imageSearch ,
} ) ,
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-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 )
) ;
2021-03-02 22:35:38 +00:00
const isBlocked = Boolean ( nextMsg . value . meta . blocking ) ;
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 (
2020-10-19 07:27:10 +00:00
lodash . has ( nextMsg , "value.content.contentWarning" )
? lodash . get ( nextMsg , "value.content.contentWarning" )
: lodash . get ( nextMsg , "value.content.text" )
2020-03-22 22:43:25 +00:00
) ;
2021-03-02 22:35:38 +00:00
msgList . push (
summary (
isBlocked
? i18n . relationshipBlockingPost
: ` ${ 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-10-19 07:27:10 +00:00
return div (
{ } ,
{ class : "thread-container" , 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-04-11 03:59:28 +00:00
const fragments = postsToShow . map ( ( p ) => post ( { msg : p } ) ) ;
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 } ` ,
2020-04-16 15:31:35 +00:00
subtopic : ` /subtopic/ ${ 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 ) ;
2021-03-02 22:35:38 +00:00
const isBlocked = Boolean ( msg . value . meta . blocking ) ;
2020-01-31 22:39:18 +00:00
const isRoot = msg . value . content . root == null ;
2020-04-16 15:31:35 +00:00
const isFork = msg . value . meta . postType === "subtopic" ;
2020-04-11 03:59:28 +00:00
const hasContentWarning =
typeof msg . value . content . contentWarning === "string" ;
2020-01-31 22:39:18 +00:00
const isThreadTarget = Boolean (
lodash . get ( msg , "value.meta.thread.target" , false )
) ;
const { name } = msg . value . meta . author ;
2020-10-08 22:11:21 +00:00
const ts _received = msg . value . meta . timestamp . received ;
const timeAgo = ts _received . since . replace ( "~" , "" ) ;
const timeAbsolute = ts _received . iso8601 . split ( "." ) [ 0 ] . replace ( "T" , " " ) ;
2020-01-31 22:39:18 +00:00
const markdownContent = markdown (
msg . value . content . text ,
msg . value . content . mentions
) ;
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 18:05:12 +00:00
const likedByNames = msg . value . meta . votes
2020-03-31 06:53:27 +00:00
. slice ( 0 , maxLikedNames )
2021-02-13 15:23:26 +00:00
. map ( ( person ) => person . name )
2020-03-31 06:53:27 +00:00
. map ( ( name ) => name . slice ( 0 , maxLikedNameLength ) )
. join ( ", " ) ;
2020-03-31 18:05:12 +00:00
const additionalLikesMessage =
likeCount > maxLikedNames ? ` + ${ likeCount - maxLikedNames } more ` : ` ` ;
const likedByMessage =
likeCount > 0 ? ` Liked by ${ likedByNames } ${ additionalLikesMessage } ` : null ;
2020-03-31 17:50:00 +00:00
2020-02-16 19:14:23 +00:00
const messageClasses = [ "post" ] ;
2020-01-31 22:39:18 +00:00
2021-01-26 09:33:29 +00:00
const recps = [ ] ;
const addRecps = ( recpsInfo ) => {
recpsInfo . forEach ( function ( recp ) {
recps . push (
a (
2021-01-27 09:57:04 +00:00
{ href : ` /author/ ${ encodeURIComponent ( recp . feedId ) } ` } ,
2021-01-26 09:33:29 +00:00
img ( { class : "avatar" , src : recp . avatarUrl , alt : "" } )
)
) ;
} ) ;
} ;
2020-01-31 22:39:18 +00:00
if ( isPrivate ) {
messageClasses . push ( "private" ) ;
2021-01-26 09:33:29 +00:00
addRecps ( msg . value . meta . recpsInfo ) ;
2020-01-31 22:39:18 +00:00
}
if ( isThreadTarget ) {
messageClasses . push ( "thread-target" ) ;
}
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 } ) ,
2020-04-16 15:31:35 +00:00
subtopic : i18n . subtopicDescription ( { 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 } ) ;
2021-03-02 22:35:38 +00:00
if ( isBlocked ) {
messageClasses . push ( "blocked" ) ;
return section (
{
id : msg . key ,
class : messageClasses . join ( " " ) ,
} ,
i18n . relationshipBlockingPost
) ;
}
2020-01-31 22:39:18 +00:00
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
2020-05-12 12:27:40 +00:00
)
2020-01-31 22:39:18 +00:00
) ,
2020-05-12 12:27:40 +00:00
span ( { class : "author-action" } , postOptions [ msg . value . meta . postType ] ) ,
2020-03-24 00:49:03 +00:00
span (
2020-10-08 22:11:21 +00:00
{
class : "time" ,
title : timeAbsolute ,
} ,
2020-03-24 00:49:03 +00:00
isPrivate ? "🔒" : null ,
2021-01-26 09:33:29 +00:00
isPrivate ? recps : null ,
2020-03-24 00:49:03 +00:00
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 18:05:12 +00:00
title : likedByMessage ,
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
2020-04-16 15:31:35 +00:00
: a ( { href : url . subtopic } , nbsp , i18n . subtopic ) ,
2020-03-24 00:49:03 +00:00
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-05-13 18:17:35 +00:00
const threadSeparator = [ div ( { class : "text-browser" } , hr ( ) , br ( ) ) ] ;
2020-02-17 20:08:03 +00:00
if ( aside ) {
2020-05-13 18:17:35 +00:00
return [ fragment , postAside ( msg ) , isRoot ? threadSeparator : null ] ;
2020-02-17 20:08:03 +00:00
} 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 (
2020-05-19 11:42:19 +00:00
i18n . editProfile ,
2020-02-26 21:38:55 +00:00
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
/ * *
2020-05-23 18:48:43 +00:00
* @ param { { avatarUrl : string , description : string , feedId : string , messages : any [ ] , name : string , relationship : object , firstPost : object , lastPost : object } } input
2020-03-28 20:32:02 +00:00
* /
2020-01-08 20:56:49 +00:00
exports . authorView = ( {
avatarUrl ,
description ,
feedId ,
messages ,
2020-05-23 03:44:26 +00:00
firstPost ,
lastPost ,
2020-01-08 20:56:49 +00:00
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
2020-05-23 18:48:43 +00:00
const linkUrl = relationship . me
? "/profile/"
: ` /author/ ${ encodeURIComponent ( feedId ) } / ` ;
2020-05-23 03:44:26 +00:00
let items = messages . map ( ( msg ) => post ( { msg } ) ) ;
if ( items . length === 0 ) {
if ( lastPost === undefined ) {
2020-05-23 18:48:43 +00:00
items . push ( section ( div ( span ( i18n . feedEmpty ) ) ) ) ;
2020-05-23 03:44:26 +00:00
} else {
items . push (
section (
div (
span ( i18n . feedRangeEmpty ) ,
a ( { href : ` ${ linkUrl } ` } , i18n . seeFullFeed )
)
)
) ;
}
} else {
const highestSeqNum = messages [ 0 ] . value . sequence ;
2020-05-23 18:48:43 +00:00
const lowestSeqNum = messages [ messages . length - 1 ] . value . sequence ;
2020-05-23 03:44:26 +00:00
let newerPostsLink ;
if ( lastPost !== undefined && highestSeqNum < lastPost . value . sequence )
2020-05-23 18:48:43 +00:00
newerPostsLink = a (
{ href : ` ${ linkUrl } ?gt= ${ highestSeqNum } ` } ,
i18n . newerPosts
) ;
else newerPostsLink = span ( i18n . newerPosts , { title : i18n . noNewerPosts } ) ;
2020-05-23 03:44:26 +00:00
let olderPostsLink ;
if ( lowestSeqNum > firstPost . value . sequence )
2020-05-23 18:48:43 +00:00
olderPostsLink = a (
{ href : ` ${ linkUrl } ?lt= ${ lowestSeqNum } ` } ,
i18n . olderPosts
) ;
2020-05-23 03:44:26 +00:00
else
olderPostsLink = span ( i18n . olderPosts , { title : i18n . beginningOfFeed } ) ;
const pagination = section (
{ class : "message" } ,
2020-05-23 18:48:43 +00:00
footer ( div ( newerPostsLink , olderPostsLink ) , br ( ) )
2020-05-23 03:44:26 +00:00
) ;
items . unshift ( pagination ) ;
items . push ( pagination ) ;
}
2020-05-23 18:48:43 +00:00
return template ( i18n . profile , prefix , items ) ;
2020-01-22 00:22:19 +00:00
} ;
2020-01-08 20:56:49 +00:00
2020-10-19 07:27:10 +00:00
exports . previewCommentView = async ( {
previewData ,
messages ,
myFeedId ,
parentMessage ,
contentWarning ,
} ) => {
2020-10-13 10:23:46 +00:00
const publishAction = ` /comment/ ${ encodeURIComponent ( messages [ 0 ] . key ) } ` ;
2020-10-19 07:27:10 +00:00
const preview = generatePreview ( {
previewData ,
contentWarning ,
action : publishAction ,
} ) ;
return exports . commentView (
{ messages , myFeedId , parentMessage } ,
preview ,
previewData . text ,
contentWarning
) ;
2020-10-13 10:23:46 +00:00
} ;
2020-10-19 07:27:10 +00:00
exports . commentView = async (
{ messages , myFeedId , parentMessage } ,
preview ,
text ,
contentWarning
) => {
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-10-13 10:23:46 +00:00
const action = ` /comment/preview/ ${ encodeURIComponent ( messages [ 0 ] . key ) } ` ;
2020-01-22 00:22:19 +00:00
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-05-11 14:40:51 +00:00
const authorName = parentMessage . value . meta . author . name ;
2020-01-08 20:56:49 +00:00
2020-03-09 11:19:01 +00:00
const publicOrPrivate = isPrivate ? i18n . commentPrivate : i18n . commentPublic ;
2020-04-16 15:31:35 +00:00
const maybeSubtopicText = isPrivate ? [ null ] : i18n . commentWarning ;
2020-01-08 20:56:49 +00:00
return template (
2020-05-11 14:40:51 +00:00
i18n . commentTitle ( { authorName } ) ,
2020-10-19 07:27:10 +00:00
div ( { class : "thread-container" } , messageElements ) ,
preview !== undefined ? preview : "" ,
2020-01-22 00:22:19 +00:00
p (
2020-02-01 21:20:22 +00:00
... i18n . commentLabel ( { publicOrPrivate , markdownUrl } ) ,
2020-04-16 15:31:35 +00:00
... maybeSubtopicText
2020-01-08 20:56:49 +00:00
) ,
2020-01-22 00:22:19 +00:00
form (
2020-10-14 09:24:45 +00:00
{ action , method , enctype : "multipart/form-data" } ,
2020-01-22 00:22:19 +00:00
textarea (
{
autofocus : true ,
required : true ,
2020-03-23 22:54:28 +00:00
name : "text" ,
2020-01-22 00:22:19 +00:00
} ,
2020-10-19 07:27:10 +00:00
text ? text : isPrivate ? null : markdownMention
2020-01-22 00:22:19 +00:00
) ,
2020-10-14 08:34:18 +00:00
label (
i18n . contentWarningLabel ,
input ( {
name : "contentWarning" ,
type : "text" ,
class : "contentWarning" ,
2020-10-19 07:27:10 +00:00
value : contentWarning ? contentWarning : "" ,
2020-10-14 08:34:18 +00:00
placeholder : i18n . contentWarningPlaceholder ,
} )
) ,
2020-10-19 07:27:10 +00:00
button ( { type : "submit" } , i18n . preview ) ,
label ( { class : "file-button" , for : "blob" } , i18n . attachFiles ) ,
2020-10-14 09:24:45 +00:00
input ( { type : "file" , id : "blob" , name : "blob" } )
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-05-11 14:40:51 +00:00
i18n . publishCustom ,
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-05-11 14:40:51 +00:00
exports . threadView = ( { messages } ) => {
const rootMessage = messages [ 0 ] ;
const rootAuthorName = rootMessage . value . meta . author . name ;
const rootSnippet = postSnippet (
2020-05-12 13:33:28 +00:00
lodash . get ( rootMessage , "value.content.text" , i18n . mysteryDescription )
2020-05-11 14:40:51 +00:00
) ;
return template ( [ ` @ ${ rootAuthorName } : ` , rootSnippet ] , thread ( messages ) ) ;
} ;
2020-01-08 20:56:49 +00:00
2020-10-12 14:14:28 +00:00
// this view is only used for the /settings/readme page.
2020-10-21 13:33:01 +00:00
// To fix style glitches it uses the default MarkdownIt and not ssb-markdown.
2020-10-12 14:14:28 +00:00
const md = new MarkdownIt ( ) ;
2020-01-08 20:56:49 +00:00
exports . markdownView = ( { text } ) => {
2020-10-12 14:14:28 +00:00
const rawHtml = md . render ( text ) ;
2020-01-08 20:56:49 +00:00
2020-05-11 14:40:51 +00:00
return template (
postSnippet ( text ) ,
section ( { class : "message" } , { innerHTML : rawHtml } )
) ;
2020-01-22 00:22:19 +00:00
} ;
2020-01-08 20:56:49 +00:00
2020-10-14 08:34:18 +00:00
exports . publishView = ( preview , text , contentWarning ) => {
2020-02-05 01:52:50 +00:00
return template (
2020-05-11 14:40:51 +00:00
i18n . publish ,
2020-02-05 01:52:50 +00:00
section (
h1 ( i18n . publish ) ,
form (
2020-10-19 07:27:10 +00:00
{
2020-10-12 11:49:32 +00:00
action : "/publish/preview" ,
method : "post" ,
enctype : "multipart/form-data" ,
} ,
2020-02-05 01:52:50 +00:00
label (
2020-02-26 21:38:55 +00:00
i18n . publishLabel ( { markdownUrl , linkTarget : "_blank" } ) ,
2020-10-19 07:27:10 +00:00
textarea ( { required : true , name : "text" } , text ? text : "" )
2020-02-26 21:38:55 +00:00
) ,
label (
i18n . contentWarningLabel ,
input ( {
name : "contentWarning" ,
type : "text" ,
class : "contentWarning" ,
2020-10-19 07:27:10 +00:00
value : contentWarning ? 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
) ,
2020-10-13 08:29:14 +00:00
button ( { type : "submit" } , i18n . preview ) ,
2020-10-19 07:27:10 +00:00
label ( { class : "file-button" , for : "blob" } , i18n . attachFiles ) ,
2020-10-13 08:29:14 +00:00
input ( { type : "file" , id : "blob" , name : "blob" } )
2020-02-05 01:52:50 +00:00
)
2020-02-14 20:28:05 +00:00
) ,
2020-10-19 07:27:10 +00:00
preview ? preview : "" ,
2020-02-14 20:28:05 +00:00
p ( i18n . publishCustomInfo ( { href : "/publish/custom" } ) )
2020-02-05 01:52:50 +00:00
) ;
} ;
2020-10-14 09:44:09 +00:00
const generatePreview = ( { previewData , contentWarning , action } ) => {
2020-10-19 07:27:10 +00:00
const { authorMeta , text , mentions } = previewData ;
2020-10-14 09:44:09 +00:00
2020-10-19 07:27:10 +00:00
// craft message that looks like it came from the db
2020-10-13 08:29:14 +00:00
// cb: this kinda fragile imo? this is for getting a proper post styling ya?
2020-10-12 11:49:32 +00:00
const msg = {
2020-10-21 13:33:01 +00:00
key : "%non-existent.preview" ,
2020-10-12 11:49:32 +00:00
value : {
author : authorMeta . id ,
// sequence: -1,
content : {
2020-10-19 07:27:10 +00:00
type : "post" ,
text : text ,
2020-10-12 11:49:32 +00:00
} ,
timestamp : Date . now ( ) ,
meta : {
isPrivate : true ,
2020-10-13 08:29:14 +00:00
votes : [ ] ,
2020-10-12 11:49:32 +00:00
author : {
name : authorMeta . name ,
avatar : {
2020-10-19 07:27:10 +00:00
url : ` /image/64/ ${ encodeURIComponent ( authorMeta . image ) } ` ,
} ,
2020-10-12 11:49:32 +00:00
} ,
2020-10-19 07:27:10 +00:00
} ,
} ,
} ;
if ( contentWarning ) msg . value . content . contentWarning = contentWarning ;
2020-10-12 11:49:32 +00:00
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 ) ;
2020-10-14 09:44:09 +00:00
return div (
2020-10-19 07:27:10 +00:00
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 } ) `
)
)
) ;
} )
) ;
2020-10-15 13:28:21 +00:00
} )
2020-10-19 07:27:10 +00:00
) ,
section (
{ class : "post-preview" } ,
post ( { msg } ) ,
2020-10-13 09:14:15 +00:00
// doesn't need blobs, preview adds them to the text
form (
2020-10-13 10:23:46 +00:00
{ action , method : "post" } ,
2020-10-13 09:14:15 +00:00
input ( {
name : "contentWarning" ,
type : "hidden" ,
value : contentWarning ,
2020-10-19 07:27:10 +00:00
} ) ,
2020-10-13 09:14:15 +00:00
input ( {
2020-10-19 07:27:10 +00:00
name : "text" ,
type : "hidden" ,
value : text ,
2020-10-13 09:14:15 +00:00
} ) ,
2020-10-19 07:27:10 +00:00
button ( { type : "submit" } , i18n . publish )
)
2020-10-14 09:44:09 +00:00
)
2020-10-19 07:27:10 +00:00
) ;
} ;
2020-10-13 09:14:15 +00:00
2020-10-14 09:44:09 +00:00
exports . previewView = ( { previewData , contentWarning } ) => {
2020-10-14 08:01:49 +00:00
const publishAction = "/publish" ;
2020-10-19 07:27:10 +00:00
const preview = generatePreview ( {
previewData ,
contentWarning ,
action : publishAction ,
} ) ;
return exports . publishView ( preview , previewData . text , contentWarning ) ;
} ;
2020-10-12 11:49:32 +00:00
2020-03-27 15:21:40 +00:00
/ * *
* @ param { { status : object , peers : any [ ] , theme : string , themeNames : string [ ] , version : string } } input
* /
2020-11-29 18:48:23 +00:00
exports . settingsView = ( { peers , theme , themeNames , version } ) => {
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
) ;
2020-04-03 22:23:23 +00:00
const syncButton = form (
{ action : "/settings/conn/sync" , method : "post" } ,
button ( { type : "submit" } , i18n . sync )
) ;
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-04-03 22:23:23 +00:00
syncButton ,
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-04-13 17:22:14 +00:00
const languageOption = ( longName , shortName ) =>
2020-02-02 17:31:43 +00:00
shortName === selectedLanguage
? option ( { value : shortName , selected : true } , longName )
: option ( { value : shortName } , longName ) ;
2020-11-29 18:48:23 +00:00
const rebuildButton = form (
{ action : "/settings/rebuild" , method : "post" } ,
button ( { type : "submit" } , i18n . rebuildName )
) ;
2020-01-08 20:56:49 +00:00
return template (
2020-05-11 14:40:51 +00:00
i18n . settings ,
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" } , [
2020-04-13 17:22:14 +00:00
// Languages are sorted alphabetically by their 'long name'.
2020-11-23 00:52:12 +00:00
/* spell-checker:disable */
2020-04-13 17:22:14 +00:00
languageOption ( "Deutsch" , "de" ) ,
languageOption ( "English" , "en" ) ,
languageOption ( "Español" , "es" ) ,
languageOption ( "Français" , "fr" ) ,
languageOption ( "Italiano" , "it" ) ,
2020-11-23 00:52:12 +00:00
/* spell-checker:enable */
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-11-29 18:48:23 +00:00
p ( i18n . indexesDescription ) ,
rebuildButton
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 (
2020-05-11 14:40:51 +00:00
[ "@" , name , i18n . likedBy ] ,
2020-02-04 21:59:54 +00:00
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-05-11 14:40:51 +00:00
viewTitle ,
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-10-19 07:27:10 +00:00
exports . previewSubtopicView = async ( {
previewData ,
messages ,
myFeedId ,
contentWarning ,
} ) => {
2020-10-14 07:41:31 +00:00
const publishAction = ` /subtopic/ ${ encodeURIComponent ( messages [ 0 ] . key ) } ` ;
2020-10-19 07:27:10 +00:00
const preview = generatePreview ( {
previewData ,
contentWarning ,
action : publishAction ,
} ) ;
return exports . subtopicView (
{ messages , myFeedId } ,
preview ,
previewData . text ,
contentWarning
) ;
2020-10-14 07:41:31 +00:00
} ;
2020-10-19 07:27:10 +00:00
exports . subtopicView = async (
{ messages , myFeedId } ,
preview ,
text ,
contentWarning
) => {
2020-10-14 07:41:31 +00:00
const subtopicForm = ` /subtopic/preview/ ${ encodeURIComponent (
2020-01-22 00:22:19 +00:00
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
2020-05-11 14:40:51 +00:00
const authorName = messages [ messages . length - 1 ] . value . meta . author . name ;
2020-01-08 20:56:49 +00:00
return template (
2020-05-11 14:40:51 +00:00
i18n . subtopicTitle ( { authorName } ) ,
2020-10-19 07:27:10 +00:00
div ( { class : "thread-container" } , messageElements ) ,
preview !== undefined ? preview : "" ,
2020-04-16 15:31:35 +00:00
p ( i18n . subtopicLabel ( { markdownUrl } ) ) ,
2020-01-22 00:22:19 +00:00
form (
2020-10-14 09:24:45 +00:00
{ action : subtopicForm , method : "post" , enctype : "multipart/form-data" } ,
2020-01-22 00:22:19 +00:00
textarea (
{
autofocus : true ,
required : true ,
2020-03-23 22:54:28 +00:00
name : "text" ,
2020-01-22 00:22:19 +00:00
} ,
2020-10-14 07:41:31 +00:00
text ? text : markdownMention
2020-01-22 00:22:19 +00:00
) ,
2020-10-14 08:34:18 +00:00
label (
i18n . contentWarningLabel ,
input ( {
name : "contentWarning" ,
type : "text" ,
class : "contentWarning" ,
2020-10-19 07:27:10 +00:00
value : contentWarning ? contentWarning : "" ,
2020-10-14 08:34:18 +00:00
placeholder : i18n . contentWarningPlaceholder ,
} )
) ,
2020-10-19 07:27:10 +00:00
button ( { type : "submit" } , i18n . preview ) ,
label ( { class : "file-button" , for : "blob" } , i18n . attachFiles ) ,
2020-10-14 09:24:45 +00:00
input ( { type : "file" , id : "blob" , name : "blob" } )
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-05-11 14:40:51 +00:00
i18n . search ,
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
2020-05-03 20:16:44 +00:00
const imageResult = ( { id , infos } ) => {
2020-05-03 21:14:19 +00:00
const encodedBlobId = encodeURIComponent ( id ) ;
2020-05-03 20:16:44 +00:00
// only rendering the first message result so far
// todo: render links to the others as well
2020-05-03 21:14:19 +00:00
const info = infos [ 0 ] ;
const encodedMsgId = encodeURIComponent ( info . msg ) ;
2020-05-03 20:16:44 +00:00
return div (
{
2020-05-03 21:14:19 +00:00
class : "image-result" ,
2020-05-03 20:16:44 +00:00
} ,
[
a (
{
2020-05-03 21:14:19 +00:00
href : ` /blob/ ${ encodedBlobId } ` ,
2020-05-03 20:16:44 +00:00
} ,
2020-05-03 21:14:19 +00:00
img ( { src : ` /image/256/ ${ encodedBlobId } ` } )
2020-05-03 20:16:44 +00:00
) ,
a (
{
href : ` /thread/ ${ encodedMsgId } # ${ encodedMsgId } ` ,
2020-05-03 21:14:19 +00:00
class : "result-text" ,
2020-05-03 20:16:44 +00:00
} ,
info . name
2020-05-03 21:14:19 +00:00
) ,
2020-05-03 20:16:44 +00:00
]
) ;
2020-05-03 21:14:19 +00:00
} ;
2020-05-03 20:16:44 +00:00
exports . imageSearchView = ( { blobs , query } ) => {
const searchInput = input ( {
name : "query" ,
required : false ,
type : "search" ,
value : query ,
} ) ;
// - 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-05-12 13:26:08 +00:00
i18n . imageSearch ,
2020-05-03 20:16:44 +00:00
section (
2020-05-03 21:02:44 +00:00
h1 ( i18n . imageSearch ) ,
2020-05-03 20:16:44 +00:00
form (
{ action : "/imageSearch" , method : "get" } ,
2020-05-03 21:02:44 +00:00
label ( i18n . imageSearchLabel , searchInput ) ,
2020-05-03 20:16:44 +00:00
button (
{
type : "submit" ,
} ,
i18n . submit
)
)
) ,
div (
{
2020-05-03 21:14:19 +00:00
class : "image-search-grid" ,
2020-05-03 20:16:44 +00:00
} ,
Object . keys ( blobs )
// todo: add pagination
. slice ( 0 , 30 )
2020-05-03 21:14:19 +00:00
. map ( ( blobId ) => imageResult ( { id : blobId , infos : blobs [ blobId ] } ) )
2020-05-03 20:16:44 +00:00
)
) ;
} ;
2020-02-27 20:09:37 +00:00
exports . hashtagView = ( { messages , hashtag } ) => {
return template (
2020-05-11 14:40:51 +00:00
` # ${ hashtag } ` ,
2020-02-27 20:09:37 +00:00
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 ;
} ;