diff --git a/matrix/matrix.go b/matrix/matrix.go index c41975e..264cff8 100644 --- a/matrix/matrix.go +++ b/matrix/matrix.go @@ -254,6 +254,47 @@ func (c *Container) HandleMessage(source EventSource, evt *gomatrix.Event) { } } +// HandleMembership is the event handler for the m.room.member state event. +func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { + isLeave := source&EventSourceLeave != 0 + isTimeline := source&EventSourceTimeline != 0 + isNonTimelineLeave := isLeave && !isTimeline + if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { + return + } else if evt.StateKey != nil && *evt.StateKey == c.config.UserID { + c.processOwnMembershipChange(evt) + } else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) { + // We don't care about other users' membership events in the initial sync or chats we've left. + return + } + + c.HandleMessage(source, evt) +} + +func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { + membership, _ := evt.Content["membership"].(string) + prevMembership := "leave" + if evt.Unsigned.PrevContent != nil { + prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) + } + debug.Printf("Processing own membership change: %s->%s in %s", prevMembership, membership, evt.RoomID) + if membership == prevMembership { + return + } + room := c.GetRoom(evt.RoomID) + switch membership { + case "join": + c.ui.MainView().AddRoom(room) + room.HasLeft = false + case "leave": + c.ui.MainView().RemoveRoom(room) + room.HasLeft = true + case "invite": + // TODO handle + debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID) + } +} + func (c *Container) parseReadReceipt(evt *gomatrix.Event) (largestTimestampEvent string) { var largestTimestamp int64 for eventID, rawContent := range evt.Content { @@ -368,63 +409,6 @@ func (c *Container) HandleTag(source EventSource, evt *gomatrix.Event) { mainView.UpdateTags(room) } -func (c *Container) processOwnMembershipChange(evt *gomatrix.Event) { - membership, _ := evt.Content["membership"].(string) - prevMembership := "leave" - if evt.Unsigned.PrevContent != nil { - prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) - } - debug.Printf("Processing own membership change: %s->%s in %s", prevMembership, membership, evt.RoomID) - if membership == prevMembership { - return - } - room := c.GetRoom(evt.RoomID) - switch membership { - case "join": - c.ui.MainView().AddRoom(room) - room.HasLeft = false - case "leave": - c.ui.MainView().RemoveRoom(room) - room.HasLeft = true - case "invite": - // TODO handle - debug.Printf("%s invited the user to %s", evt.Sender, evt.RoomID) - } -} - -// HandleMembership is the event handler for the m.room.member state event. -func (c *Container) HandleMembership(source EventSource, evt *gomatrix.Event) { - isLeave := source&EventSourceLeave != 0 - isTimeline := source&EventSourceTimeline != 0 - isNonTimelineLeave := isLeave && !isTimeline - if !c.config.AuthCache.InitialSyncDone && isNonTimelineLeave { - return - } else if evt.StateKey != nil && *evt.StateKey == c.config.UserID { - c.processOwnMembershipChange(evt) - } else if !isTimeline && (!c.config.AuthCache.InitialSyncDone || isLeave) { - // We don't care about other users' membership events in the initial sync or chats we've left. - return - } - - mainView := c.ui.MainView() - roomView := mainView.GetRoom(evt.RoomID) - if roomView == nil { - return - } - - message := mainView.ParseEvent(roomView, evt) - if message != nil { - roomView.AddMessage(message, ifc.AppendMessage) - roomView.MxRoom().LastReceivedMessage = message.Timestamp() - // We don't want notifications at startup. - if c.syncer.FirstSyncDone { - pushRules := c.PushRules().GetActions(roomView.MxRoom(), evt).Should() - mainView.NotifyMessage(roomView.MxRoom(), message, pushRules) - c.ui.Render() - } - } -} - // HandleTyping is the event handler for the m.typing event. func (c *Container) HandleTyping(source EventSource, evt *gomatrix.Event) { users := evt.Content["user_ids"].([]interface{}) diff --git a/ui/fuzzy-search-modal.go b/ui/fuzzy-search-modal.go new file mode 100644 index 0000000..4b77ca8 --- /dev/null +++ b/ui/fuzzy-search-modal.go @@ -0,0 +1,136 @@ +// gomuks - A terminal Matrix client written in Go. +// Copyright (C) 2018 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package ui + +import ( + "fmt" + "sort" + "strconv" + + "github.com/evidlo/fuzzysearch/fuzzy" + "maunium.net/go/gomuks/matrix/rooms" + "maunium.net/go/gomuks/ui/widget" + "maunium.net/go/tview" + "maunium.net/go/tcell" + "maunium.net/go/gomuks/debug" +) + +type FuzzySearchModal struct { + tview.Primitive + + search *tview.InputField + results *tview.TextView + + matches fuzzy.Ranks + selected int + + roomList []*rooms.Room + roomTitles []string + + parent *GomuksUI + mainView *MainView +} + +func NewFuzzySearchModal(mainView *MainView, width int, height int) *FuzzySearchModal { + fs := &FuzzySearchModal{ + parent: mainView.parent, + mainView: mainView, + } + + fs.InitList(mainView.rooms) + + fs.search = tview.NewInputField(). + SetLabel("Room: ") + fs.search. + SetChangedFunc(fs.changeHandler). + SetInputCapture(fs.keyHandler) + + fs.results = tview.NewTextView(). + SetDynamicColors(true). + SetRegions(true) + fs.results.SetBorderPadding(1, 0, 0, 0) + + // Flex widget containing input box and results + container := tview.NewFlex(). + SetDirection(tview.FlexRow). + AddItem(fs.search, 1, 0, true). + AddItem(fs.results, 0, 1, false) + container. + SetBorder(true). + SetBorderPadding(1, 1, 1, 1). + SetTitle("Quick Room Switcher") + + fs.Primitive = widget.TransparentCenter(width, height, container) + + return fs +} + +func (fs *FuzzySearchModal) InitList(rooms map[string]*RoomView) { + for _, room := range rooms { + fs.roomList = append(fs.roomList, room.Room) + fs.roomTitles = append(fs.roomTitles, room.Room.GetTitle()) + } +} + +func (fs *FuzzySearchModal) changeHandler(str string) { + // Get matches and display in result box + fs.matches = fuzzy.RankFindFold(str, fs.roomTitles) + if len(str) > 0 && len(fs.matches) > 0 { + sort.Sort(fs.matches) + fs.results.Clear() + for _, match := range fs.matches { + fmt.Fprintf(fs.results, `["%d"]%s[""]\n`, match.Index, match.Target) + } + fs.parent.Render() + fs.results.Highlight(strconv.Itoa(fs.matches[0].Index)) + fs.results.ScrollToBeginning() + } else { + fs.results.Clear() + fs.results.Highlight() + } +} + +func (fs *FuzzySearchModal) keyHandler(event *tcell.EventKey) *tcell.EventKey { + highlights := fs.results.GetHighlights() + switch event.Key() { + case tcell.KeyEsc: + // Close room finder + fs.parent.views.RemovePage("fuzzy-search-modal") + fs.parent.app.SetFocus(fs.parent.views) + return nil + case tcell.KeyTab: + // Cycle highlighted area to next match + if len(highlights) > 0 { + fs.selected = (fs.selected + 1) % len(fs.matches) + fs.results.Highlight(strconv.Itoa(fs.matches[fs.selected].Index)) + fs.results.ScrollToHighlight() + } + return nil + case tcell.KeyEnter: + // Switch room to currently selected room + if len(highlights) > 0 { + debug.Print("Fuzzy Selected Room:", fs.roomList[fs.matches[fs.selected].Index].GetTitle()) + fs.mainView.SwitchRoom(fs.roomList[fs.matches[fs.selected].Index].Tags()[0].Tag, fs.roomList[fs.matches[fs.selected].Index]) + } + fs.parent.views.RemovePage("fuzzy-search-modal") + fs.parent.app.SetFocus(fs.parent.views) + fs.results.Clear() + fs.search.SetText("") + return nil + } + return event +} diff --git a/ui/fuzzy-view.go b/ui/fuzzy-view.go deleted file mode 100644 index d5498d0..0000000 --- a/ui/fuzzy-view.go +++ /dev/null @@ -1,127 +0,0 @@ -// gomuks - A terminal Matrix client written in Go. -// Copyright (C) 2018 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -package ui - -import ( - "fmt" - "sort" - "strconv" - - "github.com/evidlo/fuzzysearch/fuzzy" - "maunium.net/go/gomuks/debug" - "maunium.net/go/gomuks/matrix/rooms" - "maunium.net/go/gomuks/ui/widget" - "maunium.net/go/tcell" - "maunium.net/go/tview" -) - -type FuzzyView struct { - tview.Primitive - matches fuzzy.Ranks - selected int -} - -func NewFuzzyView(view *MainView, width int, height int) *FuzzyView { - - roomList := []*rooms.Room{} - roomTitles := []string{} - for _, tag := range view.roomList.tags { - for _, room := range view.roomList.items[tag].rooms { - roomList = append(roomList, room.Room) - roomTitles = append(roomTitles, room.GetTitle()) - } - } - // search box for fuzzy search - fuzzySearch := tview.NewInputField(). - SetLabel("Room: ") - - // list of rooms matching fuzzy search - fuzzyResults := tview.NewTextView(). - SetDynamicColors(true). - SetRegions(true) - - fuzzyResults. - SetBorderPadding(1, 0, 0, 0) - - // flexbox containing input box and results - fuzzyFlex := tview.NewFlex(). - SetDirection(tview.FlexRow). - AddItem(fuzzySearch, 1, 0, true). - AddItem(fuzzyResults, 0, 1, false) - - fuzzyFlex.SetBorder(true). - SetBorderPadding(1, 1, 1, 1). - SetTitle("Fuzzy Room Finder") - - var matches fuzzy.Ranks - var selected int - fuzz := &FuzzyView{ - Primitive: widget.TransparentCenter(width, height, fuzzyFlex), - matches: matches, - selected: selected, - } - - // callback to update search box - fuzzySearch.SetChangedFunc(func(str string) { - // get matches and display in fuzzyResults - fuzz.matches = fuzzy.RankFindFold(str, roomTitles) - if len(str) > 0 && len(fuzz.matches) > 0 { - sort.Sort(fuzz.matches) - fuzzyResults.Clear() - for _, match := range fuzz.matches { - fmt.Fprintf(fuzzyResults, "[\"%d\"]%s[\"\"]\n", match.Index, match.Target) - } - view.parent.app.Draw() - fuzzyResults.Highlight(strconv.Itoa(fuzz.matches[0].Index)) - fuzzyResults.ScrollToBeginning() - } else { - fuzzyResults.Clear() - fuzzyResults.Highlight() - } - }) - - // callback to handle key events on fuzzy search - fuzzySearch.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - highlights := fuzzyResults.GetHighlights() - if event.Key() == tcell.KeyEsc { - view.parent.views.RemovePage("fuzzy") - view.parent.app.SetFocus(view.parent.views) - return nil - } else if event.Key() == tcell.KeyTab { - // cycle highlighted area to next fuzzy match - if len(highlights) > 0 { - fuzz.selected = (fuzz.selected + 1) % len(fuzz.matches) - fuzzyResults.Highlight(strconv.Itoa(fuzz.matches[fuzz.selected].Index)) - fuzzyResults.ScrollToHighlight() - } - return nil - } else if event.Key() == tcell.KeyEnter { - // switch room to currently selected room - if len(highlights) > 0 { - debug.Print("Fuzzy Selected Room:", roomList[fuzz.matches[fuzz.selected].Index].GetTitle()) - view.SwitchRoom(roomList[fuzz.matches[fuzz.selected].Index].Tags()[0].Tag, roomList[fuzz.matches[fuzz.selected].Index]) - } - view.parent.views.RemovePage("fuzzy") - fuzzyResults.Clear() - fuzzySearch.SetText("") - return nil - } - return event - }) - - return fuzz -} diff --git a/ui/messages/textbase.go b/ui/messages/textbase.go index c241c0a..f805067 100644 --- a/ui/messages/textbase.go +++ b/ui/messages/textbase.go @@ -44,6 +44,18 @@ var ( spacePattern = regexp.MustCompile(`\s+`) ) +func matchBoundaryPattern(extract tstring.TString) tstring.TString { + matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) + if len(matches) > 0 { + if match := matches[len(matches)-1]; len(match) >= 2 { + if until := match[1]; until < len(extract) { + extract = extract[:until] + } + } + } + return extract +} + // CalculateBuffer generates the internal buffer for this message that consists // of the text of this message split into lines at most as wide as the width // parameter. @@ -63,24 +75,14 @@ func (msg *BaseTextMessage) calculateBufferWithText(text tstring.TString, width } else { newlines = 0 } - // Mostly from tview/textview.go#reindexBuffer() + // Adapted from tview/textview.go#reindexBuffer() for len(str) > 0 { extract := str.Truncate(width) if len(extract) < len(str) { if spaces := spacePattern.FindStringIndex(str[len(extract):].String()); spaces != nil && spaces[0] == 0 { extract = str[:len(extract)+spaces[1]] } - - matches := boundaryPattern.FindAllStringIndex(extract.String(), -1) - if len(matches) > 0 { - match := matches[len(matches)-1] - if len(match) >= 2 { - until := match[1] - if until < len(extract) { - extract = extract[:until] - } - } - } + extract = matchBoundaryPattern(extract) } msg.buffer = append(msg.buffer, extract) str = str[len(extract):] diff --git a/ui/messages/tstring/string.go b/ui/messages/tstring/string.go index a87d16a..4f3ee29 100644 --- a/ui/messages/tstring/string.go +++ b/ui/messages/tstring/string.go @@ -67,19 +67,22 @@ func (str TString) Append(data string) TString { } func (str TString) AppendColor(data string, color tcell.Color) TString { - newStr := make(TString, len(str)+len(data)) - copy(newStr, str) - for i, char := range data { - newStr[i+len(str)] = NewColorCell(char, color) - } - return newStr + return str.AppendCustom(data, func(r rune) Cell { + return NewColorCell(r, color) + }) } func (str TString) AppendStyle(data string, style tcell.Style) TString { + return str.AppendCustom(data, func(r rune) Cell { + return NewStyleCell(r, style) + }) +} + +func (str TString) AppendCustom(data string, cellCreator func(rune) Cell) TString { newStr := make(TString, len(str)+len(data)) copy(newStr, str) for i, char := range data { - newStr[i+len(str)] = NewStyleCell(char, style) + newStr[i+len(str)] = cellCreator(char) } return newStr } diff --git a/ui/view-main.go b/ui/view-main.go index e576a24..74a6ce1 100644 --- a/ui/view-main.go +++ b/ui/view-main.go @@ -202,9 +202,9 @@ func (view *MainView) KeyEventHandler(roomView *RoomView, key *tcell.EventKey) * case tcell.KeyUp: view.SwitchRoom(view.roomList.Previous()) case tcell.KeyEnter: - fuzz := NewFuzzyView(view, 42, 12) - view.parent.views.AddPage("fuzzy", fuzz, true, true) - view.parent.app.SetFocus(fuzz) + searchModal := NewFuzzySearchModal(view, 42, 12) + view.parent.views.AddPage("fuzzy-search-modal", searchModal, true, true) + view.parent.app.SetFocus(searchModal) default: return key }