Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show mentions as profile names and treat them as indivisible elements #538

Merged
merged 2 commits into from
Oct 7, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 199 additions & 5 deletions js/views/conversation_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -1603,11 +1603,16 @@
this.$messageField.val(),
cursorPos
);
let firstHalf = `${prev}@${member.authorPhoneNumber}`;

const handle = this.memberView.addPubkeyMapping(
member.authorProfileName,
member.authorPhoneNumber
);

let firstHalf = `${prev}${handle}`;
let newCursorPos = firstHalf.length;

const needExtraWhitespace =
end.length === 0 || /[a-fA-F0-9@]/.test(end[0]);
const needExtraWhitespace = end.length === 0 || /\b/.test(end[0]);
if (needExtraWhitespace) {
firstHalf += ' ';
newCursorPos += 1;
Expand Down Expand Up @@ -1810,7 +1815,9 @@
this.model.clearTypingTimers();

const input = this.$messageField;
const message = window.Signal.Emoji.replaceColons(input.val()).trim();

let message = this.memberView.replaceMentions(input.val());
message = window.Signal.Emoji.replaceColons(message).trim();

let toast;
if (extension.expired()) {
Expand Down Expand Up @@ -1853,6 +1860,7 @@
);

input.val('');
this.memberView.deleteMention();
this.setQuoteMessage(null);
this.resetLinkPreview();
this.focusMessageFieldAndClearDisabled();
Expand Down Expand Up @@ -2186,12 +2194,183 @@
}
},

handleDeleteOrBackspace(event, isDelete) {
const $input = this.$messageField[0];
const text = this.$messageField.val();

// Only handle the case when nothing is selected
if ($input.selectionDirection !== 'none') {
// Note: if this ends up deleting a handle, we should
// (ideally) check if we need to update the mapping in
// `this.memberView`, but that's not vital as we already
// reset it on every 'send'
return;
}

const mentions = this.memberView.pendingMentions();

const _ = window.Lodash; // no underscore.js please
const predicate = isDelete ? _.startsWith : _.endsWith;

const pos = $input.selectionStart;
const part = isDelete ? text.substr(pos) : text.substr(0, pos);
sachaaaaa marked this conversation as resolved.
Show resolved Hide resolved

let curMention = null;
_.forEach(mentions, (val, key) => {
sachaaaaa marked this conversation as resolved.
Show resolved Hide resolved
let shouldContinue = true;
if (predicate(part, key)) {
curMention = key;
shouldContinue = false;
}
return shouldContinue;
});

if (!curMention) {
return;
}

event.preventDefault();

const beforeMention = isDelete
? text.substr(0, pos)
: text.substr(0, pos - curMention.length);
const afterMention = isDelete
? text.substr(pos + curMention.length)
: text.substr(pos);

const resText = beforeMention + afterMention;
this.$messageField.val(resText);
$input.selectionStart = pos;
$input.selectionEnd = pos;

this.memberView.deleteMention(curMention);
},

handleLeftRight(event, isLeft) {
// Return next cursor position candidate before we take
// various modifier keys into account
const nextPos = (text, cursorPos, isLeft2, isAltPressed) => {
// If the next char is ' ', skip it if Alt is pressed
let pos = cursorPos;
if (isAltPressed) {
const nextChar = isLeft2
? text.substr(pos - 1, 1)
: text.substr(pos, 1);
if (nextChar === ' ') {
pos = isLeft2 ? pos - 1 : pos + 1;
}
}

const part = isLeft2 ? text.substr(0, pos) : text.substr(pos);

const mentions = this.memberView.pendingMentions();

const predicate = isLeft2
? window.Lodash.endsWith
: window.Lodash.startsWith;

let curMention = null;
_.forEach(mentions, (val, key) => {
sachaaaaa marked this conversation as resolved.
Show resolved Hide resolved
let shouldContinue = true;
if (predicate(part, key)) {
curMention = key;
shouldContinue = false;
}
return shouldContinue;
});

const offset = curMention ? curMention.length : 1;

const resPos = isLeft2 ? Math.max(0, pos - offset) : pos + offset;

return resPos;
};

event.preventDefault();

const $input = this.$messageField[0];

const posStart = $input.selectionStart;
const posEnd = $input.selectionEnd;

const text = this.$messageField.val();

const posToChange =
$input.selectionDirection === 'forward' ? posEnd : posStart;

let newPos = nextPos(text, posToChange, isLeft, event.altKey);

// If command (macos) key is pressed, go to the beginning/end
// (this shouldn't affect Windows, but we should double check that)
if (event.metaKey) {
newPos = isLeft ? 0 : text.length;
}

// Alt would normally make the cursor go until the next whitespace,
// but we need to take the presence of a mention into account
if (event.altKey) {
const searchFrom = isLeft ? posToChange - 1 : posToChange + 1;
const toSearch = isLeft
? text.substr(0, searchFrom)
: text.substr(searchFrom);

// Note: we don't seem to support tabs etc, thus no /\s/
let nextAltPos = isLeft
? toSearch.lastIndexOf(' ')
: toSearch.indexOf(' ');

if (nextAltPos === -1) {
nextAltPos = isLeft ? 0 : text.length;
} else if (isLeft) {
nextAltPos += 1;
}

if (isLeft) {
newPos = Math.min(newPos, nextAltPos);
} else {
newPos = Math.max(newPos, nextAltPos + searchFrom);
}
}

// ==== Handle selection business ====
let newPosStart = newPos;
let newPosEnd = newPos;

let direction = $input.selectionDirection;

if (event.shiftKey) {
if (direction === 'none') {
if (isLeft) {
direction = 'backward';
} else {
direction = 'forward';
}
}
} else {
direction = 'none';
}

if (direction === 'forward') {
newPosStart = posStart;
} else if (direction === 'backward') {
newPosEnd = posEnd;
}

if (newPosStart === newPosEnd) {
direction = 'none';
}

$input.setSelectionRange(newPosStart, newPosEnd, direction);
},

