diff --git a/matrix.go b/matrix.go index a9b55b9..5992a60 100644 --- a/matrix.go +++ b/matrix.go @@ -19,6 +19,7 @@ package main import ( "encoding/json" "fmt" + "strings" "time" "maunium.net/go/gomatrix" @@ -130,6 +131,7 @@ func (c *MatrixContainer) Start() { syncer := c.client.Syncer.(*gomatrix.DefaultSyncer) syncer.OnEventType("m.room.message", c.HandleMessage) + syncer.OnEventType("m.room.member", c.HandleMembership) syncer.OnEventType("m.typing", c.HandleTyping) for { @@ -155,15 +157,32 @@ func (c *MatrixContainer) HandleMessage(evt *gomatrix.Event) { timestampInt64, _ := timestampNumber.Int64() timestamp := time.Now() if timestampInt64 != 0 { - timestamp = time.Unix(timestampInt64 / 1000, timestampInt64 % 1000 * 1000) + timestamp = time.Unix(timestampInt64/1000, timestampInt64%1000*1000) } c.ui.MainView().AddMessage(evt.RoomID, evt.Sender, message, timestamp) } +func (c *MatrixContainer) HandleMembership(evt *gomatrix.Event) { + if evt.StateKey != nil && *evt.StateKey == c.config.Session.MXID { + membership, _ := evt.Content["membership"].(string) + prevMembership := "leave" + if evt.Unsigned.PrevContent != nil { + prevMembership, _ = evt.Unsigned.PrevContent["membership"].(string) + } + if membership == prevMembership { + return + } + if membership == "join" { + c.ui.MainView().AddRoom(evt.RoomID) + } else if membership == "leave" { + c.ui.MainView().RemoveRoom(evt.RoomID) + } + } +} + func (c *MatrixContainer) HandleTyping(evt *gomatrix.Event) { users := evt.Content["user_ids"].([]interface{}) - c.debug.Print(users, "are typing") strUsers := make([]string, len(users)) for i, user := range users { @@ -173,11 +192,13 @@ func (c *MatrixContainer) HandleTyping(evt *gomatrix.Event) { } func (c *MatrixContainer) SendMessage(roomID, message string) { + c.gmx.Recover() c.SendTyping(roomID, false) c.client.SendText(roomID, message) } func (c *MatrixContainer) SendTyping(roomID string, typing bool) { + c.gmx.Recover() time := time.Now().Unix() if c.typing > time && typing { return @@ -192,7 +213,26 @@ func (c *MatrixContainer) SendTyping(roomID string, typing bool) { } } -func (c *MatrixContainer) GetState(roomID string) []*gomatrix.Event { +func (c *MatrixContainer) JoinRoom(roomID string) error { + if len(roomID) == 0 { + return fmt.Errorf("invalid room ID") + } + + server := "" + if roomID[0] == '!' { + server = roomID[strings.Index(roomID, ":")+1:] + } + + resp, err := c.client.JoinRoom(roomID, server, nil) + if err != nil { + return err + } + + c.ui.MainView().AddRoom(resp.RoomID) + return nil +} + +func (c *MatrixContainer) getState(roomID string) []*gomatrix.Event { content := make([]*gomatrix.Event, 0) err := c.client.StateEvent(roomID, "", "", &content) if err != nil { @@ -202,15 +242,15 @@ func (c *MatrixContainer) GetState(roomID string) []*gomatrix.Event { return content } -func (c *MatrixContainer) UpdateState(roomID string) { +func (c *MatrixContainer) GetRoom(roomID string) *gomatrix.Room { room := c.client.Store.LoadRoom(roomID) - if room == nil { - return - } - events := c.GetState(room.ID) - if events != nil { - for _, event := range events { - room.UpdateState(event) + if len(room.State) == 0 { + events := c.getState(room.ID) + if events != nil { + for _, event := range events { + room.UpdateState(event) + } } } + return room } diff --git a/room-view.go b/room-view.go index b9b911e..d5b9ca2 100644 --- a/room-view.go +++ b/room-view.go @@ -20,11 +20,11 @@ import ( "fmt" "hash/fnv" "regexp" - "sort" "strings" "time" "github.com/gdamore/tcell" + "maunium.net/go/gomatrix" "maunium.net/go/tview" ) @@ -35,7 +35,7 @@ type RoomView struct { content *tview.TextView status *tview.TextView userList *tview.TextView - users sort.StringSlice + room *gomatrix.Room } var colorNames []string @@ -49,16 +49,17 @@ func init() { } } -func NewRoomView(topic string) *RoomView { +func NewRoomView(room *gomatrix.Room) *RoomView { view := &RoomView{ Box: tview.NewBox(), topic: tview.NewTextView(), content: tview.NewTextView(), status: tview.NewTextView(), userList: tview.NewTextView(), + room: room, } view.topic. - SetText(strings.Replace(topic, "\n", " ", -1)). + SetText(strings.Replace(room.GetTopic(), "\n", " ", -1)). SetBackgroundColor(tcell.ColorDarkGreen) view.status.SetBackgroundColor(tcell.ColorDimGray) view.userList.SetDynamicColors(true) @@ -86,6 +87,12 @@ func (view *RoomView) Draw(screen tcell.Screen) { } func (view *RoomView) SetTyping(users []string) { + for index, user := range users { + member := view.room.GetMember(user) + if member != nil { + users[index] = member.DisplayName + } + } if len(users) == 0 { view.status.SetText("") } else if len(users) < 2 { @@ -102,7 +109,7 @@ var colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6})\]`) func color(s string) string { h := fnv.New32a() h.Write([]byte(s)) - color := colorNames[int(h.Sum32()) % len(colorNames)] + color := colorNames[int(h.Sum32())%len(colorNames)] return fmt.Sprintf("[%s]%s[white]", color, s) } @@ -111,29 +118,21 @@ func escapeColor(s string) string { } func (view *RoomView) AddMessage(sender, message string, timestamp time.Time) { + member := view.room.GetMember(sender) + if member != nil { + sender = member.DisplayName + } fmt.Fprintf(view.content, "[%s] %s: %s\n", timestamp.Format("15:04:05"), color(sender), escapeColor(message)) } -func (view *RoomView) SetUsers(users []string) { - view.users = sort.StringSlice(users) - view.users.Sort() +func (view *RoomView) UpdateUserList() { var buf strings.Builder - for _, user := range view.users { - buf.WriteString(color(user)) - buf.WriteRune('\n') + for _, user := range view.room.GetMembers() { + if user.Membership == "join" { + buf.WriteString(color(user.DisplayName)) + buf.WriteRune('\n') + } } view.userList.SetText(buf.String()) } - -func (view *RoomView) RemoveUser(user string) { - i := view.users.Search(user) - if i >= 0 { - view.users = append(view.users[:i], view.users[i+1:]...) - view.userList.SetText(strings.Join(view.users, "\n")) - } -} - -func (view *RoomView) AddUser(user string) { - view.SetUsers(append(view.users, user)) -} diff --git a/session.go b/session.go index d83d443..44f0787 100644 --- a/session.go +++ b/session.go @@ -88,7 +88,7 @@ func (s *Session) LoadNextBatch(_ string) string { func (s *Session) LoadRoom(mxid string) *gomatrix.Room { room, ok := s.Rooms[mxid] if !ok || room == nil { - room := gomatrix.NewRoom(mxid) + room = gomatrix.NewRoom(mxid) s.SaveRoom(room) } return room diff --git a/view-main.go b/view-main.go index 3c536c9..58fbd80 100644 --- a/view-main.go +++ b/view-main.go @@ -85,9 +85,9 @@ func (ui *GomuksUI) NewMainView() tview.Primitive { func (view *MainView) InputChanged(text string) { if len(text) == 0 { - view.matrix.SendTyping(view.CurrentRoomID(), false) + go view.matrix.SendTyping(view.CurrentRoomID(), false) } else if text[0] != '/' { - view.matrix.SendTyping(view.CurrentRoomID(), true) + go view.matrix.SendTyping(view.CurrentRoomID(), true) } } @@ -100,15 +100,16 @@ func (view *MainView) InputDone(key tcell.Key) { args := strings.SplitN(text, " ", 2) command := strings.ToLower(args[0]) args = args[1:] - view.HandleCommand(room, command, args) + go view.HandleCommand(room, command, args) } else { - view.matrix.SendMessage(room, text) + go view.matrix.SendMessage(room, text) } view.input.SetText("") } } func (view *MainView) HandleCommand(room, command string, args []string) { + view.gmx.Recover() view.debug.Print("Handling command", command, args) switch command { case "/quit": @@ -120,6 +121,7 @@ func (view *MainView) HandleCommand(room, command string, args []string) { view.config.Session.Save() view.gmx.Stop() case "/part": + fallthrough case "/leave": view.matrix.client.LeaveRoom(room) case "/join": @@ -127,9 +129,9 @@ func (view *MainView) HandleCommand(room, command string, args []string) { view.AddMessage(room, "*", "Usage: /join ", time.Now()) break } - mxid := args[0] - server := mxid[strings.Index(mxid, ":")+1:] - view.matrix.client.JoinRoom(mxid, server, nil) + view.debug.Print(view.matrix.JoinRoom(args[0])) + default: + view.AddMessage(room, "*", "Unknown command.", time.Now()) } } @@ -165,36 +167,58 @@ func (view *MainView) SwitchRoom(roomIndex int) { } view.currentRoomIndex = roomIndex % len(view.roomIDs) view.roomView.SwitchToPage(view.CurrentRoomID()) + view.roomList.SetCurrentItem(roomIndex) view.parent.Render() } +func (view *MainView) addRoom(index int, room string) { + roomStore := view.matrix.GetRoom(room) + + view.roomList.AddItem(roomStore.GetTitle(), "", 0, func() { + view.SwitchRoom(index) + }) + if !view.roomView.HasPage(room) { + roomView := NewRoomView(roomStore) + view.rooms[room] = roomView + view.roomView.AddPage(room, roomView, true, false) + roomView.UpdateUserList() + } +} + +func (view *MainView) HasRoom(room string) bool { + for _, existingRoom := range view.roomIDs { + if existingRoom == room { + return true + } + } + return false +} + +func (view *MainView) AddRoom(room string) { + if view.HasRoom(room) { + return + } + view.roomIDs = append(view.roomIDs, room) + view.addRoom(len(view.roomIDs) - 1, room) +} + +func (view *MainView) RemoveRoom(room string) { + if !view.HasRoom(room) { + return + } + view.roomList.RemoveItem(view.currentRoomIndex) + if view.CurrentRoomID() == room { + view.SwitchRoom(view.currentRoomIndex - 1) + } + view.roomView.RemovePage(room) +} + func (view *MainView) SetRoomList(rooms []string) { view.roomIDs = rooms view.roomList.Clear() + view.roomView.Clear() for index, room := range rooms { - localRoomIndex := index - - view.matrix.UpdateState(room) - roomStore := view.matrix.config.Session.LoadRoom(room) - - name := room - topic := "" - var users []string - if roomStore != nil { - name = roomStore.GetTitle() - topic = roomStore.GetTopic() - users = roomStore.GetMembers() - } - - view.roomList.AddItem(name, "", 0, func() { - view.SwitchRoom(localRoomIndex) - }) - if !view.roomView.HasPage(room) { - roomView := NewRoomView(topic) - roomView.SetUsers(users) - view.rooms[room] = roomView - view.roomView.AddPage(room, roomView, true, false) - } + view.addRoom(index, room) } view.SwitchRoom(0) }