@@ -43,9 +43,12 @@ function render() {
//
// The topic is the whole address — the server it is published to is the
// install's one ntfy, set in the deployment and not something a user picks.
function notifyStatus ( topic ) {
return topic ? ` Topic: ${ topic } ` : 'No topic set — pages go to the team’ s fallback topic.' ;
}
function notifyForm ( user ) {
const err = h ( 'p' , { class : 'form-error' , role : 'alert' , hidden : true } ) ;
const ok = h ( 'p' , { class : 'form-ok' , role : 'status' , hidden : true } ) ;
const topic = h ( 'input' , {
name : 'ntfy_topic' , type : 'text' , autocomplete : 'off' ,
autocapitalize : 'none' , spellcheck : false ,
@@ -54,29 +57,7 @@ function notifyForm(user) {
} ) ;
const submit = h ( 'button' , { class : 'btn btn-primary' , type : 'submit' , text : 'Save topic' } ) ;
// Only offered once a topic is saved: the test publishes to whatever the
// server has stored, not to whatever is half-typed in the field.
const test = h ( 'button' , {
class : 'btn' , type : 'button' , text : 'Send a test push' ,
hidden : ! user . ntfy _topic ,
onclick : async ( ) => {
err . hidden = true ;
ok . hidden = true ;
test . disabled = true ;
try {
await api . testNotification ( ) ;
ok . textContent = 'Sent. If nothing arrives, the topic is wrong or ntfy is not reachable.' ;
ok . hidden = false ;
} catch ( ex ) {
err . textContent = ex . message ;
err . hidden = false ;
} finally {
test . disabled = false ;
}
} ,
} ) ;
const form = h ( 'form' , { class : 'card pw-form' } ,
const form = h ( 'form' , { class : 'stacked-form' } ,
h ( 'label' , { } ,
h ( 'span' , { text : 'ntfy topic' } ) ,
topic ) ,
@@ -89,25 +70,46 @@ function notifyForm(user) {
h ( 'p' , { class : 'muted small' } ,
'Anyone who knows the topic can read your pages and publish to it, so ' ,
'pick something unguessable rather than your name.' ) ,
err , ok ,
h ( 'div' , { class : 'row-actions' } , submit , test ) ,
err ,
submit ,
) ;
const status = h ( 'p' , { class : 'muted' , text : notifyStatus ( user . ntfy _topic ) } ) ;
const summary = h ( 'summary' , { text : user . ntfy _topic ? 'Change topic' : 'Set a topic' } ) ;
const details = h ( 'details' , { class : 'account-fold' } , summary , form ) ;
// Only offered once a topic is saved: the test publishes to whatever the
// server has stored, not to whatever is half-typed in the field.
const test = h ( 'button' , {
class : 'btn' , type : 'button' , text : 'Send a test push' ,
hidden : ! user . ntfy _topic ,
onclick : async ( ) => {
test . disabled = true ;
try {
await api . testNotification ( ) ;
toast ( 'Sent. If nothing arrives, the topic is wrong or ntfy is not reachable.' ) ;
} catch ( ex ) {
toast ( ex . message , 'error' ) ;
} finally {
test . disabled = false ;
}
} ,
} ) ;
form . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
err . hidden = true ;
ok . hidden = true ;
submit . disabled = true ;
try {
const updated = await api . setNotifyTarget ( user . id , topic . value . trim ( ) ) ;
// Keep the cached user in step, so the onboarding checklist stops
// asking for this and the test button appears without a reload.
state . me . user = updated ;
ok . textContent = updated . ntfy _topic
? 'Topic saved.'
: 'Topic cleared. Your pages go to the team’ s fallback topic.' ;
ok . hidden = false ;
status . textContent = notifyStatus ( updated . ntfy _topic ) ;
summary . textContent = updated . ntfy _topic ? 'Change topic' : 'Set a topic' ;
test . hidden = ! updated . ntfy _topic ;
details . open = false ;
toast ( updated . ntfy _topic ? 'Topic saved' : 'Topic cleared. Your pages go to the team’ s fallback topic.' ) ;
} catch ( ex ) {
err . textContent = ex . message ;
err . hidden = false ;
@@ -115,7 +117,11 @@ function notifyForm(user) {
submit . disabled = false ;
}
} ) ;
return form ;
return h ( 'div' , { class : 'card pw-form' } ,
status ,
h ( 'div' , { class : 'row-actions' } , test ) ,
details ) ;
}
// With password login switched off a password opens nothing, so somebody who
@@ -131,14 +137,13 @@ function passwordSection(user, hasPassword) {
] ;
}
return [
h ( 'div' , { class : 'page-head' } , h ( 'h2' , { text : hasPassword ? 'Change password' : 'Set a p assword' } ) ) ,
h ( 'div' , { class : 'page-head' } , h ( 'h2' , { text : 'P assword' } ) ) ,
passwordForm ( user , hasPassword ) ,
] ;
}
function passwordForm ( user , hasPassword ) {
const err = h ( 'p' , { class : 'form-error' , role : 'alert' , hidden : true } ) ;
const ok = h ( 'p' , { class : 'form-ok' , role : 'status' , hidden : true } ) ;
const current = hasPassword
? h ( 'input' , { name : 'current' , type : 'password' , autocomplete : 'current-password' , required : true } )
: null ;
@@ -148,18 +153,24 @@ function passwordForm(user, hasPassword) {
// A hidden username field lets password managers file the new password
// under the right account.
const form = h ( 'form' , { class : 'card pw -form' , autocomplete : 'on' } ,
const form = h ( 'form' , { class : 'stacked -form' , autocomplete : 'on' } ,
h ( 'input' , { type : 'text' , name : 'username' , autocomplete : 'username' , value : user . username , hidden : true , readonly : true } ) ,
current && h ( 'label' , { } , h ( 'span' , { text : 'Current password' } ) , current ) ,
h ( 'label' , { } , h ( 'span' , { text : 'New password' } ) , next ) ,
h ( 'label' , { } , h ( 'span' , { text : 'Repeat new password' } ) , again ) ,
err , ok , submit ,
err , submit ,
) ;
const status = h ( 'p' , {
class : 'muted' ,
text : hasPassword ? 'Password set.' : 'No password set — sign-in needs one of the other methods.' ,
} ) ;
const summary = h ( 'summary' , { text : hasPassword ? 'Change password' : 'Set a password' } ) ;
const details = h ( 'details' , { class : 'account-fold' } , summary , form ) ;
form . addEventListener ( 'submit' , async ( e ) => {
e . preventDefault ( ) ;
err . hidden = true ;
ok . hidden = true ;
if ( next . value !== again . value ) {
err . textContent = 'The new passwords do not match.' ;
err . hidden = false ;
@@ -171,13 +182,14 @@ function passwordForm(user, hasPassword) {
state . me . has _password = true ;
form . reset ( ) ;
if ( ! current ) {
// From now on the form needs the current-password field.
// From now on the form needs the current-password field, and a fresh
// render already comes up with the fold closed.
render ( ) ;
toast ( 'Password saved' ) ;
return ;
}
ok . textContent = 'Password saved. Other devices have been signed out.' ;
ok . hidden = false ;
details . open = false ;
toast ( 'Password saved. Other devices have been signed out.' ) ;
} catch ( ex ) {
err . textContent = ex . message ;
err . hidden = false ;
@@ -185,7 +197,7 @@ function passwordForm(user, hasPassword) {
submit . disabled = false ;
}
} ) ;
return form ;
return h ( 'div' , { class : 'card pw-form' } , status , details ) ;
}
function shortcuts ( ) {