Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Chat Browser for general chat #38

Merged
merged 2 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ func main() {
WithStatusController().
WithGameController().
WithDownloadController().
WithUserController()
WithUserController().
WithChatController()

app := app.New()
window := app.NewWindow(getApplicationTitle())
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
github.com/rs/zerolog v1.31.0
github.com/seternate/go-lanty v0.1.0-beta.0.20240607200813-712d640af65d
github.com/seternate/go-lanty v0.1.0-beta.0.20240809132204-3be3d1587bdd
golang.design/x/clipboard v0.7.0
)

Expand All @@ -26,6 +26,7 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gopherjs/gopherjs v1.17.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down
12 changes: 4 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25d
github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
Expand Down Expand Up @@ -254,14 +256,8 @@ github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seternate/go-lanty v0.1.0-beta h1:gpclCPCz5H2hmNeyHrLxVe1sP3d/5SzKSEzG+SHWgas=
github.com/seternate/go-lanty v0.1.0-beta/go.mod h1:1mxIN82SScfhtjQYzjUSFSz3ArrgOIsHBSjHl54BaMc=
github.com/seternate/go-lanty v0.1.0-beta.0.20240505140903-1ecd9b6d4b5d h1:Fy0j73ur2t4zuSomJBEDJjr71PxFC4SF0sbM/YN5t3k=
github.com/seternate/go-lanty v0.1.0-beta.0.20240505140903-1ecd9b6d4b5d/go.mod h1:1mxIN82SScfhtjQYzjUSFSz3ArrgOIsHBSjHl54BaMc=
github.com/seternate/go-lanty v0.1.0-beta.0.20240517150350-28bf44a66830 h1:ba5WOpu0ckeJivCbMrRHRg09vLA8pPSqI9WxbTzGGbU=
github.com/seternate/go-lanty v0.1.0-beta.0.20240517150350-28bf44a66830/go.mod h1:1mxIN82SScfhtjQYzjUSFSz3ArrgOIsHBSjHl54BaMc=
github.com/seternate/go-lanty v0.1.0-beta.0.20240607200813-712d640af65d h1:GHEWKLXg3IZs28cD9q7B8L9z1UnLCjS81uNK7g25Rnc=
github.com/seternate/go-lanty v0.1.0-beta.0.20240607200813-712d640af65d/go.mod h1:1mxIN82SScfhtjQYzjUSFSz3ArrgOIsHBSjHl54BaMc=
github.com/seternate/go-lanty v0.1.0-beta.0.20240809132204-3be3d1587bdd h1:vb22tGtb2yVepWmO/O6qHH6Zgqo8Ztd+PK47kg3xQCU=
github.com/seternate/go-lanty v0.1.0-beta.0.20240809132204-3be3d1587bdd/go.mod h1:QL66bV5xXhUysHVUI8VaX773dsvXJp0bDzrXWWq3IOs=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
Expand Down
114 changes: 114 additions & 0 deletions pkg/controller/chatcontroller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package controller

import (
"slices"
"sync"
"time"

"github.com/rs/zerolog/log"
"github.com/seternate/go-lanty/pkg/chat"
"github.com/seternate/go-lanty/pkg/util"
)

type ChatController struct {
parent *Controller
subscriber []chan chat.Message
ticker *time.Ticker
mutex sync.RWMutex
settingschanged chan struct{}
}

func NewChatController(parent *Controller) (controller *ChatController) {
controller = &ChatController{
parent: parent,
subscriber: make([]chan chat.Message, 50),
ticker: time.NewTicker(time.Second),
settingschanged: make(chan struct{}, 50),
}

controller.parent.Settings.Subscribe(controller.settingschanged)
controller.run()
return
}

func (controller *ChatController) SendTextMessage(message string) {
err := controller.parent.client.Chat.SendMessage(chat.NewTextMessage(controller.parent.User.GetUser(), message))
if err != nil {
log.Error().Err(err).Msg("error sending textmessage to server")
}
}

func (controller *ChatController) Subscribe(subscriber chan chat.Message) {
defer controller.mutex.Unlock()
controller.mutex.Lock()
controller.subscriber = append(controller.subscriber, subscriber)
}

