package components import ( "time" "github.com/maxence-charriere/go-app/v9/pkg/app" "github.com/mlctrez/goapp-mdc/pkg/base" "github.com/mlctrez/goapp-mdc/pkg/icon" "github.com/mlctrez/goapp-mdc/pkg/textfield" log "github.com/sirupsen/logrus" "go.mills.io/saltyim" "go.mills.io/saltyim/internal/pwa/storage" "go.yarn.social/lextwt" ) const ( descText = `salty.im is an open specification for a new Saltpack based e2e encrypted messaging protocol and platform for secure communications with a focus on privacy, security and being self-hosted.` saltyChatRecvMessageAction = "saltychat.recv-message.action" saltyChatSentMessageAction = "saltychat.sent-message.action" ) var client *saltyim.Client // SaltyChat ... type SaltyChat struct { app.Compo base.JsUtil isAppInstallable bool dialog *ModalDialog chatBox *ChatBox friend string chatInput *textfield.TextField } func (h *SaltyChat) init(ctx app.Context) { ctx.Page().SetTitle("Salty Chat") } func (h *SaltyChat) load(ctx app.Context) { ctx.Page().SetTitle("Salty Chat") ctx.Page().SetDescription(descText) } func (h *SaltyChat) OnPreRender(ctx app.Context) { h.init(ctx) h.load(ctx) } func (h *SaltyChat) OnResize(ctx app.Context) { h.ResizeContent() } func (h *SaltyChat) OnAppInstallChange(ctx app.Context) { h.isAppInstallable = ctx.IsAppInstallable() } func (h *SaltyChat) OnNav(ctx app.Context) { h.refreshMessages(ctx) } func (h *SaltyChat) refreshMessages(ctx app.Context) { if ctx.Page().URL().Fragment == "" { return } h.friend = ctx.Page().URL().Fragment h.chatBox.User = h.friend h.chatBox.UpdateMessages(ctx) } func (h *SaltyChat) OnMount(ctx app.Context) { h.isAppInstallable = ctx.IsAppInstallable() h.refreshMessages(ctx) if app.IsClient { h.connect(ctx) } else { log.Println("app not running as a client?") } ctx.Handle(saltyChatRecvMessageAction, h.incomingMessage) ctx.Handle(saltyChatSentMessageAction, h.outgoingMessage) } func (h *SaltyChat) connect(ctx app.Context) { log.Println("connect()") if client != nil { log.Println("already have a connected client") return } log.Println("creating new client") identity, err := GetIdentityFromState(ctx) if err != nil { h.dialog.ShowError("missing identity, please configure", err.Error()) return } state, err := GetStateFromState(ctx) if err != nil { log.Errorf("error loading state: %s", err) state = saltyim.NewState() } clientIdentity := saltyim.WithClientIdentity(saltyim.WithIdentityBytes(identity.Contents())) newClient, err := saltyim.NewClient(clientIdentity, saltyim.WithState(state)) if err != nil { h.dialog.ShowError("error setting up client", err.Error()) return } newClient.SetSend(&saltyim.ProxySend{SendEndpoint: app.Getenv("SendEndpoint")}) newClient.SetLookup(&saltyim.ProxyLookup{LookupEndpoint: app.Getenv("LookupEndpoint")}) client = newClient ctx.Async(func() { stateCh := time.NewTicker(time.Second * 20) inboxCh := client.Subscribe(ctx.Dispatcher().Context(), "", "", "") outboxCh := client.OutboxClient(nil).Subscribe(ctx.Dispatcher().Context(), "", "", "") for { select { case <-ctx.Done(): stateCh.Stop() return case msg := <-inboxCh: // passing both the message and the text in case we need the message key at some point ctx.NewActionWithValue(saltyChatRecvMessageAction, msg, app.T("text", msg.Text)) case msg := <-outboxCh: // passing both the message and the text in case we need the message key at some point ctx.NewActionWithValue(saltyChatSentMessageAction, msg, app.T("text", msg.Text)) case <-stateCh.C: if err := SetStateToState(ctx, client.State()); err != nil { log.WithError(err).Warn("error saving state") } } } }) } func (h *SaltyChat) incomingMessage(ctx app.Context, action app.Action) { messageText := action.Tags.Get("text") s, err := lextwt.ParseSalty(messageText) if err != nil { h.dialog.ShowError("incoming message error", err.Error()) return } switch s := s.(type) { case *lextwt.SaltyText: user := s.User.String() storage.ContactsLocalStorage(ctx).Add(user) storage.ConversationsLocalStorage(ctx, user).Append(messageText) // only update when incoming user's message is the active chat if h.friend == user { h.chatBox.UpdateMessages(ctx) } else { // TODO: Creates some initial content of the new chat // to give the user a change to Accept/Reject the contact. log.Printf("new incoming chat from %s", user) } } } func (h *SaltyChat) outgoingMessage(ctx app.Context, action app.Action) { messageText := action.Tags.Get("text") s, err := lextwt.ParseSalty(messageText) if err != nil { h.dialog.ShowError("outgoing message error", err.Error()) return } friend := "" switch s := s.(type) { case *lextwt.SaltyText: friend = s.User.String() storage.ContactsLocalStorage(ctx).Add(friend) storage.ConversationsLocalStorage(ctx, friend). Append(string(saltyim.PackMessageTime(client.Me(), s.LiteralText(), s.Timestamp))) } // only update when incoming user's message is the active chat if h.friend == friend { h.chatBox.UpdateMessages(ctx) } else { // TODO: Creates some initial content of the new chat // to give the user a change to Accept/Reject the contact. log.Printf("new incoming chat from %s", friend) } } func (h *SaltyChat) Render() app.UI { if h.chatBox == nil { h.chatBox = &ChatBox{} h.chatInput = &textfield.TextField{Id: "chat-input", Placeholder: "New Message"} h.dialog = &ModalDialog{} } h.chatBox.User = h.friend return PageBody( app.Div().ID("wrapper").Body( h.chatBox, app.Form().OnSubmit(h.handleSendMessage).Body( h.chatInput, icon.MISend.Button().ID("chat-send"), ), h.dialog, ), ) } func (h *SaltyChat) handleSendMessage(ctx app.Context, e app.Event) { e.PreventDefault() msg := h.chatInput.Value h.chatInput.Value = "" h.focusChatInput() if msg == "" || h.friend == "" { // nothing to send and no friend selected log.Printf("no send since msg = %q or Friend = %q", msg, h.friend) return } // determine current user to send message to and use client to send the message if client != nil { h.chatBox.User = h.friend ctx.Async(func() { if err := client.Send(h.friend, msg); err != nil { h.dialog.ShowError("error sending message", err.Error()) } }) } } func (h *SaltyChat) focusChatInput() { chatInputValue := h.JsUtil.JsValueAtPath(h.chatInput.Id + "-input") chatInputValue.Set("value", "") chatInputValue.Call("focus") }