Create pills when tab-completing or clicking nicks

This commit is contained in:
Tulir Asokan 2018-04-18 13:38:33 +03:00
parent 127c896291
commit 3750d5007f
10 changed files with 69 additions and 42 deletions

View File

@ -21,7 +21,9 @@ import (
"fmt" "fmt"
"math" "math"
"os" "os"
"strings"
"github.com/mattn/go-runewidth"
"maunium.net/go/gomuks/debug" "maunium.net/go/gomuks/debug"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/lib/open" "maunium.net/go/gomuks/lib/open"
@ -278,22 +280,28 @@ func (view *MessageView) handleUsernameClick(message ifc.MessageMeta, prevMessag
return false return false
} }
sender := []rune(uiMessage.Sender()) if len(uiMessage.Sender()) == 0 {
if len(sender) == 0 {
return false return false
} }
sender := fmt.Sprintf("[%s](https://matrix.to/#/%s)", uiMessage.Sender(), uiMessage.SenderID())
cursorPos := view.parent.input.GetCursorOffset() cursorPos := view.parent.input.GetCursorOffset()
text := []rune(view.parent.input.GetText()) text := view.parent.input.GetText()
var newText []rune var buf strings.Builder
if cursorPos == 0 { if cursorPos == 0 {
newText = append(sender, ':', ' ') buf.WriteString(sender)
newText = append(newText, text...) buf.WriteRune(':')
buf.WriteRune(' ')
buf.WriteString(text)
} else { } else {
newText = append(text[0:cursorPos], sender...) textBefore := runewidth.Truncate(text, cursorPos, "")
newText = append(newText, ' ') textAfter := text[len(textBefore):]
newText = append(newText, text[cursorPos:]...) buf.WriteString(textBefore)
buf.WriteString(sender)
buf.WriteRune(' ')
buf.WriteString(textAfter)
} }
newText := buf.String()
view.parent.input.SetText(string(newText)) view.parent.input.SetText(string(newText))
view.parent.input.SetCursorOffset(cursorPos + len(newText) - len(text)) view.parent.input.SetCursorOffset(cursorPos + len(newText) - len(text))
return true return true

View File