func (controller *ChatController) Unsubscribe(subscriber chan chat.Message) {
defer controller.mutex.Unlock()
controller.mutex.Lock()
index := slices.Index(controller.subscriber, subscriber)
_ = slices.Delete(controller.subscriber, index, index+1)
}

func (controller *ChatController) run() {
controller.parent.WaitGroup().Add(2)
go controller.connectionWatcher()
go controller.messageReader()
}

func (controller *ChatController) connectionWatcher() {
defer func() { controller.parent.WaitGroup().Done(); controller.parent.client.Chat.Disconnect() }()
_, err := controller.parent.client.Chat.Connect()
if err != nil {
log.Error().Err(err).Msg("error connecting to chat")
} else {
log.Debug().Msg("successfully connected to chat")
}
for {
select {
case <-controller.parent.Context().Done():
log.Trace().Err(controller.parent.Context().Err()).Msg("exiting ChatController connectionWatcher()")
return
case <-controller.ticker.C:
if controller.parent.client.Chat.Error != nil {
log.Debug().Err(controller.parent.client.Chat.Error).Msg("trying to reconnect to chat due to error in chatservice")
err = controller.parent.client.Chat.Reconnect()
if err != nil {
log.Error().Err(err).Msg("error reconnecting to chat")
} else {
log.Debug().Msg("successfully reconnected to chat")
}
}
case <-controller.settingschanged:
log.Debug().Err(controller.parent.client.Chat.Error).Msg("trying to reconnect to chat due to a settings change")
err = controller.parent.client.Chat.Reconnect()
if err != nil {
log.Error().Err(err).Msg("error reconnecting to chat")
} else {
log.Debug().Msg("successfully reconnected to chat")
}
}
}
}

func (controller *ChatController) messageReader() {
defer controller.parent.WaitGroup().Done()
for {
select {
case <-controller.parent.Context().Done():
log.Trace().Err(controller.parent.Context().Err()).Msg("exiting ChatController messageReader()")
return
case message := <-controller.parent.client.Chat.Messages:
controller.notifySubcriber(message)
}
}
}

func (controller *ChatController) notifySubcriber(message chat.Message) {
defer controller.mutex.RUnlock()
controller.mutex.RLock()
for _, subscriber := range controller.subscriber {
util.ChannelWriteNonBlocking(subscriber, message)
}
}
6 changes: 6 additions & 0 deletions pkg/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Controller struct {
Download *DownloadController
Game *GameController
User *UserController
Chat *ChatController

settings *setting.Settings
client *api.Client
Expand Down Expand Up @@ -90,3 +91,8 @@ func (controller *Controller) WithUserController() *Controller {
controller.User = NewUserController(controller, 3*handler.UserStaleDuration/4)
return controller
}

func (controller *Controller) WithChatController() *Controller {
controller.Chat = NewChatController(controller)
return controller
}
126 changes: 126 additions & 0 deletions pkg/widget/chatbrowser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package widget

import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
fynetheme "fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/rs/zerolog/log"
"github.com/seternate/go-lanty-client/pkg/controller"
"github.com/seternate/go-lanty-client/pkg/theme"
)

type ChatBrowser struct {
widget.BaseWidget
controller *controller.Controller
messageboard *MessageBoard
scroll *ScrollWithState
messageentry *widget.Entry
sendbutton *widget.Button
newmessage chan struct{}
}

func NewChatBrowser(controller *controller.Controller) (chatbrowser *ChatBrowser) {
chatbrowser = &ChatBrowser{
controller: controller,
messageboard: NewMessageBoard(controller),
messageentry: widget.NewMultiLineEntry(),
newmessage: make(chan struct{}, 50),
sendbutton: widget.NewButtonWithIcon("", fynetheme.MailSendIcon(), nil),
}
chatbrowser.ExtendBaseWidget(chatbrowser)

chatbrowser.scroll = NewVScrollWithState(chatbrowser.messageboard)

chatbrowser.messageentry.SetMinRowsVisible(2)
chatbrowser.messageentry.Wrapping = fyne.TextWrapWord
chatbrowser.messageentry.SetPlaceHolder("Type your message here!")
chatbrowser.messageentry.OnSubmitted = func(s string) {
controller.Chat.SendTextMessage(s)
}

chatbrowser.sendbutton.OnTapped = func() {
controller.Chat.SendTextMessage(chatbrowser.messageentry.Text)
}

chatbrowser.messageboard.Subscribe(chatbrowser.newmessage)
chatbrowser.run()

return chatbrowser
}

