fix(message_input): add support for mentions list in (group) dms (#649)
Co-authored-by: Ayyan <ayn2op@gmail.com>
This commit is contained in:
parent
9c9815544f
commit
7e8af16e72
6 changed files with 165 additions and 66 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
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) }
|
||||
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 (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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue