mirror of
https://git.mills.io/saltyim/saltyim.git
synced 2024-06-30 18:51:03 +00:00
![mlctrez](/assets/img/avatar_default.png)
Co-authored-by: mlctrez <mlctrez@gmail.com> Reviewed-on: https://git.mills.io/saltyim/saltyim/pulls/157 Reviewed-by: James Mills <james@mills.io> Co-authored-by: mlctrez <mlctrez@noreply@mills.io> Co-committed-by: mlctrez <mlctrez@noreply@mills.io>
250 lines
6.4 KiB
Go
250 lines
6.4 KiB
Go
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(identity.Addr(), 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")
|
|
}
|