func (widget *ChatBrowser) run() {
widget.controller.WaitGroup().Add(1)
go widget.messageUpdater()
}

func (widget *ChatBrowser) messageUpdater() {
defer widget.controller.WaitGroup().Done()
for {
select {
case <-widget.controller.Context().Done():
log.Trace().Msg("exiting chatbrowser messageUpdater()")
return
case <-widget.newmessage:
widget.Refresh()
if widget.scroll.IsNearBottom(0.80) {
widget.scroll.ScrollToBottom()
}
}
}
}

func (browser *ChatBrowser) CreateRenderer() fyne.WidgetRenderer {
return newChatBrowserRenderer(browser)
}

type chatBrowserRenderer struct {
widget *ChatBrowser
}

func newChatBrowserRenderer(widget *ChatBrowser) fyne.WidgetRenderer {
renderer := &chatBrowserRenderer{
widget: widget,
}
return renderer
}

func (renderer *chatBrowserRenderer) Objects() []fyne.CanvasObject {
objects := []fyne.CanvasObject{
renderer.widget.scroll,
renderer.widget.sendbutton,
renderer.widget.messageentry,
}
return objects
}

func (renderer *chatBrowserRenderer) Layout(size fyne.Size) {
bottomHeight := fyne.Max(renderer.widget.messageentry.MinSize().Height, renderer.widget.sendbutton.MinSize().Height)
renderer.widget.messageboard.Resize(fyne.NewSize(size.Width-2*theme.InnerPadding(), renderer.widget.messageboard.MinSize().Height))
renderer.widget.scroll.Move(fyne.NewPos(theme.InnerPadding(), theme.InnerPadding()))
renderer.widget.scroll.Resize(fyne.NewSize(size.Width-2*theme.InnerPadding(), size.Height-3*theme.InnerPadding()-bottomHeight))
renderer.widget.sendbutton.Resize(fyne.NewSize(bottomHeight, bottomHeight))
renderer.widget.sendbutton.Move(fyne.NewPos(size.Width-theme.InnerPadding()-renderer.widget.sendbutton.Size().Width, renderer.widget.scroll.Position().Y+renderer.widget.scroll.Size().Height+theme.InnerPadding()))
renderer.widget.messageentry.Move(fyne.NewPos(renderer.widget.scroll.Position().X, renderer.widget.scroll.Position().Y+renderer.widget.scroll.Size().Height+theme.InnerPadding()))
renderer.widget.messageentry.Resize(fyne.NewSize(size.Width-3*theme.InnerPadding()-renderer.widget.sendbutton.Size().Width, bottomHeight))

}

func (renderer *chatBrowserRenderer) MinSize() fyne.Size {
bottomHeight := fyne.Max(renderer.widget.messageentry.MinSize().Height, renderer.widget.sendbutton.MinSize().Height)
minWidth := fyne.Max(3*theme.InnerPadding()+renderer.widget.messageentry.MinSize().Width+renderer.widget.sendbutton.MinSize().Width, 2*theme.InnerPadding()+renderer.widget.scroll.MinSize().Width)
minHeight := 3*theme.InnerPadding() + bottomHeight
return fyne.NewSize(minWidth, minHeight)
}

func (renderer *chatBrowserRenderer) Refresh() {
renderer.widget.messageboard.Refresh()
renderer.widget.scroll.Refresh()
renderer.widget.messageentry.Refresh()
renderer.widget.sendbutton.Refresh()
//Without first messagetiles will be never be shown until "resize" event happens
//then all messagetile are shown correctly
canvas.Refresh(renderer.widget)
}

func (renderer *chatBrowserRenderer) Destroy() {}
Loading
Loading