Przeglądaj źródła

fix(message_input): add support for mentions list in (group) dms (#649)

Co-authored-by: Ayyan <ayn2op@gmail.com>
xqrs 4 miesięcy temu
rodzic
commit
7e8af16e72
6 zmienionych plików z 165 dodań i 66 usunięć
  1. 1 2
      cmd/chatview.go
  2. 3 5
      cmd/guilds_tree.go
  3. 145 46
      cmd/message_input.go
  4. 8 8
      cmd/messages_list.go
  5. 6 3
      cmd/state.go
  6. 2 2
      internal/config/config.toml

+ 1 - 2
cmd/chatview.go

@@ -28,8 +28,7 @@ type chatView struct {
 	messagesList *messagesList
 	messageInput *messageInput
 
-	selectedGuildID   discord.GuildID
-	selectedChannelID discord.ChannelID
+	selectedChannel *discord.Channel
 
 	app *tview.Application
 	cfg *config.Config

+ 3 - 5
cmd/guilds_tree.go

@@ -142,8 +142,6 @@ PARENT_CHANNELS:
 }
 
 func (gt *guildsTree) onSelected(node *tview.TreeNode) {
-	app.chatView.messageInput.reset()
-
 	if len(node.GetChildren()) != 0 {
 		node.SetExpanded(!node.IsExpanded())
 		return
@@ -183,6 +181,8 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
 			app.chatView.messagesList.requestGuildMembers(guildID, messages)
 		}
 
+		app.chatView.selectedChannel = channel
+
 		app.chatView.messagesList.reset()
 		app.chatView.messagesList.setTitle(*channel)
 		app.chatView.messagesList.drawMessages(messages)
@@ -197,9 +197,7 @@ func (gt *guildsTree) onSelected(node *tview.TreeNode) {
 			app.SetFocus(app.chatView.messageInput)
 		}
 
-		app.chatView.selectedChannelID = channel.ID
-		app.chatView.selectedGuildID = channel.GuildID
-	case nil: // Direct messages
+	case nil: // Direct messages folder
 		channels, err := discordState.PrivateChannels()
 		if err != nil {
 			slog.Error("failed to get private channels", "err", err)

+ 145 - 46
cmd/message_input.go

@@ -47,8 +47,6 @@ type messageInput struct {
 	lastSearch      time.Time
 }
 
-type memberList []discord.Member
-
 func newMessageInput(cfg *config.Config) *messageInput {
 	mi := &messageInput{
 		TextArea:        tview.NewTextArea(),
@@ -96,7 +94,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
 
 	case mi.cfg.Keys.MessageInput.Send:
 		if app.chatView.GetVisibile(mentionsListPageName) {
-			mi.tabComplete(false)
+			mi.tabComplete()
 			return nil
 		}
 
@@ -119,7 +117,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
 
 		return nil
 	case mi.cfg.Keys.MessageInput.TabComplete:
-		go app.QueueUpdateDraw(func() { mi.tabComplete(false) })
+		go app.QueueUpdateDraw(func() { mi.tabComplete() })
 		return nil
 	}
 
@@ -135,7 +133,7 @@ func (mi *messageInput) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
 			}
 		}
 
-		go app.QueueUpdateDraw(func() { mi.tabComplete(true) })
+		go app.QueueUpdateDraw(func() { mi.tabSuggestion() })
 	}
 
 	return event
@@ -149,7 +147,7 @@ func (mi *messageInput) paste() {
 }
 
 func (mi *messageInput) send() {
-	if !app.chatView.selectedChannelID.IsValid() {
+	if app.chatView.selectedChannel == nil {
 		return
 	}
 
@@ -158,7 +156,7 @@ func (mi *messageInput) send() {
 		return
 	}
 
-	text = processText(app.chatView.selectedChannelID, []byte(text))
+	text = processText(app.chatView.selectedChannel, []byte(text))
 
 	if mi.edit {
 		m, err := app.chatView.messagesList.selectedMessage()
@@ -176,8 +174,8 @@ func (mi *messageInput) send() {
 	} else {
 		data := mi.sendMessageData
 		data.Content = text
-		if _, err := discordState.SendMessageComplex(app.chatView.selectedChannelID, *data); err != nil {
-			slog.Error("failed to send message in channel", "channel_id", app.chatView.selectedChannelID, "err", err)
+		if _, err := discordState.SendMessageComplex(app.chatView.selectedChannel.ID, *data); err != nil {
+			slog.Error("failed to send message in channel", "channel_id", app.chatView.selectedChannel.ID, "err", err)
 		}
 	}
 
@@ -193,7 +191,7 @@ func (mi *messageInput) send() {
 	app.chatView.messagesList.ScrollToEnd()
 }
 
-func processText(cID discord.ChannelID, src []byte) string {
+func processText(channel *discord.Channel, src []byte) string {
 	var (
 		ranges     [][2]int
 		canMention = true
@@ -217,19 +215,36 @@ func processText(cID discord.ChannelID, src []byte) string {
 	})
 
 	for _, rng := range ranges {
-		src = slices.Replace(src, rng[0], rng[1], expandMentions(cID, src[rng[0]:rng[1]])...)
+		src = slices.Replace(src, rng[0], rng[1], expandMentions(channel, src[rng[0]:rng[1]])...)
 	}
 
 	return string(src)
 }
 
-func expandMentions(cID discord.ChannelID, src []byte) []byte {
+func expandMentions(c *discord.Channel, src []byte) []byte {
 	return mentionRegex.ReplaceAllFunc(src, func(input []byte) []byte {
 		output := input
 		name := string(input[1:])
-		discordState.MemberStore.Each(app.chatView.selectedGuildID, func(m *discord.Member) bool {
-			if strings.EqualFold(m.User.Username, name) && channelHasUser(cID, m.User.ID) {
-				output = []byte(m.User.ID.Mention())
+		if c.Type == discord.DirectMessage || c.Type == discord.GroupDM {
+			for _, user := range c.DMRecipients {
+				if strings.EqualFold(user.Username, name) {
+					return []byte(user.ID.Mention())
+				}
+			}
+			// self ping
+			me, err := discordState.Cabinet.Me()
+			if err != nil {
+				slog.Error("failed to get client user (me)", "err", err)
+			} else if strings.EqualFold(me.Username, name) {
+				return []byte(me.ID.Mention())
+			}
+			return output
+		}
+		discordState.MemberStore.Each(c.GuildID, func(m *discord.Member) bool {
+			if strings.EqualFold(m.User.Username, name) {
+				if channelHasUser(c.ID, m.User.ID) {
+					output = []byte(m.User.ID.Mention())
+				}
 				return true
 			}
 			return false
@@ -238,7 +253,7 @@ func expandMentions(cID discord.ChannelID, src []byte) []byte {
 	})
 }
 
-func (mi *messageInput) tabComplete(isAuto bool) {
+func (mi *messageInput) tabComplete() {
 	posEnd, name, r := mi.GetWordUnderCursor(func(r rune) bool {
 		return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '.'
 	})
@@ -248,13 +263,17 @@ func (mi *messageInput) tabComplete(isAuto bool) {
 	}
 	pos := posEnd - (len(name) + 1)
 
-	gID := app.chatView.selectedGuildID
-	cID := app.chatView.selectedChannelID
+	gID := app.chatView.selectedChannel.GuildID
 
-	if !isAuto {
-		if mi.cfg.AutocompleteLimit == 0 {
+	if mi.cfg.AutocompleteLimit == 0 {
+		if !gID.IsValid() {
+			users := app.chatView.selectedChannel.DMRecipients
+			res := fuzzy.FindFrom(name, userList(users))
+			if len(res) > 0 {
+				mi.Replace(pos, posEnd, "@"+users[res[0].Index].Username+" ")
+			}
+		} else {
 			mi.searchMember(gID, name)
-
 			members, err := discordState.Cabinet.Members(gID)
 			if err != nil {
 				slog.Error("failed to get members from state", "guild_id", gID, "err", err)
@@ -263,45 +282,93 @@ func (mi *messageInput) tabComplete(isAuto bool) {
 
 			res := fuzzy.FindFrom(name, memberList(members))
 			for _, r := range res {
-				if channelHasUser(cID, members[r.Index].User.ID) {
+				if channelHasUser(app.chatView.selectedChannel.ID, members[r.Index].User.ID) {
 					mi.Replace(pos, posEnd, "@"+members[r.Index].User.Username+" ")
 					return
 				}
 			}
-			return
 		}
-		if mi.mentionsList.GetItemCount() == 0 {
-			return
-		}
-		_, name = mi.mentionsList.GetItemText(mi.mentionsList.GetCurrentItem())
-		mi.Replace(pos, posEnd, "@"+name+" ")
+		return
+	}
+	if mi.mentionsList.GetItemCount() == 0 {
+		return
+	}
+	_, name = mi.mentionsList.GetItemText(mi.mentionsList.GetCurrentItem())
+	mi.Replace(pos, posEnd, "@"+name+" ")
+	mi.stopTabCompletion()
+}
+
+func (mi *messageInput) tabSuggestion() {
+	_, name, r := mi.GetWordUnderCursor(func(r rune) bool {
+		return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '.'
+	})
+	if r != '@' {
 		mi.stopTabCompletion()
 		return
 	}
+	gID := app.chatView.selectedChannel.GuildID
+	cID := app.chatView.selectedChannel.ID
+	mi.mentionsList.Clear()
 
-	// Special case, show recent messages' authors
+	var shown map[string]struct{}
+	var userDone struct{}
 	if name == "" {
+		shown = make(map[string]struct{})
+		// Don't show @me in the list of recent authors
+		me, err := discordState.Cabinet.Me()
+		if err != nil {
+			slog.Error("failed to get client user (me)", "err", err)
+		} else {
+			shown[me.Username] = userDone
+		}
+	}
+
+	// DMs have recipients, not members
+	if !gID.IsValid() {
+		if name == "" { // show recent messages' authors
+			msgs, err := discordState.Cabinet.Messages(cID)
+			if err != nil {
+				return
+			}
+			for _, m := range msgs {
+				if _, ok := shown[m.Author.Username]; ok {
+					continue
+				}
+				shown[m.Author.Username] = userDone
+				mi.addMentionUser(&m.Author)
+			}
+		} else {
+			users := app.chatView.selectedChannel.DMRecipients
+			me, err := discordState.Cabinet.Me()
+			if err != nil {
+				slog.Error("failed to get client user (me)", "err", err)
+			} else {
+				users = append(users, *me)
+			}
+			res := fuzzy.FindFrom(name, userList(users))
+			for _, r := range res {
+				mi.addMentionUser(&users[r.Index])
+			}
+		}
+	} else if name == "" { // show recent messages' authors
 		msgs, err := discordState.Cabinet.Messages(cID)
 		if err != nil {
 			return
 		}
-		shown := make(map[string]bool)
-		mi.mentionsList.Clear()
 		for _, m := range msgs {
-			if shown[m.Author.Username] {
+			if _, ok := shown[m.Author.Username]; ok {
 				continue
 			}
-			shown[m.Author.Username] = true
+			shown[m.Author.Username] = userDone
 			discordState.MemberState.RequestMember(gID, m.Author.ID)
 			if mem, err := discordState.Cabinet.Member(gID, m.Author.ID); err == nil {
-				if mi.addMentionItem(gID, mem) {
+				if mi.addMentionMember(gID, mem) {
 					break
 				}
 			}
 		}
 	} else {
 		mi.searchMember(gID, name)
-		mi.mentionsList.Clear()
 		mems, err := discordState.Cabinet.Members(gID)
 		if err != nil {
 			slog.Error("fetching members failed", "err", err)
@@ -313,7 +380,7 @@ func (mi *messageInput) tabComplete(isAuto bool) {
 		}
 		for _, r := range res {
 			if channelHasUser(cID, mems[r.Index].User.ID) &&
-				mi.addMentionItem(gID, &mems[r.Index]) {
+				mi.addMentionMember(gID, &mems[r.Index]) {
 				break
 			}
 		}
@@ -324,12 +391,27 @@ func (mi *messageInput) tabComplete(isAuto bool) {
 		return
 	}
 
-	_, col, _, _ := mi.GetCursor()
-	mi.showMentionList(col - 1)
+	mi.showMentionList()
+}
+
+type memberList []discord.Member
+type userList   []discord.User
+
+func (ml memberList) String(i int) string {
+	return ml[i].Nick + ml[i].User.DisplayName + ml[i].User.Tag()
 }
 
-func (m memberList) String(i int) string { return m[i].Nick + m[i].User.DisplayName + m[i].User.Tag() }
-func (m memberList) Len() int            { return len(m) }
+func (ml memberList) Len() int {
+	return len(ml)
+}
+
+func (ul userList) String(i int) string {
+	return ul[i].DisplayName + ul[i].Tag()
+}
+
+func (ul userList) Len() int {
+	return len(ul)
+}
 
 // channelHasUser checks if a user has permission to view the specified channel
 func channelHasUser(channelID discord.ChannelID, userID discord.UserID) bool {
@@ -369,7 +451,7 @@ func (mi *messageInput) searchMember(gID discord.GuildID, name string) {
 	mi.cache.Create(key, app.chatView.messagesList.waitForChunkEvent())
 }
 
-func (mi *messageInput) showMentionList(col int) {
+func (mi *messageInput) showMentionList() {
 	borders := 0
 	if mi.cfg.Theme.Border.Enabled {
 		borders = 1
@@ -394,6 +476,7 @@ func (mi *messageInput) showMentionList(col int) {
 		}
 
 		w = min(w+borders*2, maxW)
+		_, col, _, _ := mi.GetCursor()
 		x += min(col, maxW-w)
 	}
 
@@ -405,7 +488,7 @@ func (mi *messageInput) showMentionList(col int) {
 	app.SetFocus(mi)
 }
 
-func (mi *messageInput) addMentionItem(gID discord.GuildID, m *discord.Member) bool {
+func (mi *messageInput) addMentionMember(gID discord.GuildID, m *discord.Member) bool {
 	if m == nil {
 		return false
 	}
@@ -427,9 +510,7 @@ func (mi *messageInput) addMentionItem(gID discord.GuildID, m *discord.Member) b
 	presence, err := discordState.Cabinet.Presence(gID, m.User.ID)
 	if err != nil {
 		slog.Info("failed to get presence from state", "guild_id", gID, "user_id", m.User.ID, "err", err)
-	}
-
-	if presence != nil && presence.Status == discord.OfflineStatus {
+	} else if presence.Status == discord.OfflineStatus {
 		name = fmt.Sprintf("[::d]%s[::D]", name)
 	}
 
@@ -437,6 +518,24 @@ func (mi *messageInput) addMentionItem(gID discord.GuildID, m *discord.Member) b
 	return mi.mentionsList.GetItemCount() > int(mi.cfg.AutocompleteLimit)
 }
 
+func (mi *messageInput) addMentionUser(user *discord.User) {
+	if user == nil {
+		return
+	}
+
+	name := user.DisplayOrUsername()
+	presence, err := discordState.Cabinet.Presence(discord.NullGuildID, user.ID)
+	if err != nil {
+		slog.Info("failed to get presence from state", "user_id", user.ID, "err", err)
+	} else if presence.Status == discord.OfflineStatus {
+		name = fmt.Sprintf("[::d]%s[::D]", name)
+	}
+
+	mi.mentionsList.AddItem(name, user.Username, 0, nil)
+	return
+}
+
+// used by chatView
 func (mi *messageInput) removeMentionsList() {
 	app.chatView.
 		RemovePage(mentionsListPageName).
@@ -494,7 +593,7 @@ func (mi *messageInput) addTitle(s string) {
 }
 
 func (mi *messageInput) openFilePicker() {
-	if !app.chatView.selectedChannelID.IsValid() {
+	if app.chatView.selectedChannel == nil {
 		return
 	}
 

+ 8 - 8
cmd/messages_list.go

@@ -240,7 +240,7 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
 		return nil, errors.New("no message is currently selected")
 	}
 
-	m, err := discordState.Cabinet.Message(app.chatView.selectedChannelID, ml.selectedMessageID)
+	m, err := discordState.Cabinet.Message(app.chatView.selectedChannel.ID, ml.selectedMessageID)
 	if err != nil {
 		return nil, fmt.Errorf("could not retrieve selected message: %w", err)
 	}
@@ -249,7 +249,7 @@ func (ml *messagesList) selectedMessage() (*discord.Message, error) {
 }
 
 func (ml *messagesList) selectedMessageIndex() (int, error) {
-	ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannelID)
+	ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannel.ID)
 	if err != nil {
 		return -1, err
 	}
@@ -295,9 +295,9 @@ func (ml *messagesList) onInputCapture(event *tcell.EventKey) *tcell.EventKey {
 }
 
 func (ml *messagesList) _select(name string) {
-	ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannelID)
+	ms, err := discordState.Cabinet.Messages(app.chatView.selectedChannel.ID)
 	if err != nil {
-		slog.Error("failed to get messages", "err", err, "channel_id", app.chatView.selectedChannelID)
+		slog.Error("failed to get messages", "err", err, "channel_id", app.chatView.selectedChannel.ID)
 		return
 	}
 
@@ -622,16 +622,16 @@ func (ml *messagesList) delete() {
 		}
 	}
 
-	if err := discordState.DeleteMessage(app.chatView.selectedChannelID, msg.ID, ""); err != nil {
-		slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannelID, "message_id", msg.ID, "err", err)
+	if err := discordState.DeleteMessage(app.chatView.selectedChannel.ID, msg.ID, ""); err != nil {
+		slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannel.ID, "message_id", msg.ID, "err", err)
 		return
 	}
 
 	ml.selectedMessageID = 0
 	ml.Highlight()
 
-	if err := discordState.MessageRemove(app.chatView.selectedChannelID, msg.ID); err != nil {
-		slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannelID, "message_id", msg.ID, "err", err)
+	if err := discordState.MessageRemove(app.chatView.selectedChannel.ID, msg.ID); err != nil {
+		slog.Error("failed to delete message", "channel_id", app.chatView.selectedChannel.ID, "message_id", msg.ID, "err", err)
 		return
 	}
 

+ 6 - 3
cmd/state.go

@@ -146,7 +146,8 @@ func onReady(r *gateway.ReadyEvent) {
 }
 
 func onMessageCreate(message *gateway.MessageCreateEvent) {
-	if app.chatView.selectedChannelID == message.ChannelID {
+	if app.chatView.selectedChannel != nil &&
+	   app.chatView.selectedChannel.ID == message.ChannelID {
 		app.chatView.messagesList.drawMessage(app.chatView.messagesList, message.Message)
 		app.Draw()
 	}
@@ -157,13 +158,15 @@ func onMessageCreate(message *gateway.MessageCreateEvent) {
 }
 
 func onMessageUpdate(message *gateway.MessageUpdateEvent) {
-	if app.chatView.selectedChannelID == message.ChannelID {
+	if app.chatView.selectedChannel != nil &&
+	   app.chatView.selectedChannel.ID == message.ChannelID {
 		onMessageDelete(&gateway.MessageDeleteEvent{ID: message.ID, ChannelID: message.ChannelID, GuildID: message.GuildID})
 	}
 }
 
 func onMessageDelete(message *gateway.MessageDeleteEvent) {
-	if app.chatView.selectedChannelID == message.ChannelID {
+	if app.chatView.selectedChannel != nil &&
+	   app.chatView.selectedChannel.ID == message.ChannelID {
 		messages, err := discordState.Cabinet.Messages(message.ChannelID)
 		if err != nil {
 			slog.Error("failed to get messages from state", "err", err, "channel_id", message.ChannelID)

+ 2 - 2
internal/config/config.toml

@@ -11,8 +11,8 @@ markdown = true
 hide_blocked_users = true
 show_attachment_links = true
 
-# Use autocomplete_limit = 0 to disable autocompleting mentions
-# Note: tab completion will still work, but it won't show any list.
+# Max members to be in the mention autocomplete suggestions list
+# Note: Use autocomplete_limit = 0 to disable.
 autocomplete_limit = 20
 
 # The number of messages to fetch when a text-based channel is selected from guilds tree. The minimum and maximum value is 0 and 100, respectively.