Path: blob/main/internal/window/login/component.go
450 views
package login12import (3"context"4"strings"56"github.com/diamondburned/adaptive"7"github.com/diamondburned/arikawa/v3/session"8"github.com/diamondburned/chatkit/components/secretdialog"9"github.com/diamondburned/chatkit/kits/secret"10"github.com/diamondburned/gotk4/pkg/gtk/v4"11"github.com/diamondburned/gotkit/app"12"github.com/diamondburned/gotkit/gtkutil"13"github.com/diamondburned/gotkit/gtkutil/cssutil"14"github.com/pkg/errors"15"libdb.so/dissent/internal/window/login/loading"16)1718// LoginComponent is the main component in the login page.19type Component struct {20*gtk.Box21Inner *gtk.Box2223Loading *loading.PulsatingBar24Methods *Methods25Bottom *gtk.Box26Remember *rememberMeBox27ErrorRev *gtk.Revealer28LogIn *gtk.Button2930ctx context.Context31page *Page32}3334var componentCSS = cssutil.Applier("login-component", `35.login-component {36background: mix(@theme_bg_color, @theme_fg_color, 0.05);37border-radius: 12px;38min-width: 250px;39margin: 12px;40padding: 0;41}42.login-component > *:not(.osd) {43margin: 0 8px;44}45.login-component > *:nth-child(2) {46margin-top: 6px;47}48.login-component > *:first-child {49margin-top: 8px;50}51.login-component > *:not(:first-child) {52margin-bottom: 4px;53}54.login-component > *:last-child {55margin-bottom: 8px;56}57.login-component > notebook {58background: none;59}60.login-component .adaptive-errorlabel {61margin-bottom: 8px;62}63.login-button {64background-color: #7289DA;65color: #FFFFFF;66}67.login-with {68font-weight: bold;69margin-bottom: 2px;70}71.login-decrypt-button {72margin-left: 4px;73}74`)7576const decryptMsg = `You've previously chosen to remember the token and may have77used a password to encrypt it. This button unlocks that encrypted token and logs78in using it.`7980// NewComponent creates a new login Component.81func NewComponent(ctx context.Context, p *Page) *Component {82c := Component{83ctx: ctx,84page: p,85}8687c.Loading = loading.NewPulsatingBar(loading.PulseFast | loading.PulseBarOSD)8889loginWith := gtk.NewLabel("Login using:")90loginWith.AddCSSClass("login-with")91loginWith.SetXAlign(0)9293c.Methods = NewMethods(&c)9495c.Remember = newRememberMeBox(ctx)9697c.ErrorRev = gtk.NewRevealer()98c.ErrorRev.SetTransitionType(gtk.RevealerTransitionTypeSlideDown)99c.ErrorRev.SetRevealChild(false)100101c.LogIn = gtk.NewButtonWithLabel("Log In")102c.LogIn.AddCSSClass("suggested-action")103c.LogIn.AddCSSClass("login-button")104c.LogIn.SetHExpand(true)105c.LogIn.ConnectClicked(c.login)106107decrypt := gtk.NewButtonWithLabel("Decrypt (?)")108decrypt.AddCSSClass("login-decrypt-button")109decrypt.SetSensitive(false)110decrypt.SetTooltipText(strings.ReplaceAll(decryptMsg, "\n", " "))111decrypt.ConnectClicked(c.askDecrypt)112113buttonBox := gtk.NewBox(gtk.OrientationHorizontal, 0)114buttonBox.Append(c.LogIn)115buttonBox.Append(decrypt)116117gtkutil.Async(ctx, func() func() {118if secret.IsEncrypted(ctx) {119return func() { decrypt.SetSensitive(true) }120} else {121return func() { decrypt.Hide() }122}123})124125c.Inner = gtk.NewBox(gtk.OrientationVertical, 0)126c.Inner.Append(loginWith)127c.Inner.Append(c.Methods)128c.Inner.Append(c.Remember)129c.Inner.Append(c.ErrorRev)130c.Inner.Append(buttonBox)131componentCSS(c.Inner)132133c.Box = gtk.NewBox(gtk.OrientationVertical, 0)134c.Box.AddCSSClass("login-component-outer")135c.Box.SetHAlign(gtk.AlignCenter)136c.Box.SetVAlign(gtk.AlignCenter)137c.Box.Append(c.Loading)138c.Box.Append(c.Inner)139140return &c141}142143// ShowError reveals the error label and shows it to the user.144func (c *Component) ShowError(err error) {145errLabel := adaptive.NewErrorLabel(err)146c.ErrorRev.SetChild(errLabel)147c.ErrorRev.SetRevealChild(true)148}149150// HideError hides the error label.151func (c *Component) HideError() {152c.ErrorRev.SetRevealChild(false)153}154155// Login presses the Login button.156func (c *Component) Login() {157c.LogIn.Activate()158}159160func (c *Component) login() {161switch {162case c.Methods.IsEmail():163c.loginEmail(164c.Methods.Email.Email.Text(),165c.Methods.Email.Password.Text(),166c.Methods.Email.TOTP.Text(),167)168case c.Methods.IsToken():169c.loginToken(170c.Methods.Token.Token.Text(),171)172}173}174175func (c *Component) SetBusy() {176c.SetSensitive(false)177c.Loading.Show()178}179180func (c *Component) SetDone() {181c.SetSensitive(true)182c.Loading.Hide()183}184185func (c *Component) loginEmail(email, password, totp string) {186c.SetBusy()187188gtkutil.Async(c.ctx, func() func() {189u, err := session.Login(c.ctx, email, password, totp)190if err != nil {191return func() {192c.ShowError(errors.Wrap(err, "cannot login"))193c.SetDone()194}195}196197return func() {198c.loginToken(u.Token)199c.SetDone()200}201})202}203204func (c *Component) loginToken(token string) {205go func() {206driver := c.Remember.SecretDriver()207if driver == nil {208return209}210211if err := driver.Set("account", []byte(token)); err != nil {212app.Error(c.ctx, errors.Wrap(err, "cannot store account as secret"))213}214}()215216c.page.asyncUseToken(token)217}218219func (c *Component) askDecrypt() {220secretdialog.PromptPassword(221c.ctx, secretdialog.PromptDecrypt,222func(ok bool, enc *secret.EncryptedFile) {223if ok {224c.page.asyncLoadFromSecrets(enc)225}226},227)228}229230// Methods is the notebook containing entries for different login methods.231type Methods struct {232*gtk.Notebook233Email struct { // Username and Password234*gtk.Box235Email *FormEntry236Password *FormEntry237TOTP *FormEntry238}239Token struct { // Token240*gtk.Box241Token *FormEntry242}243}244245var methodsCSS = cssutil.Applier("login-methods", `246.login-methods > * {247margin: 0;248}249.login-methods > header > tabs > tab {250min-width: 0;251padding-left: 8px;252padding-right: 8px;253}254.login-methods > stack {255padding: 0 4px;256}257.login-methods .login-formentry {258margin-top: 8px;259}260.login-methods header tab:checked {261background-color: @accent_color;262}263.login-form-2fa {264margin-left: 6px;265}266.login-form-2fa entry {267font-family: monospace;268}269`)270271// NewMethods creates a new Methods widget.272func NewMethods(c *Component) *Methods {273m := Methods{}274275m.Email.Email = NewFormEntry("Email")276m.Email.Email.AddCSSClass("login-form-email")277m.Email.Email.FocusNextOnActivate()278m.Email.Email.Entry.SetInputPurpose(gtk.InputPurposeEmail)279280m.Email.Password = NewFormEntry("Password")281m.Email.Password.AddCSSClass("login-form-password")282m.Email.Password.SetHExpand(true)283m.Email.Password.FocusNextOnActivate()284m.Email.Password.Entry.SetInputPurpose(gtk.InputPurposePassword)285m.Email.Password.Entry.SetVisibility(false)286287m.Email.TOTP = NewFormEntry("TOTP")288m.Email.TOTP.AddCSSClass("login-form-2fa")289m.Email.TOTP.ConnectActivate(c.Login)290m.Email.TOTP.Entry.SetInputPurpose(gtk.InputPurposePIN)291m.Email.TOTP.Entry.SetPlaceholderText("000000")292m.Email.TOTP.Entry.SetMaxLength(6)293m.Email.TOTP.Entry.SetWidthChars(6)294295// Hack to collapse the TOTP entry.296if text, ok := m.Email.TOTP.Entry.FirstChild().(*gtk.Text); ok {297text.SetPropagateTextWidth(true)298}299300// [ 0 | 1 | 2 | 3 ]301// 0 [ Email ]302// 1 [ Password ][TOTP]303passwordBox := gtk.NewBox(gtk.OrientationHorizontal, 0)304passwordBox.Append(m.Email.Password)305passwordBox.Append(m.Email.TOTP)306307m.Email.Box = gtk.NewBox(gtk.OrientationVertical, 0)308m.Email.Append(m.Email.Email)309m.Email.Append(passwordBox)310311m.Token.Token = NewFormEntry("Token")312m.Token.Token.AddCSSClass("login-form-token")313m.Token.Token.ConnectActivate(c.Login)314m.Token.Token.Entry.SetInputPurpose(gtk.InputPurposePassword)315m.Token.Token.Entry.SetVisibility(false)316317m.Token.Box = gtk.NewBox(gtk.OrientationVertical, 0)318m.Token.SetVAlign(gtk.AlignStart)319m.Token.Append(m.Token.Token)320321m.Notebook = gtk.NewNotebook()322m.Notebook.SetShowBorder(false)323m.Notebook.AppendPage(m.Token, gtk.NewLabel("Token"))324m.Notebook.AppendPage(m.Email, gtk.NewLabel("Email"))325m.Notebook.SetCurrentPage(0)326327if stack, ok := m.Notebook.LastChild().(*gtk.Stack); ok {328stack.SetTransitionType(gtk.StackTransitionTypeSlideLeftRight)329}330331methodsCSS(m)332return &m333}334335func (m *Methods) IsToken() bool { return m.CurrentPage() == 0 }336func (m *Methods) IsEmail() bool { return m.CurrentPage() == 1 }337338// FormEntry is a widget containing a label and an entry.339type FormEntry struct {340*gtk.Box341Label *gtk.Label342Entry *gtk.Entry343}344345var formEntryCSS = cssutil.Applier("login-formentry", ``)346347// NewFormEntry creates a new FormEntry.348func NewFormEntry(label string) *FormEntry {349e := FormEntry{}350e.Label = gtk.NewLabel(label)351e.Label.SetXAlign(0)352353e.Entry = gtk.NewEntry()354e.Entry.SetVExpand(true)355e.Entry.SetHasFrame(true)356357e.Box = gtk.NewBox(gtk.OrientationVertical, 0)358e.Box.Append(e.Label)359e.Box.Append(e.Entry)360formEntryCSS(e)361362return &e363}364365// Text gets the value entry.366func (e *FormEntry) Text() string { return e.Entry.Text() }367368// FocusNext navigates to the next widget.369func (e *FormEntry) FocusNext() {370e.Entry.Emit("move-focus", gtk.DirTabForward)371}372373// FocusNextOnActivate binds Enter to navigate to the next widget when it's374// pressed.375func (e *FormEntry) FocusNextOnActivate() {376e.Entry.ConnectActivate(e.FocusNext)377}378379// ConnectActivate connects the activate signal hanlder to the Entry.380func (e *FormEntry) ConnectActivate(f func()) {381e.Entry.ConnectActivate(f)382}383384385