Improve/mention input segments (#3631)

* improve calculate input segments relate to mention feature

* bump version

* rename variable
This commit is contained in:
frank 2023-06-19 19:08:45 +08:00 committed by GitHub
parent 1eb92a19a2
commit 0fcf3abd83
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 164 additions and 86 deletions

View File

@ -1 +1 @@
0.159.0
0.159.1

View File

@ -309,12 +309,16 @@ func (m *MentionManager) OnChangeText(chatID, fullText string) (*ChatMentionCont
ctx.MentionState.End = diff.end
ctx.MentionState.operation = diff.operation
ctx.MentionState.AtIdxs = calcAtIdxs(ctx.MentionState)
atIndexes, err := calculateAtIndexEntries(ctx.MentionState)
if err != nil {
return ctx, err
}
ctx.MentionState.AtIdxs = atIndexes
m.logger.Debug("OnChangeText", zap.String("chatID", chatID), zap.Any("state", ctx.MentionState))
return m.calculateSuggestions(chatID, fullText)
}
func (m *MentionManager) recheckAtIdxs(chatID string, text string, publicKey string) (*ChatMentionContext, error) {
func (m *MentionManager) recheckAtIdxs(chatID string, fullText string, publicKey string) (*ChatMentionContext, error) {
user, err := m.mentionableUserGetter.getMentionableUser(chatID, publicKey)
if err != nil {
return nil, err
@ -322,8 +326,12 @@ func (m *MentionManager) recheckAtIdxs(chatID string, text string, publicKey str
ctx := m.getChatMentionContext(chatID)
state := ctx.MentionState
mentionableUsers := map[string]*MentionableUser{user.ID: user}
newAtIdxs := checkIdxForMentions(text, state.AtIdxs, mentionableUsers)
ctx.InputSegments = calculateInput(text, newAtIdxs)
newAtIdxs := checkIdxForMentions(fullText, state.AtIdxs, mentionableUsers)
inputSegments, success := calculateInput(fullText, newAtIdxs)
if !success {
m.logger.Warn("recheckAtIdxs: calculateInput failed", zap.String("chatID", chatID), zap.String("fullText", fullText), zap.Any("state", ctx.MentionState))
}
ctx.InputSegments = inputSegments
state.AtIdxs = newAtIdxs
return ctx, nil
}
@ -357,7 +365,10 @@ func (m *MentionManager) calculateSuggestionsWithMentionableUsers(chatID string,
}
newAtIndexEntries := checkIdxForMentions(fullText, state.AtIdxs, mentionableUsers)
calculatedInput := calculateInput(fullText, newAtIndexEntries)
calculatedInput, success := calculateInput(fullText, newAtIndexEntries)
if !success {
m.logger.Warn("calculateSuggestionsWithMentionableUsers: calculateInput failed", zap.String("chatID", chatID), zap.String("fullText", fullText), zap.Any("state", state))
}
var end int
switch state.operation {
@ -704,6 +715,9 @@ func hasMatchingPrefix(text, searchedText string) bool {
func isMentioned(user *MentionableUser, text string) bool {
regexStr := ""
for i, name := range user.names() {
if name == "" {
continue
}
name = strings.ToLower(name)
if i != 0 {
regexStr += "|"
@ -738,6 +752,7 @@ func matchMention(text string, users map[string]*MentionableUser, mentionKeyIdx
return nil
case userSuggestionsCnt == 1:
user := getFirstUser(userSuggestions)
// maybe len(users) == 1 and user input `@` so we need to recheck if the user is really mentioned
if isMentioned(user, string(tr[mentionKeyIdx+1:])) {
return user
}
@ -839,55 +854,59 @@ func (e *AtIndexEntry) String() string {
return fmt.Sprintf("{From: %d, To: %d, Checked: %t, Mentioned: %t, NextAtIdx: %d}", e.From, e.To, e.Checked, e.Mentioned, e.NextAtIdx)
}
// implementation reference: https://github.com/status-im/status-react/blob/04d0252e013d9c67862e77a3467dd32c3abde934/src/status_im/chat/models/mentions.cljs#L433
func calcAtIdxs(state *MentionState) []*AtIndexEntry {
func calculateAtIndexEntries(state *MentionState) ([]*AtIndexEntry, error) {
var keptAtIndexEntries []*AtIndexEntry
var oldRunes []rune
var newRunes []rune
var previousRunes = []rune(state.PreviousText)
switch state.operation {
case textOperationAdd:
newRunes = []rune(state.NewText)
case textOperationDelete:
oldRunes = previousRunes[state.Start : state.End+1]
case textOperationReplace:
oldRunes = previousRunes[state.Start : state.End+1]
newRunes = []rune(state.NewText)
default:
return nil, fmt.Errorf("unknown text operation: %d", state.operation)
}
oldLen := len(oldRunes)
newLen := len(newRunes)
diff := newLen - oldLen
oldAtSignIndexes := getAtSignIdxs(string(oldRunes), state.Start)
newAtSignIndexes := getAtSignIdxs(state.NewText, state.Start)
newAtSignIndexesCount := len(newAtSignIndexes)
if len(state.AtIdxs) == 0 {
result := make([]*AtIndexEntry, newAtSignIndexesCount)
for i, idx := range newAtSignIndexes {
result[i] = &AtIndexEntry{
From: idx,
Checked: false,
}
}
return result
}
newTextLen := len([]rune(state.NewText))
oldTextLen := len([]rune(state.PreviousText))
oldEnd := state.Start + oldTextLen
diff := newTextLen - oldTextLen
var keptAtIdxs []*AtIndexEntry
for _, entry := range state.AtIdxs {
from := entry.From
to := entry.To
toPlus1 := to + 1
if from >= oldEnd {
entry.From = from + diff
entry.To = to + diff
keptAtIdxs = append(keptAtIdxs, entry)
} else if from < state.Start && toPlus1 < state.Start {
// NOTE: (not to+1) means is not checked yet, but (not to+1) seems always false, so we ignore it here temporarily
// https://github.com/status-im/status-mobile/blob/04d0252e013d9c67862e77a3467dd32c3abde934/src/status_im/chat/models/mentions.cljs#L454
keptAtIdxs = append(keptAtIdxs, entry)
} else if from < state.Start && toPlus1 >= state.Start {
keptAtIdxs = append(keptAtIdxs, &AtIndexEntry{
From: from,
Checked: false,
})
deleted := false
for _, idx := range oldAtSignIndexes {
if idx == entry.From {
deleted = true
}
}
if !deleted {
if entry.From >= state.Start { // update range with diff
entry.From += diff
entry.To += diff
}
if entry.From < state.Start && entry.To >= state.Start { // impacted after user edit so need to be rechecked
entry.Checked = false
}
keptAtIndexEntries = append(keptAtIndexEntries, entry)
}
}
return addNewAtSignIndexes(keptAtIndexEntries, newAtSignIndexes), nil
}
func addNewAtSignIndexes(keptAtIdxs []*AtIndexEntry, newAtSignIndexes []int) []*AtIndexEntry {
var newAtIndexEntries []*AtIndexEntry
var added bool
var lastNewIdx *int
var lastNewIdx int
newAtSignIndexesCount := len(newAtSignIndexes)
if newAtSignIndexesCount > 0 {
idx := newAtSignIndexes[newAtSignIndexesCount-1]
lastNewIdx = &idx
lastNewIdx = newAtSignIndexes[newAtSignIndexesCount-1]
}
for _, entry := range keptAtIdxs {
if lastNewIdx != nil && entry.From > *lastNewIdx && !added {
if newAtSignIndexesCount > 0 && !added && entry.From > lastNewIdx {
newAtIndexEntries = append(newAtIndexEntries, makeAtIdxs(newAtSignIndexes)...)
newAtIndexEntries = append(newAtIndexEntries, entry)
added = true
@ -933,7 +952,7 @@ func checkAtIndexEntry(fullText string, entry *AtIndexEntry, mentionableUsers ma
if entry.Checked {
return entry
}
result := MatchMention(fullText+charAtSign, mentionableUsers, entry.From)
result := MatchMention(fullText, mentionableUsers, entry.From)
if result != nil && result.Match != "" {
return &AtIndexEntry{
From: entry.From,
@ -977,37 +996,52 @@ func checkIdxForMentions(fullText string, currentAtIndexEntries []*AtIndexEntry,
return nil
}
func calculateInput(text string, idxs []*AtIndexEntry) []InputSegment {
if len(idxs) == 0 {
return []InputSegment{{Type: Text, Value: text}}
func appendInputSegment(result *[]InputSegment, typ SegmentType, value string, fullText *string) {
if value != "" {
*result = append(*result, InputSegment{Type: typ, Value: value})
*fullText += value
}
idxCount := len(idxs)
lastFrom := idxs[idxCount-1].From
}
func calculateInput(text string, atIndexEntries []*AtIndexEntry) ([]InputSegment, bool) {
if len(atIndexEntries) == 0 {
return []InputSegment{{Type: Text, Value: text}}, true
}
idxCount := len(atIndexEntries)
lastFrom := atIndexEntries[idxCount-1].From
var result []InputSegment
if idxs[0].From != 0 {
result = append(result, InputSegment{Type: Text, Value: subs(text, 0, idxs[0].From)})
fullText := ""
if atIndexEntries[0].From != 0 {
t := subs(text, 0, atIndexEntries[0].From)
appendInputSegment(&result, Text, t, &fullText)
}
for _, entry := range idxs {
for _, entry := range atIndexEntries {
from := entry.From
to := entry.To
nextAtIdx := entry.NextAtIdx
mentioned := entry.Mentioned
if mentioned && nextAtIdx != intUnknown {
result = append(result, InputSegment{Type: Mention, Value: subs(text, from, to+1)})
result = append(result, InputSegment{Type: Text, Value: subs(text, to+1, nextAtIdx)})
t := subs(text, from, to+1)
appendInputSegment(&result, Mention, t, &fullText)
t = subs(text, to+1, nextAtIdx)
appendInputSegment(&result, Text, t, &fullText)
} else if mentioned && lastFrom == from {
result = append(result, InputSegment{Type: Mention, Value: subs(text, from, to+1)})
result = append(result, InputSegment{Type: Text, Value: subs(text, to+1)})
t := subs(text, from, to+1)
appendInputSegment(&result, Mention, t, &fullText)
t = subs(text, to+1)
appendInputSegment(&result, Text, t, &fullText)
} else {
result = append(result, InputSegment{Type: Text, Value: subs(text, from, to+1)})
t := subs(text, from, to+1)
appendInputSegment(&result, Text, t, &fullText)
}
}
return result
return result, fullText == text
}
func subs(s string, start int, end ...int) string {

View File

@ -251,27 +251,6 @@ func TestGetAtSignIdxs(t *testing.T) {
}
}
func TestCalcAtIdxs(t *testing.T) {
state := MentionState{
AtIdxs: []*AtIndexEntry{
{From: 0, To: 3, Checked: false},
},
NewText: "@abc",
PreviousText: "",
Start: 0,
}
want := []*AtIndexEntry{
{From: 0, To: 0, Checked: false},
{From: 4, To: 7, Checked: false},
}
got := calcAtIdxs(&state)
if !reflect.DeepEqual(got, want) {
t.Errorf("calcAtIdxs() = %v, want %v", got, want)
}
}
func TestToInfo(t *testing.T) {
newText := " "
t.Run("toInfo base case", func(t *testing.T) {
@ -891,13 +870,11 @@ func TestMentionSuggestionAtSignSpaceCases(t *testing.T) {
t.Logf("After OnChangeText, Input: %+v, MentionState:%+v, InputSegments:%+v\n", tc.inputText, ctx.MentionState, ctx.InputSegments)
require.Equal(t, tc.expectedSize, len(ctx.MentionSuggestions))
}
require.Len(t, ctx.InputSegments, 3)
require.Equal(t, Mention, ctx.InputSegments[0].Type)
require.Equal(t, "@ @", ctx.InputSegments[0].Value)
require.Len(t, ctx.InputSegments, 2)
require.Equal(t, Text, ctx.InputSegments[0].Type)
require.Equal(t, "@ ", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, "@", ctx.InputSegments[1].Value)
require.Equal(t, Text, ctx.InputSegments[2].Type)
require.Equal(t, "@", ctx.InputSegments[2].Value)
}
func TestSelectMention(t *testing.T) {
@ -923,6 +900,73 @@ func TestSelectMention(t *testing.T) {
require.Equal(t, 0, len(ctx.MentionSuggestions))
}
func TestInputSegments(t *testing.T) {
_, chatID, mentionManager := setupMentionSuggestionTest(t, nil)
ctx, err := mentionManager.OnChangeText(chatID, "@u1")
require.NoError(t, err)
require.Equal(t, 1, len(ctx.InputSegments))
require.Equal(t, Text, ctx.InputSegments[0].Type)
require.Equal(t, "@u1", ctx.InputSegments[0].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u1 @User Number One")
require.NoError(t, err)
require.Equal(t, 2, len(ctx.InputSegments))
require.Equal(t, Text, ctx.InputSegments[0].Type)
require.Equal(t, "@u1 ", ctx.InputSegments[0].Value)
require.Equal(t, Mention, ctx.InputSegments[1].Type)
require.Equal(t, "@User Number One", ctx.InputSegments[1].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u1 @User Number O")
require.NoError(t, err)
require.Equal(t, 2, len(ctx.InputSegments))
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, "@User Number O", ctx.InputSegments[1].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Number One")
require.NoError(t, err)
require.Equal(t, 3, len(ctx.InputSegments))
require.Equal(t, Mention, ctx.InputSegments[0].Type)
require.Equal(t, "@u2", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, " ", ctx.InputSegments[1].Value)
require.Equal(t, Mention, ctx.InputSegments[2].Type)
require.Equal(t, "@User Number One", ctx.InputSegments[2].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Number One a ")
require.NoError(t, err)
require.Equal(t, 4, len(ctx.InputSegments))
require.Equal(t, Mention, ctx.InputSegments[2].Type)
require.Equal(t, "@User Number One", ctx.InputSegments[2].Value)
require.Equal(t, Text, ctx.InputSegments[3].Type)
require.Equal(t, " a ", ctx.InputSegments[3].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u2 @User Numbed One a ")
require.NoError(t, err)
require.Equal(t, 3, len(ctx.InputSegments))
require.Equal(t, Mention, ctx.InputSegments[0].Type)
require.Equal(t, "@u2", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[2].Type)
require.Equal(t, "@User Numbed One a ", ctx.InputSegments[2].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@ @ ")
require.NoError(t, err)
require.Equal(t, 2, len(ctx.InputSegments))
require.Equal(t, Text, ctx.InputSegments[0].Type)
require.Equal(t, "@ ", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, "@ ", ctx.InputSegments[1].Value)
ctx, err = mentionManager.OnChangeText(chatID, "@u3 @ ")
require.NoError(t, err)
require.Equal(t, 3, len(ctx.InputSegments))
require.Equal(t, Mention, ctx.InputSegments[0].Type)
require.Equal(t, "@u3", ctx.InputSegments[0].Value)
require.Equal(t, Text, ctx.InputSegments[1].Type)
require.Equal(t, " ", ctx.InputSegments[1].Value)
require.Equal(t, Text, ctx.InputSegments[2].Type)
require.Equal(t, "@ ", ctx.InputSegments[2].Value)
}
func setupMentionSuggestionTest(t *testing.T, mentionableUserMapInput map[string]*MentionableUser) (map[string]*MentionableUser, string, *MentionManager) {
mentionableUserMap := mentionableUserMapInput
if mentionableUserMap == nil {