Browse Source

Add method to determine references from message text

JC Brand 7 years ago
parent
commit
f2b017ec89
6 changed files with 135 additions and 6 deletions
  1. 3 3
      spec/autocomplete.js
  2. 54 0
      spec/messages.js
  3. 0 1
      src/converse-autocomplete.js
  4. 1 1
      src/converse-muc-views.js
  5. 62 1
      src/converse-muc.js
  6. 15 0
      src/utils/core.js

+ 3 - 3
spec/autocomplete.js

@@ -13,7 +13,7 @@
     const Strophe = converse.env.Strophe;
     const u = converse.env.utils;
 
-    return describe("A groupchat textarea", function () {
+    describe("The nickname autocomplete feature", function () {
 
         it("shows all autocompletion options when the user presses @",
             mock.initConverseWithPromises(
@@ -146,7 +146,7 @@
                     'stopPropagation': _.noop,
                     'keyCode': 13 // Enter
                 });
-                expect(textarea.value).toBe('hello s some2 ');
+                expect(textarea.value).toBe('hello s @some2 ');
 
                 // Test that pressing tab twice selects
                 presence = $pres({
@@ -166,7 +166,7 @@
 
                 view.keyPressed(tab_event);
                 view.keyUp(tab_event);
-                expect(textarea.value).toBe('hello z3r0 ');
+                expect(textarea.value).toBe('hello @z3r0 ');
 
                 done();
             }).catch(_.partial(console.error, _));

+ 54 - 0
spec/messages.js

@@ -1202,6 +1202,60 @@
             });
         }));
 
+        describe("in which someone is mentioned", function () {
+
+            it("includes XEP-0372 references to that person",
+                mock.initConverseWithPromises(
+                    null, ['rosterGroupsFetched'], {},
+                        function (done, _converse) {
+
+                test_utils.openAndEnterChatRoom(_converse, 'lounge', 'localhost', 'tom')
+                .then(() => {
+                    const view = _converse.chatboxviews.get('lounge@localhost');
+                    ['z3r0', 'mr.robot', 'gibson', 'sw0rdf1sh'].forEach((nick) => {
+                        _converse.connection._dataRecv(test_utils.createRequest(
+                            $pres({
+                                'to': 'tom@localhost/resource',
+                                'from': `lounge@localhost/${nick}`
+                            })
+                            .c('x', {xmlns: Strophe.NS.MUC_USER})
+                            .c('item', {
+                                'affiliation': 'none',
+                                'jid': `${nick}@localhost/resource`,
+                                'role': 'participant'
+                            })));
+                    });
+
+                    let [text, references] = view.model.parseForReferences('hello z3r0')
+                    expect(references.length).toBe(0);
+                    expect(text).toBe('hello z3r0');
+
+                    [text, references] = view.model.parseForReferences('hello @z3r0')
+                    expect(references.length).toBe(1);
+                    expect(text).toBe('hello z3r0');
+                    expect(JSON.stringify(references))
+                        .toBe('[{"begin":6,"end":10,"type":"mention","uri":"xmpp:z3r0@localhost"}]');
+
+                    [text, references] = view.model.parseForReferences('hello @some1 @z3r0 @gibson @mr.robot, how are you?')
+                    expect(text).toBe('hello @some1 z3r0 gibson mr.robot, how are you?');
+                    expect(JSON.stringify(references))
+                        .toBe('[{"begin":13,"end":17,"type":"mention","uri":"xmpp:z3r0@localhost"},'+
+                               '{"begin":18,"end":24,"type":"mention","uri":"xmpp:gibson@localhost"},'+
+                               '{"begin":25,"end":33,"type":"mention","uri":"xmpp:mr.robot@localhost"}]');
+
+                    [text, references] = view.model.parseForReferences('yo @gib')
+                    expect(text).toBe('yo @gib');
+                    expect(references.length).toBe(0);
+
+                    [text, references] = view.model.parseForReferences('yo @gibsonian')
+                    expect(text).toBe('yo @gibsonian');
+                    expect(references.length).toBe(0);
+                    done();
+                }).catch(_.partial(console.error, _));
+            }));
+        });
+
+
         describe("when received from someone else", function () {
 
             it("will open a chatbox and be displayed inside it",

+ 0 - 1
src/converse-autocomplete.js

@@ -237,7 +237,6 @@
                     } else {
                         selected = this.ul.children[this.index];
                     }
-
                     if (selected) {
                         const suggestion = this.suggestions[this.index];
                         this.insertValue(suggestion);

+ 1 - 1
src/converse-muc-views.js

@@ -617,7 +617,7 @@
                         'min_chars': 1,
                         'match_current_word': true,
                         'match_on_tab': true,
-                        'list': () => this.model.occupants.map(o => ({'label': o.get('nick'), 'value': o.get('nick')})),
+                        'list': () => this.model.occupants.map(o => ({'label': o.get('nick'), 'value': `@${o.get('nick')}`})),
                         'filter': _converse.FILTER_STARTSWITH,
                         'trigger_on_at': true
                     });

+ 62 - 1
src/converse-muc.js

@@ -308,14 +308,75 @@
                     _converse.connection.sendPresence(presence);
                 },
 