// Note: not only input, but keypresses too (rename?)
handleInputEvent(event) {
// Note: schedule the member list handler shortly afterwards, so
// that the input element has time to update its cursor position to
// what the user would expect
window.requestAnimationFrame(this.maybeShowMembers.bind(this, event));
if (this.model.isPublic()) {
window.requestAnimationFrame(this.maybeShowMembers.bind(this, event));
}

const keyCode = event.which || event.keyCode;

Expand Down Expand Up @@ -2235,6 +2414,20 @@

if (keyPressedLeft || keyPressedRight) {
this.$messageField.trigger('input');
this.handleLeftRight(event, keyPressedLeft);

return;
}

const keyPressedDelete = keyCode === 46;
const keyPressedBackspace = keyCode === 8;

if (keyPressedDelete) {
this.handleDeleteOrBackspace(event, true);
}

if (keyPressedBackspace) {
this.handleDeleteOrBackspace(event, false);
}

this.updateMessageFieldSize();
Expand Down Expand Up @@ -2309,6 +2502,7 @@

let allMembers = window.lokiPublicChatAPI.getListOfMembers();
allMembers = allMembers.filter(d => !!d);
allMembers = allMembers.filter(d => d.authorProfileName !== 'Anonymous');
allMembers = _.uniq(allMembers, true, d => d.authorPhoneNumber);

const cursorPos = event.target.selectionStart;
Expand Down
42 changes: 42 additions & 0 deletions js/views/member_list_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Whisper.MemberListView = Whisper.View.extend({
initialize(options) {
this.member_list = [];
this.memberMapping = {};
this.selected_idx = 0;
this.onClicked = options.onClicked;
this.render();
Expand Down Expand Up @@ -43,6 +44,31 @@
this.render();
}
},
replaceMentions(message) {
let result = message;

// Sort keys from long to short, so we don't have to
// worry about one key being a substring of another
const keys = _.sortBy(_.keys(this.memberMapping), d => -d.length);

keys.forEach(key => {
const pubkey = this.memberMapping[key];
result = result.split(key).join(`@${pubkey}`);
});

return result;
},
pendingMentions() {
return this.memberMapping;
},
deleteMention(mention) {
if (mention) {
delete this.memberMapping[mention];
} else {
// Delete all mentions if no argument is passed
this.memberMapping = {};
}
},
membersShown() {
return this.member_list.length !== 0;
},
Expand All @@ -60,5 +86,21 @@
selectedMember() {
return this.member_list[this.selected_idx];
},
addPubkeyMapping(name, pubkey) {
let handle = `@${name}`;
let chars = 4;

while (
_.has(this.memberMapping, handle) &&
this.memberMapping[handle] !== pubkey
) {
const shortenedPubkey = pubkey.substr(pubkey.length - chars);
handle = `@${name}(..${shortenedPubkey})`;
chars += 1;
}

this.memberMapping[handle] = pubkey;
return handle;
},
});
})();
2 changes: 2 additions & 0 deletions preload.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ if (config.appInstance) {
title += ` - ${config.appInstance}`;
}

window.Lodash = require('lodash');

window.platform = process.platform;
window.getDefaultPoWDifficulty = () => config.defaultPoWDifficulty;
window.getTitle = () => title;
Expand Down