2023-01-20 19:16:18 -05:00
import { LitElement , html , unsafeHTML } from './lit-all.min.js' ;
2022-09-06 19:26:43 -04:00
import * as tfutils from './tf-utils.js' ;
import * as tfrpc from '/static/tfrpc.js' ;
2022-10-15 14:22:13 -04:00
import { styles } from './tf-styles.js' ;
import Tribute from './tribute.esm.js' ;
2022-09-06 19:26:43 -04:00
class TfComposeElement extends LitElement {
static get properties ( ) {
return {
whoami : { type : String } ,
users : { type : Object } ,
root : { type : String } ,
branch : { type : String } ,
2022-10-18 19:00:57 -04:00
apps : { type : Object } ,
2023-01-20 19:16:18 -05:00
drafts : { type : Object } ,
2023-03-29 18:02:12 -04:00
} ;
2022-09-06 19:26:43 -04:00
}
2022-10-15 14:22:13 -04:00
static styles = styles ;
2022-09-06 19:26:43 -04:00
constructor ( ) {
super ( ) ;
this . users = { } ;
this . root = undefined ;
this . branch = undefined ;
2022-10-18 19:00:57 -04:00
this . apps = undefined ;
2023-01-20 19:16:18 -05:00
this . drafts = { } ;
2022-09-06 19:26:43 -04:00
}
2023-01-20 19:16:18 -05:00
process _text ( text ) {
if ( ! text ) {
return '' ;
}
2022-10-16 15:05:22 -04:00
/* Update mentions. */
2023-01-29 20:45:23 -05:00
let draft = this . get _draft ( ) ;
let updated = false ;
2022-10-16 15:05:22 -04:00
for ( let match of text . matchAll ( /\[([^\[]+)]\(([@&%][^\)]+)/g ) ) {
let name = match [ 1 ] ;
let link = match [ 2 ] ;
let balance = 0 ;
let bracket _end = match . index + match [ 1 ] . length + '[]' . length - 1 ;
for ( let i = bracket _end ; i >= 0 ; i -- ) {
if ( text . charAt ( i ) == ']' ) {
balance ++ ;
} else if ( text . charAt ( i ) == '[' ) {
balance -- ;
}
if ( balance <= 0 ) {
name = text . substring ( i + 1 , bracket _end ) ;
break ;
}
}
2023-01-29 20:45:23 -05:00
if ( ! draft . mentions ) {
draft . mentions = { } ;
}
if ( ! draft . mentions [ link ] ) {
draft . mentions [ link ] = {
2022-10-16 15:05:22 -04:00
link : link ,
2023-03-29 18:02:12 -04:00
} ;
2022-10-16 15:05:22 -04:00
}
2023-01-29 20:45:23 -05:00
draft . mentions [ link ] . name = name . startsWith ( '@' ) ? name . substring ( 1 ) : name ;
updated = true ;
}
if ( updated ) {
this . requestUpdate ( ) ;
2022-10-16 15:05:22 -04:00
}
2023-01-20 19:16:18 -05:00
return tfutils . markdown ( text ) ;
}
2022-10-16 15:05:22 -04:00
2023-01-20 20:39:00 -05:00
input ( event ) {
2023-01-20 19:16:18 -05:00
let edit = this . renderRoot . getElementById ( 'edit' ) ;
let preview = this . renderRoot . getElementById ( 'preview' ) ;
preview . innerHTML = this . process _text ( edit . value ) ;
2023-01-29 20:45:23 -05:00
let content _warning = this . renderRoot . getElementById ( 'content_warning' ) ;
let content _warning _preview = this . renderRoot . getElementById ( 'content_warning_preview' ) ;
if ( content _warning && content _warning _preview ) {
content _warning _preview . innerText = content _warning . value ;
}
2023-01-20 20:39:00 -05:00
}
2023-01-28 14:39:41 -05:00
notify ( draft ) {
this . dispatchEvent ( new CustomEvent ( 'tf-draft' , {
bubbles : true ,
composed : true ,
detail : {
id : this . branch ,
draft : draft
} ,
} ) ) ;
}
2023-01-29 20:45:23 -05:00
change ( ) {
let draft = this . get _draft ( ) ;
draft . text = this . renderRoot . getElementById ( 'edit' ) ? . value ;
draft . content _warning = this . renderRoot . getElementById ( 'content_warning' ) ? . value ;
this . notify ( draft ) ;
2022-10-16 15:05:22 -04:00
}
2023-01-14 17:27:35 -05:00
convert _to _format ( buffer , type , mime _type ) {
2022-10-16 16:31:32 -04:00
return new Promise ( function ( resolve , reject ) {
let img = new Image ( ) ;
img . onload = function ( ) {
let canvas = document . createElement ( 'canvas' ) ;
2022-12-09 19:35:53 -05:00
let width _scale = Math . min ( img . width , 1024 ) / img . width ;
let height _scale = Math . min ( img . height , 1024 ) / img . height ;
let scale = Math . min ( width _scale , height _scale ) ;
canvas . width = img . width * scale ;
canvas . height = img . height * scale ;
2022-10-16 16:31:32 -04:00
let context = canvas . getContext ( '2d' ) ;
2022-12-09 19:35:53 -05:00
context . drawImage ( img , 0 , 0 , canvas . width , canvas . height ) ;
2023-01-14 17:27:35 -05:00
let data _url = canvas . toDataURL ( mime _type ) ;
2022-10-16 16:31:32 -04:00
let result = atob ( data _url . split ( ',' ) [ 1 ] ) . split ( '' ) . map ( x => x . charCodeAt ( 0 ) ) ;
resolve ( result ) ;
2023-03-29 18:02:12 -04:00
} ;
2022-10-16 16:31:32 -04:00
img . onerror = function ( event ) {
reject ( new Error ( 'Failed to load image.' ) ) ;
} ;
let raw = Array . from ( new Uint8Array ( buffer ) ) . map ( b => String . fromCharCode ( b ) ) . join ( '' ) ;
let original = ` data: ${ type } ;base64, ${ btoa ( raw ) } ` ;
img . src = original ;
} ) ;
}
2022-11-29 21:37:27 -05:00
async add _file ( file ) {
try {
2023-01-29 20:45:23 -05:00
let draft = this . get _draft ( ) ;
2022-11-29 21:37:27 -05:00
let self = this ;
let buffer = await file . arrayBuffer ( ) ;
let type = file . type ;
if ( type . startsWith ( 'image/' ) ) {
2023-01-14 17:27:35 -05:00
let best _buffer ;
let best _type ;
2023-01-17 18:10:17 -05:00
for ( let format of [ 'image/png' , 'image/jpeg' , 'image/webp' ] ) {
2023-01-14 17:27:35 -05:00
let test _buffer = await self . convert _to _format ( buffer , file . type , format ) ;
if ( ! best _buffer || test _buffer . length < best _buffer . length ) {
best _buffer = test _buffer ;
best _type = format ;
}
}
buffer = best _buffer ;
type = best _type ;
2022-11-29 21:37:27 -05:00
} else {
buffer = Array . from ( new Uint8Array ( buffer ) ) ;
}
let id = await tfrpc . rpc . store _blob ( buffer ) ;
let name = type . split ( '/' ) [ 0 ] + ':' + file . name ;
2023-01-29 20:45:23 -05:00
if ( ! draft . mentions ) {
draft . mentions = { } ;
}
draft . mentions [ id ] = {
2022-10-16 15:05:22 -04:00
link : id ,
2022-11-29 21:37:27 -05:00
name : name ,
type : type ,
size : buffer . length ? ? buffer . byteLength ,
2022-10-16 15:05:22 -04:00
} ;
let edit = self . renderRoot . getElementById ( 'edit' ) ;
2022-11-29 21:37:27 -05:00
edit . value += ` \n ![ ${ name } ]( ${ id } ) ` ;
2023-01-20 20:39:00 -05:00
self . change ( ) ;
2023-01-29 20:45:23 -05:00
self . input ( ) ;
2022-11-29 21:37:27 -05:00
} catch ( e ) {
2022-10-16 16:31:32 -04:00
alert ( e ? . message ) ;
2022-11-29 21:37:27 -05:00
}
2022-09-06 19:26:43 -04:00
}
2022-10-04 21:40:21 -04:00
paste ( event ) {
let self = this ;
2022-10-16 16:31:32 -04:00
for ( let item of event . clipboardData . items ) {
2022-10-04 21:40:21 -04:00
if ( item . type ? . startsWith ( 'image/' ) ) {
let file = item . getAsFile ( ) ;
if ( ! file ) {
continue ;
}
2022-10-16 15:05:22 -04:00
self . add _file ( file ) ;
2022-10-04 21:40:21 -04:00
break ;
}
}
}
2023-08-31 21:34:01 -04:00
async submit ( ) {
2022-09-06 19:26:43 -04:00
let self = this ;
2023-01-29 20:45:23 -05:00
let draft = this . get _draft ( ) ;
2022-09-06 19:26:43 -04:00
let edit = this . renderRoot . getElementById ( 'edit' ) ;
let message = {
type : 'post' ,
text : edit . value ,
} ;
if ( this . root || this . branch ) {
message . root = this . root ;
message . branch = this . branch ;
}
2023-01-29 20:45:23 -05:00
if ( Object . values ( draft . mentions || { } ) . length ) {
message . mentions = Object . values ( draft . mentions ) ;
}
if ( draft . content _warning !== undefined ) {
message . contentWarning = draft . content _warning ;
2022-10-15 15:02:09 -04:00
}
2022-09-06 19:26:43 -04:00
console . log ( 'Would post:' , message ) ;
2023-08-31 21:34:01 -04:00
if ( draft . encrypt _to ) {
let to = new Set ( draft . encrypt _to ) ;
to . add ( this . whoami ) ;
to = [ ... to ] ;
2023-09-02 09:25:31 -04:00
message . recps = to ;
console . log ( 'message is now' , message ) ;
2023-08-31 21:34:01 -04:00
message = await tfrpc . rpc . encrypt ( this . whoami , to , JSON . stringify ( message ) ) ;
console . log ( 'encrypted as' , message ) ;
}
try {
await tfrpc . rpc . appendMessage ( this . whoami , message ) . then ( function ( ) {
edit . value = '' ;
self . change ( ) ;
self . notify ( undefined ) ;
self . requestUpdate ( ) ;
} ) ;
} catch ( error ) {
2022-09-06 19:26:43 -04:00
alert ( error . message ) ;
2023-08-31 21:34:01 -04:00
}
2022-09-06 19:26:43 -04:00
}
discard ( ) {
let edit = this . renderRoot . getElementById ( 'edit' ) ;
edit . value = '' ;
2023-01-20 20:39:00 -05:00
this . change ( ) ;
2023-01-28 14:39:41 -05:00
let preview = this . renderRoot . getElementById ( 'preview' ) ;
preview . innerHTML = '' ;
this . notify ( undefined ) ;
2022-09-06 19:26:43 -04:00
}
attach ( ) {
let self = this ;
let edit = this . renderRoot . getElementById ( 'edit' ) ;
let input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . onchange = function ( event ) {
let file = event . target . files [ 0 ] ;
2022-10-16 15:05:22 -04:00
self . add _file ( file ) ;
2022-09-06 19:26:43 -04:00
} ;
input . click ( ) ;
}
2023-09-20 20:26:29 -04:00
async autocomplete ( text , callback ) {
this . last _autocomplete = text ;
let results = [ ] ;
try {
let rows = await tfrpc . rpc . query ( `
SELECT messages . content FROM messages _fts ( ? )
JOIN messages ON messages . rowid = messages _fts . rowid
WHERE messages . content LIKE ?
ORDER BY timestamp DESC LIMIT 10
` , ['"' + text.replace('"', '""') + '"', ` % ! [ % $ { text } % ] ( % ) % ` ]);
for ( let row of rows ) {
for ( let match of row . content . matchAll ( /!\[([^\]]*)\]\((&.*?)\)/g ) ) {
2023-09-20 20:40:47 -04:00
if ( match [ 1 ] . toLowerCase ( ) . indexOf ( text . toLowerCase ( ) ) != - 1 ) {
2023-09-20 20:26:29 -04:00
results . push ( { key : match [ 1 ] , value : match [ 2 ] } ) ;
}
}
}
} finally {
if ( this . last _autocomplete === text ) {
callback ( results ) ;
}
}
}
2022-10-15 14:22:13 -04:00
firstUpdated ( ) {
let tribute = new Tribute ( {
2023-09-20 20:26:29 -04:00
collection : [
{
values : Object . entries ( this . users ) . map ( x => ( { key : x [ 1 ] . name , value : x [ 0 ] } ) ) ,
selectTemplate : function ( item ) {
return ` [@ ${ item . original . key } ]( ${ item . original . value } ) ` ;
} ,
} ,
{
trigger : '&' ,
values : this . autocomplete ,
selectTemplate : function ( item ) {
return ` ![ ${ item . original . key } ]( ${ item . original . value } ) ` ;
} ,
} ,
] ,
2022-10-15 14:22:13 -04:00
} ) ;
tribute . attach ( this . renderRoot . getElementById ( 'edit' ) ) ;
}
2023-01-29 20:45:23 -05:00
updated ( ) {
super . updated ( ) ;
let edit = this . renderRoot . getElementById ( 'edit' ) ;
if ( this . last _updated _text !== edit . value ) {
let preview = this . renderRoot . getElementById ( 'preview' ) ;
preview . innerHTML = this . process _text ( edit . value ) ;
this . last _updated _text = edit . value ;
}
2023-08-31 21:34:01 -04:00
let encrypt = this . renderRoot . getElementById ( 'encrypt_to' ) ;
if ( encrypt ) {
let tribute = new Tribute ( {
values : Object . entries ( this . users ) . map ( x => ( { key : x [ 1 ] . name , value : x [ 0 ] } ) ) ,
selectTemplate : function ( item ) {
return item . original . value ;
} ,
} ) ;
tribute . attach ( encrypt ) ;
}
2023-01-29 20:45:23 -05:00
}
2022-10-16 15:05:22 -04:00
remove _mention ( id ) {
2023-01-29 20:45:23 -05:00
let draft = this . get _draft ( ) ;
delete draft . mentions [ id ] ;
this . notify ( draft ) ;
this . requestUpdate ( ) ;
2022-10-16 15:05:22 -04:00
}
render _mention ( mention ) {
let self = this ;
return html `
2023-05-02 12:47:27 -04:00
< div style = "display: flex; flex-direction: row" >
< div style = "align-self: center; margin: 0.5em" >
< input type = "button" value = "🚮" title = "Remove ${mention.name} mention" @ click = $ { ( ) => self . remove _mention ( mention . link ) } > < / i n p u t >
< / d i v >
< div style = "display: flex; flex-direction: column" >
< h3 > $ { mention . name } < / h 3 >
< div style = "padding-left: 1em" >
$ { Object . entries ( mention )
. filter ( x => x [ 0 ] != 'name' )
. map ( x => html ` <div><span style="font-weight: bold"> ${ x [ 0 ] } </span>: ${ x [ 1 ] } </div> ` ) }
< / d i v >
< / d i v >
2022-10-16 15:05:22 -04:00
< / d i v > ` ;
}
2022-10-18 19:00:57 -04:00
render _attach _app ( ) {
let self = this ;
async function attach _selected _app ( ) {
let name = self . renderRoot . getElementById ( 'select' ) . value ;
let id = self . apps [ name ] ;
let mentions = { } ;
mentions [ id ] = {
name : name ,
link : id ,
type : 'application/tildefriends' ,
} ;
if ( name && id ) {
let app = JSON . parse ( await tfrpc . rpc . get _blob ( id ) ) ;
for ( let entry of Object . entries ( app . files ) ) {
mentions [ entry [ 1 ] ] = {
name : entry [ 0 ] ,
link : entry [ 1 ] ,
} ;
}
}
2023-03-29 18:02:12 -04:00
let draft = self . get _draft ( ) ;
2023-01-29 20:45:23 -05:00
draft . mentions = Object . assign ( draft . mentions || { } , mentions ) ;
2023-03-29 18:02:12 -04:00
self . requestUpdate ( ) ;
self . notify ( draft ) ;
self . apps = null ;
2022-10-18 19:00:57 -04:00
}
if ( this . apps ) {
return html `
2024-01-11 23:23:31 -05:00
< div class = "w3-card-4 w3-margin w3-padding" >
< select id = "select" class = "w3-select w3-dark-grey" >
2022-10-18 19:00:57 -04:00
$ { Object . keys ( self . apps ) . map ( app => html ` <option value= ${ app } > ${ app } </option> ` ) }
< / s e l e c t >
2024-01-11 23:23:31 -05:00
< button class = "w3-button w3-dark-grey" @ click = $ { attach _selected _app } > Attach < / b u t t o n >
< button class = "w3-button w3-dark-grey" @ click = $ { ( ) => this . apps = null } > Cancel < / b u t t o n >
2022-10-18 19:00:57 -04:00
< / d i v >
` ;
}
}
render _attach _app _button ( ) {
2023-03-29 18:02:12 -04:00
let self = this ;
2022-10-18 19:00:57 -04:00
async function attach _app ( ) {
2023-03-29 18:02:12 -04:00
self . apps = await tfrpc . rpc . apps ( ) ;
2022-10-18 19:00:57 -04:00
}
if ( ! this . apps ) {
2024-01-11 23:23:31 -05:00
return html ` <button class="w3-button w3-dark-grey" @click= ${ attach _app } >Attach App</button> ` ;
2022-10-18 19:00:57 -04:00
} else {
2024-01-11 23:23:31 -05:00
return html ` <button class="w3-button w3-dark-grey" @click= ${ ( ) => this . apps = null } >Discard App</button> ` ;
2022-10-18 19:00:57 -04:00
}
}
2023-01-29 20:45:23 -05:00
set _content _warning ( value ) {
let draft = this . get _draft ( ) ;
draft . content _warning = value ;
this . notify ( draft ) ;
this . requestUpdate ( ) ;
}
render _content _warning ( ) {
let self = this ;
let draft = this . get _draft ( ) ;
if ( draft . content _warning !== undefined ) {
return html `
2024-01-11 23:23:31 -05:00
< div class = "w3-container w3-padding" >
< p >
< input type = "checkbox" class = "w3-check w3-dark-grey" id = "cw" @ change = $ { ( ) => self . set _content _warning ( undefined ) } checked = "checked" > < / i n p u t >
< label for = "cw" > CW < / l a b e l >
< / p >
< input type = "text" class = "w3-input w3-border w3-dark-grey" id = "content_warning" placeholder = "Enter a content warning here." @ input = $ { this . input } @ change = $ { this . change } value = $ { draft . content _warning } > < / i n p u t >
2023-01-29 20:45:23 -05:00
< / d i v >
` ;
} else {
return html `
2024-01-11 23:23:31 -05:00
< input type = "checkbox" class = "w3-check w3-dark-grey" id = "cw" @ change = $ { ( ) => self . set _content _warning ( '' ) } > < / i n p u t >
2023-01-29 20:45:23 -05:00
< label for = "cw" > CW < / l a b e l >
` ;
}
}
get _draft ( ) {
return this . drafts [ this . branch || '' ] || { } ;
}
2023-08-31 21:34:01 -04:00
update _encrypt ( event ) {
let input = event . srcElement ;
let matches = input . value . match ( /@.*?\.ed25519/g ) ;
if ( matches ) {
let draft = this . get _draft ( ) ;
let to = [ ... new Set ( matches . concat ( draft . encrypt _to ) ) ] ;
this . set _encrypt ( to ) ;
input . value = '' ;
}
}
render _encrypt ( ) {
let draft = this . get _draft ( ) ;
if ( draft . encrypt _to === undefined ) {
return ;
}
return html `
< div style = "display: flex; flex-direction: row; width: 100%" >
< label for = "encrypt_to" > 🔐 To : < / l a b e l >
< input type = "text" id = "encrypt_to" style = "display: flex; flex: 1 1" @ input = $ { this . update _encrypt } > < / i n p u t >
2024-01-11 23:23:31 -05:00
< button class = "w3-button w3-dark-grey" @ click = $ { ( ) => this . set _encrypt ( undefined ) } > 🚮 < / b u t t o n >
2023-08-31 21:34:01 -04:00
< / d i v >
< ul >
$ { draft . encrypt _to . map ( x => html `
< li >
< tf - user id = $ { x } . users = $ { this . users } > < / t f - u s e r >
2024-01-11 23:23:31 -05:00
< input type = "button" class = "w3-button w3-dark-grey" value = "🚮" @ click = $ { ( ) => this . set _encrypt ( draft . encrypt _to . filter ( id => id != x ) ) } > < / i n p u t >
2023-08-31 21:34:01 -04:00
< / l i > ` ) }
< / u l >
` ;
}
set _encrypt ( encrypt ) {
let draft = this . get _draft ( ) ;
draft . encrypt _to = encrypt ;
this . notify ( draft ) ;
this . requestUpdate ( ) ;
}
2022-09-06 19:26:43 -04:00
render ( ) {
2022-10-16 15:05:22 -04:00
let self = this ;
2023-01-29 20:45:23 -05:00
let draft = self . get _draft ( ) ;
let content _warning =
draft . content _warning !== undefined ?
2024-01-11 23:23:31 -05:00
html ` <div class="w3-panel w3-round-xlarge w3-blue">
< p id = "content_warning_preview" > $ { draft . content _warning } < / p >
< / d i v > ` :
2023-01-29 20:45:23 -05:00
undefined ;
2023-08-31 21:34:01 -04:00
let encrypt = draft . encrypt _to !== undefined ?
undefined :
2024-01-11 23:23:31 -05:00
html ` <button class="w3-button w3-dark-grey" @click= ${ ( ) => this . set _encrypt ( [ ] ) } >🔐</button> ` ;
2022-10-15 14:22:13 -04:00
let result = html `
2024-01-11 23:23:31 -05:00
< div class = "w3-card-4 w3-blue-grey w3-padding" style = "box-sizing: border-box" >
$ { this . render _encrypt ( ) }
< div style = "display: flex; flex-direction: row; width: 100%" >
< div style = "flex: 1 0 50%" class = "w3-padding" >
< textarea class = "w3-input w3-dark-grey w3-border" style = "resize: vertical" placeholder = "Write a post here." id = "edit" @ input = $ { this . input } @ change = $ { this . change } @ paste = $ { this . paste } > $ { draft . text } < / t e x t a r e a >
< / d i v >
< div style = "flex: 1 0 50%" class = "w3-padding" >
$ { content _warning }
< div id = "preview" > < / d i v >
< / d i v >
2023-01-29 20:45:23 -05:00
< / d i v >
2024-01-11 23:23:31 -05:00
$ { Object . values ( draft . mentions || { } ) . map ( x => self . render _mention ( x ) ) }
$ { this . render _attach _app ( ) }
$ { this . render _content _warning ( ) }
< button class = "w3-button w3-dark-grey" id = "submit" @ click = $ { this . submit } > Submit < / b u t t o n >
< button class = "w3-button w3-dark-grey" @ click = $ { this . attach } > Attach < / b u t t o n >
$ { this . render _attach _app _button ( ) }
$ { encrypt }
< button class = "w3-button w3-dark-grey" @ click = $ { this . discard } > Discard < / b u t t o n >
2022-09-06 19:26:43 -04:00
< / d i v >
` ;
2022-10-15 14:22:13 -04:00
return result ;
2022-09-06 19:26:43 -04:00
}
}
2023-08-02 20:30:48 -04:00
customElements . define ( 'tf-compose' , TfComposeElement ) ;