+                getReferenceForMention (mention, index) {
+                    const longest_match = u.getLongestSubstring(mention, this.occupants.map(o => o.get('nick')));
+                    if (!longest_match) {
+                        return null;
+                    }
+                    if ((mention[longest_match.length] || '').match(/[A-Za-zäëïöüâêîôûáéíóúàèìòùÄËÏÖÜÂÊÎÔÛÁÉÍÓÚÀÈÌÒÙ]/i)) {
+                        // avoid false positives, i.e. mentions that have
+                        // further alphabetical characters than our longest
+                        // match.
+                        return null;
+                    }
+                    const occupant = this.occupants.findOccupant({'nick': longest_match});
+                    if (!occupant) {
+                        return null;
+                    }
+                    const obj = {
+                        'begin': index,
+                        'end': index + longest_match.length,
+                        'type': 'mention'
+                    };
+                    if (occupant.get('jid')) {
+                        obj.uri = `xmpp:${occupant.get('jid')}`
+                    }
+                    return obj;
+                },
+
+                extractReference (text, index) {
+                    for (let i=index; i<text.length; i++) {
+                        if (text[i] !== '@') {
+                            continue
+                        } else {
+                            const match = text.slice(i+1),
+                                  ref = this.getReferenceForMention(match, i)
+                            if (ref) {
+                                return [text.slice(0, i) + match, ref, i]
+                            }
+                        }
+                    }
+                    return;
+                },
+
+                parseForReferences (text) {
+                    const refs = [];
+                    let index = 0;
+                    while (index < (text || '').length) {
+                        const result = this.extractReference(text, index);
+                        if (result) {
+                            text = result[0]; // @ gets filtered out
+                            refs.push(result[1]);
+                            index = result[2];
+                        } else {
+                            break;
+                        }
+                    }
+                    return [text, refs];
+                },
+
                 getOutgoingMessageAttributes (text, spoiler_hint) {
                     const is_spoiler = this.get('composing_spoiler');
+                    var references;
+                    [text, references] = this.parseForReferences(text);
+
                     return {
-                        'nick': this.get('nick'),
                         'from': `${this.get('jid')}/${this.get('nick')}`,
                         'fullname': this.get('nick'),
                         'is_spoiler': is_spoiler,
                         'message': text ? u.httpToGeoUri(emojione.shortnameToUnicode(text), _converse) : undefined,
+                        'nick': this.get('nick'),
+                        'references': references,
                         'sender': 'me',
                         'spoiler_hint': is_spoiler ? spoiler_hint : undefined,
                         'type': 'groupchat'

+ 15 - 0
src/utils/core.js

@@ -98,6 +98,21 @@
 
     var u = {};
 
+    u.getLongestSubstring = function (string, candidates) {
+        function reducer (accumulator, current_value) {
+            if (string.startsWith(current_value)) {
+                if (current_value.length > accumulator.length) {
+                    return current_value;
+                } else {
+                    return accumulator;
+                }
+            } else {
+                return accumulator;
+            }
+        }
+        return candidates.reduce(reducer, '');
+    }
+
     u.getNextElement = function (el, selector='*') {
         let next_el = el.nextElementSibling;
         while (!_.isNull(next_el) && !sizzle.matchesSelector(next_el, selector)) {