@ -33,6 +33,7 @@ func init() {
type BaseMessage struct { type BaseMessage struct {
MsgID string MsgID string
MsgType string MsgType string
MsgSenderID string
MsgSender string MsgSender string
MsgSenderColor tcell.Color MsgSenderColor tcell.Color
MsgTimestamp time.Time MsgTimestamp time.Time
@ -43,9 +44,10 @@ type BaseMessage struct {
prevBufferWidth int prevBufferWidth int
} }
func newBaseMessage(id, sender, msgtype string, timestamp time.Time) BaseMessage { func newBaseMessage(id, sender, displayname, msgtype string, timestamp time.Time) BaseMessage {
return BaseMessage{ return BaseMessage{
MsgSender: sender, MsgSenderID: sender,
MsgSender: displayname,
MsgTimestamp: timestamp, MsgTimestamp: timestamp,
MsgSenderColor: widget.GetHashColor(sender), MsgSenderColor: widget.GetHashColor(sender),
MsgType: msgtype, MsgType: msgtype,
@ -66,6 +68,7 @@ func (msg *BaseMessage) CopyFrom(from ifc.MessageMeta) {
fromMsg, ok := from.(UIMessage) fromMsg, ok := from.(UIMessage)
if ok { if ok {
msg.MsgSenderID = fromMsg.SenderID()
msg.MsgSender = fromMsg.RealSender() msg.MsgSender = fromMsg.RealSender()
msg.MsgID = fromMsg.ID() msg.MsgID = fromMsg.ID()
msg.MsgType = fromMsg.Type() msg.MsgType = fromMsg.Type()
@ -99,6 +102,10 @@ func (msg *BaseMessage) Sender() string {
} }
} }
func (msg *BaseMessage) SenderID() string {
return msg.MsgSenderID
}
func (msg *BaseMessage) RealSender() string { func (msg *BaseMessage) RealSender() string {
return msg.MsgSender return msg.MsgSender
} }

View File

@ -34,9 +34,9 @@ type ExpandedTextMessage struct {
} }
// NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state. // NewExpandedTextMessage creates a new ExpandedTextMessage object with the provided values and the default state.
func NewExpandedTextMessage(id, sender, msgtype string, text tstring.TString, timestamp time.Time) UIMessage { func NewExpandedTextMessage(id, sender, displayname, msgtype string, text tstring.TString, timestamp time.Time) UIMessage {
return &ExpandedTextMessage{ return &ExpandedTextMessage{
BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp), BaseTextMessage: newBaseTextMessage(id, sender, displayname, msgtype, timestamp),
MsgText: text, MsgText: text,
} }
} }

View File

@ -45,9 +45,9 @@ type ImageMessage struct {
} }
// NewImageMessage creates a new ImageMessage object with the provided values and the default state. // NewImageMessage creates a new ImageMessage object with the provided values and the default state.
func NewImageMessage(gmx ifc.Gomuks, id, sender, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage { func NewImageMessage(gmx ifc.Gomuks, id, sender, displayname, msgtype, homeserver, fileID string, data []byte, timestamp time.Time) UIMessage {
return &ImageMessage{ return &ImageMessage{
newBaseMessage(id, sender, msgtype, timestamp), newBaseMessage(id, sender, displayname, msgtype, timestamp),
homeserver, homeserver,
fileID, fileID,
data, data,

View File

@ -30,6 +30,7 @@ type UIMessage interface {
Buffer() []tstring.TString Buffer() []tstring.TString
Height() int Height() int
SenderID() string
RealSender() string RealSender() string
RegisterGomuks(gmx ifc.Gomuks) RegisterGomuks(gmx ifc.Gomuks)
} }

View File

@ -31,15 +31,11 @@ import (
) )
func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseEvent(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
member := room.GetMember(evt.Sender)
if member != nil {
evt.Sender = member.DisplayName
}
switch evt.Type { switch evt.Type {
case "m.room.message": case "m.room.message":
return ParseMessage(gmx, room, evt) return ParseMessage(gmx, room, evt)
case "m.room.member": case "m.room.member":
return ParseMembershipEvent(evt) return ParseMembershipEvent(room, evt)
} }
return nil return nil
} }
@ -53,6 +49,11 @@ func unixToTime(unix int64) time.Time {
} }
func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage { func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
displayname := evt.Sender
member := room.GetMember(evt.Sender)
if member != nil {
displayname = member.DisplayName
}
msgtype, _ := evt.Content["msgtype"].(string) msgtype, _ := evt.Content["msgtype"].(string)
ts := unixToTime(evt.Timestamp) ts := unixToTime(evt.Timestamp)
switch msgtype { switch msgtype {
@ -60,10 +61,10 @@ func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) message
format, hasFormat := evt.Content["format"].(string) format, hasFormat := evt.Content["format"].(string)
if hasFormat && format == "org.matrix.custom.html" { if hasFormat && format == "org.matrix.custom.html" {
text := ParseHTMLMessage(room, evt) text := ParseHTMLMessage(room, evt)
return messages.NewExpandedTextMessage(evt.ID, evt.Sender, msgtype, text, ts) return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, msgtype, text, ts)
} else { } else {
text, _ := evt.Content["body"].(string) text, _ := evt.Content["body"].(string)
return messages.NewTextMessage(evt.ID, evt.Sender, msgtype, text, ts) return messages.NewTextMessage(evt.ID, evt.Sender, displayname, msgtype, text, ts)
} }
case "m.image": case "m.image":
url, _ := evt.Content["url"].(string) url, _ := evt.Content["url"].(string)
@ -71,12 +72,16 @@ func ParseMessage(gmx ifc.Gomuks, room *rooms.Room, evt *gomatrix.Event) message
if err != nil { if err != nil {
debug.Printf("Failed to download %s: %v", url, err) debug.Printf("Failed to download %s: %v", url, err)
} }
return messages.NewImageMessage(gmx, evt.ID, evt.Sender, msgtype, hs, id, data, ts) return messages.NewImageMessage(gmx, evt.ID, evt.Sender, displayname, msgtype, hs, id, data, ts)
} }
return nil return nil
} }
func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring.TString) { func getMembershipEventContent(room *rooms.Room, evt *gomatrix.Event) (sender string, text tstring.TString) {
member := room.GetMember(evt.Sender)
if member != nil {
evt.Sender = member.DisplayName
}
membership, _ := evt.Content["membership"].(string) membership, _ := evt.Content["membership"].(string)
displayname, _ := evt.Content["displayname"].(string) displayname, _ := evt.Content["displayname"].(string)
if len(displayname) == 0 { if len(displayname) == 0 {
@ -121,8 +126,8 @@ func getMembershipEventContent(evt *gomatrix.Event) (sender string, text tstring
return return
} }
func ParseMembershipEvent(evt *gomatrix.Event) messages.UIMessage { func ParseMembershipEvent(room *rooms.Room, evt *gomatrix.Event) messages.UIMessage {
sender, text := getMembershipEventContent(evt) displayname, text := getMembershipEventContent(room, evt)
ts := unixToTime(evt.Timestamp) ts := unixToTime(evt.Timestamp)
return messages.NewExpandedTextMessage(evt.ID, sender, "m.room.membership", text, ts) return messages.NewExpandedTextMessage(evt.ID, evt.Sender, displayname, "m.room.membership", text, ts)
} }

View File

@ -32,8 +32,8 @@ type BaseTextMessage struct {
BaseMessage BaseMessage
} }
func newBaseTextMessage(id, sender, msgtype string, timestamp time.Time) BaseTextMessage { func newBaseTextMessage(id, sender, displayname, msgtype string, timestamp time.Time) BaseTextMessage {
return BaseTextMessage{newBaseMessage(id, sender, msgtype, timestamp)} return BaseTextMessage{newBaseMessage(id, sender, displayname, msgtype, timestamp)}
} }
// Regular expressions used to split lines when calculating the buffer. // Regular expressions used to split lines when calculating the buffer.

View File

@ -36,9 +36,9 @@ type TextMessage struct {
} }
// NewTextMessage creates a new UITextMessage object with the provided values and the default state. // NewTextMessage creates a new UITextMessage object with the provided values and the default state.
func NewTextMessage(id, sender, msgtype, text string, timestamp time.Time) UIMessage { func NewTextMessage(id, sender, displayname, msgtype, text string, timestamp time.Time) UIMessage {
return &TextMessage{ return &TextMessage{
BaseTextMessage: newBaseTextMessage(id, sender, msgtype, timestamp), BaseTextMessage: newBaseTextMessage(id, sender, displayname, msgtype, timestamp),
MsgText: text, MsgText: text,
} }
} }

View File

@ -23,11 +23,11 @@ import (
"strings" "strings"
"time" "time"
"maunium.net/go/tcell"
"maunium.net/go/gomuks/interface" "maunium.net/go/gomuks/interface"
"maunium.net/go/gomuks/matrix/rooms" "maunium.net/go/gomuks/matrix/rooms"
"maunium.net/go/gomuks/ui/messages" "maunium.net/go/gomuks/ui/messages"
"maunium.net/go/gomuks/ui/widget" "maunium.net/go/gomuks/ui/widget"
"maunium.net/go/tcell"
"maunium.net/go/tview" "maunium.net/go/tview"
) )
@ -213,16 +213,15 @@ func (view *RoomView) SetTyping(users []string) {
} }
} }
func (view *RoomView) AutocompleteUser(existingText string) (completions []string) { func (view *RoomView) AutocompleteUser(existingText string) (completions []*rooms.Member) {
textWithoutPrefix := existingText textWithoutPrefix := existingText
if strings.HasPrefix(existingText, "@") { if strings.HasPrefix(existingText, "@") {
textWithoutPrefix = existingText[1:] textWithoutPrefix = existingText[1:]
} }
for _, user := range view.Room.GetMembers() { for _, user := range view.Room.GetMembers() {
if strings.HasPrefix(user.DisplayName, textWithoutPrefix) { if strings.HasPrefix(user.DisplayName, textWithoutPrefix) ||
completions = append(completions, user.DisplayName) strings.HasPrefix(user.UserID, existingText) {
} else if strings.HasPrefix(user.UserID, existingText) { completions = append(completions, user)
completions = append(completions, user.UserID)
} }
} }
return return
@ -257,10 +256,12 @@ func (view *RoomView) UpdateUserList() {
func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage { func (view *RoomView) newUIMessage(id, sender, msgtype, text string, timestamp time.Time) messages.UIMessage {
member := view.Room.GetMember(sender) member := view.Room.GetMember(sender)
displayname := sender
if member != nil { if member != nil {
sender = member.DisplayName displayname = member.DisplayName
} }
return messages.NewTextMessage(id, sender, msgtype, text, timestamp) msg := messages.NewTextMessage(id, sender, displayname, msgtype, text, timestamp)
return msg
} }
func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) ifc.Message { func (view *RoomView) NewMessage(id, sender, msgtype, text string, timestamp time.Time) ifc.Message {

View File

@ -104,17 +104,22 @@ func findWordToTabComplete(text string) string {
func (view *MainView) InputTabComplete(roomView *RoomView, text string, cursorOffset int) string { func (view *MainView) InputTabComplete(roomView *RoomView, text string, cursorOffset int) string {
str := runewidth.Truncate(text, cursorOffset, "") str := runewidth.Truncate(text, cursorOffset, "")
word := findWordToTabComplete(str) word := findWordToTabComplete(str)
userCompletions := roomView.AutocompleteUser(word) userCompletions := roomView.AutocompleteUser(word)
if len(userCompletions) == 1 { if len(userCompletions) == 1 {
startIndex := len(str) - len(word) startIndex := len(str) - len(word)
completion := userCompletions[0] member := userCompletions[0]
completion := fmt.Sprintf("[%s](https://matrix.to/#/%s)", member.DisplayName, member.UserID)
if startIndex == 0 { if startIndex == 0 {
completion = completion + ": " completion = completion + ": "
} }
text = str[0:startIndex] + completion + text[len(str):] text = str[0:startIndex] + completion + text[len(str):]
} else if len(userCompletions) > 1 && len(userCompletions) < 6 { } else if len(userCompletions) > 1 && len(userCompletions) <= 5 {
roomView.SetStatus(fmt.Sprintf("Completions: %s", strings.Join(userCompletions, ", "))) // roomView.SetStatus(fmt.Sprintf("Completions: %s", strings.Join(userCompletions, ", ")))
} else if len(userCompletions) > 5 {
roomView.SetStatus("Over 5 completion options.")
} }
return text return text
} }