Merge branch 'clearnet' into PR_file_server

* clearnet:
  fix method
  separate out new/open, more lint/cleanup
  separate out note to self
  create window.mixpanel here
  use window.mixpanel, remove eventEmitter
  allow mixpanel to be already set up elsewhere
  Address review comments
  Add missing file in tests
  Add UI elements for searching and selecting members in a group chat
  Desktop Analytics
  Replaced value with a descriptive constant.
  Add comment to fix up confusion.
  Keep a cache of the last 5 fetched messages for public chat so we can use it to detect duplicate messages.

# Conflicts:
#	js/background.js
#	js/modules/loki_public_chat_api.js
pull/518/head
sachaaaaa 6 years ago
commit 7756d4f0f3

@ -126,6 +126,7 @@
<div class='bottom-bar' id='footer'>
<div class='emoji-panel-container'></div>
<div class='member-list-container'></div>
<div class='attachment-list'></div>
<div class='compose'>
<form class='send clearfix file-input'>
@ -681,6 +682,7 @@
<script type='text/javascript' src='js/views/message_view.js'></script>
<script type='text/javascript' src='js/views/key_verification_view.js'></script>
<script type='text/javascript' src='js/views/message_list_view.js'></script>
<script type='text/javascript' src='js/views/member_list_view.js'></script>
<script type='text/javascript' src='js/views/group_member_list_view.js'></script>
<script type='text/javascript' src='js/views/recorder_view.js'></script>
<script type='text/javascript' src='js/views/conversation_view.js'></script>

@ -233,6 +233,8 @@
window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey);
// singleton to interface the File server
window.lokiFileServerAPI = new window.LokiFileServerAPI(ourKey);
// are there limits on tracking, is this unneeded?
// window.mixpanel.track("Desktop boot");
window.lokiP2pAPI = new window.LokiP2pAPI(ourKey);
window.lokiP2pAPI.on('pingContact', pubKey => {
const isPing = true;

@ -1,5 +1,5 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController,
clearTimeout, MessageController */
clearTimeout, MessageController, window */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
@ -9,6 +9,7 @@ const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
const PUBLICCHAT_CHAN_POLL_EVERY = 20 * 1000; // 20s
const PUBLICCHAT_DELETION_POLL_EVERY = 5 * 1000; // 5s
const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
class LokiAppDotNetAPI extends EventEmitter {
constructor(ourKey) {
@ -336,6 +337,10 @@ class LokiPublicChannelAPI {
this.deleteLastId = 1;
this.timers = {};
this.running = true;
// Cache for duplicate checking
this.lastMessagesCache = [];
// end properties
log.info(`registered LokiPublicChannel ${channelId}`);
@ -630,6 +635,34 @@ class LokiPublicChannelAPI {
return; // Invalid message
}
// Duplicate check
const isDuplicate = message => {
// The username in this case is the users pubKey
const sameUsername = message.username === adnMessage.user.username;
const sameText = message.text === adnMessage.text;
// Don't filter out messages that are too far apart from each other
const timestampsSimilar =
Math.abs(message.timestamp - timestamp) <=
PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES;
return sameUsername && sameText && timestampsSimilar;
};
// Filter out any messages that we got previously
if (this.lastMessagesCache.some(isDuplicate)) {
return; // Duplicate message
}
// Add the message to the lastMessage cache and keep the last 5 recent messages
this.lastMessagesCache = [
...this.lastMessagesCache,
{
username: adnMessage.user.username,
text: adnMessage.text,
timestamp,
},
].splice(-5);
const messageData = {
serverId: adnMessage.id,
friendRequest: false,
@ -719,8 +752,12 @@ class LokiPublicChannelAPI {
objBody: payload,
});
if (!res.err && res.response) {
window.mixpanel.track('Public Message Sent');
return res.response.data.id;
}
// there's no retry on desktop
// this is supposed to be after retries
window.mixpanel.track('Failed to Send Public Message');
return false;
}
}

@ -178,6 +178,7 @@ class LokiMessageAPI {
try {
// eslint-disable-next-line more/no-then
success = await firstTrue(promises);
window.mixpanel.track('Sent Message Using Swarm API');
} catch (e) {
if (e instanceof textsecure.WrongDifficultyError) {
// Force nonce recalculation
@ -191,6 +192,7 @@ class LokiMessageAPI {
throw e;
}
if (!success) {
window.mixpanel.track('Failed to Send Message Using Swarm API');
throw new window.textsecure.EmptySwarmError(
pubKey,
'Ran out of swarm nodes to query'
@ -255,6 +257,7 @@ class LokiMessageAPI {
} catch (e) {
log.warn('Loki send message:', e);
if (e instanceof textsecure.WrongSwarmError) {
window.mixpanel.track('Migrated Snode');
const { newSwarm } = e;
await lokiSnodeAPI.updateSwarmNodes(params.pubKey, newSwarm);
this.sendingData[params.timestamp].swarm = newSwarm;

@ -0,0 +1,12 @@
const Mixpanel = require('mixpanel');
class LokiMixpanelAPI {
constructor() {
this.mixpanel = Mixpanel.init('736cd9a854a157591153efacd1164e9a');
}
track(label) {
this.mixpanel.track(label);
}
}
module.exports = LokiMixpanelAPI;

@ -118,6 +118,7 @@ class LokiSnodeAPI {
port: snode.storage_port,
}));
} catch (e) {
window.mixpanel.track('Seed Node Failed');
if (seedNodes.length === 0) {
throw new window.textsecure.SeedNodeError(
'Failed to contact seed node'
@ -133,6 +134,7 @@ class LokiSnodeAPI {
const filteredNodes = swarmNodes.filter(
node => node.address !== nodeUrl && node.ip !== nodeUrl
);
window.mixpanel.track('Unreachable Snode');
await conversation.updateSwarmNodes(filteredNodes);
}

@ -43,6 +43,7 @@ const {
const { Lightbox } = require('../../ts/components/Lightbox');
const { LightboxGallery } = require('../../ts/components/LightboxGallery');
const { MainHeader } = require('../../ts/components/MainHeader');
const { MemberList } = require('../../ts/components/conversation/MemberList');
const {
MediaGallery,
} = require('../../ts/components/conversation/media-gallery/MediaGallery');
@ -217,6 +218,7 @@ exports.setup = (options = {}) => {
Lightbox,
LightboxGallery,
MainHeader,
MemberList,
MediaGallery,
Message,
MessageBody,

@ -280,6 +280,13 @@
this.$('.discussion-container').append(this.view.el);
this.view.render();
this.memberView = new Whisper.MemberListView({
el: this.$('.member-list-container'),
onClicked: this.selectMember.bind(this),
});
this.memberView.render();
this.$messageField = this.$('.send-message');
this.onResize = this.forceUpdateMessageFieldSize.bind(this);
@ -307,9 +314,9 @@
events: {
keydown: 'onKeyDown',
'submit .send': 'checkUnverifiedSendMessage',
'input .send-message': 'updateMessageFieldSize',
'keydown .send-message': 'updateMessageFieldSize',
'submit .send': 'handleSubmitPressed',
'input .send-message': 'handleInputEvent',
'keydown .send-message': 'handleInputEvent',
'keyup .send-message': 'onKeyUp',
click: 'onClick',
'click .bottom-bar': 'focusMessageField',
@ -1556,6 +1563,34 @@
dialog.focusCancel();
},
selectMember(member) {
const stripQuery = input => {
const pos = input.lastIndexOf('@');
// This should never happen, but we check just in case
if (pos === -1) {
return input;
}
return input.substr(0, pos);
};
const prev = stripQuery(this.$messageField.val());
const result = `${prev}@${member.authorPhoneNumber} `;
this.$messageField.val(result);
this.$messageField.trigger('input');
},
async handleSubmitPressed(e, options = {}) {
if (this.memberView.membersShown()) {
const member = this.memberView.selectedMember();
this.selectMember(member);
} else {
await this.checkUnverifiedSendMessage(e, options);
}
},
async checkUnverifiedSendMessage(e, options = {}) {
e.preventDefault();
this.sendStart = Date.now();
@ -2112,7 +2147,10 @@
}
},
updateMessageFieldSize(event) {
// Note: not only input, but keypresses too (rename?)
handleInputEvent(event) {
this.maybeShowMembers(event);
const keyCode = event.which || event.keyCode;
if (
@ -2126,6 +2164,41 @@
this.$('.bottom-bar form').submit();
return;
}
const keyPressedLeft = keyCode === 37;
const keyPressedUp = keyCode === 38;
const keyPressedRight = keyCode === 39;
const keyPressedDown = keyCode === 40;
const keyPressedTab = keyCode === 9;
const preventDefault = keyPressedUp || keyPressedDown || keyPressedTab;
if (this.memberView.membersShown() && preventDefault) {
if (keyPressedDown) {
this.memberView.selectDown();
} else if (keyPressedUp) {
this.memberView.selectUp();
} else if (keyPressedTab) {
// Tab is treated as Enter in this context
this.handleSubmitPressed();
}
const $selected = this.$('.member-selected');
if ($selected.length) {
$selected[0].scrollIntoView({ behavior: 'smooth' });
}
event.preventDefault();
return;
}
if (keyPressedLeft || keyPressedRight) {
this.$messageField.trigger('input');
}
this.updateMessageFieldSize();
},
updateMessageFieldSize() {
this.toggleMicrophone();
this.view.measureScrollPosition();
@ -2150,6 +2223,63 @@
this.view.scrollToBottomIfNeeded();
},
maybeShowMembers(event) {
const filterMembers = (caseSensitiveQuery, member) => {
const { authorPhoneNumber, authorProfileName } = member;
const profileName = authorProfileName
? authorProfileName.toLowerCase()
: '';
const query = caseSensitiveQuery.toLowerCase();
if (authorPhoneNumber.includes(query) || profileName.includes(query)) {
return true;
}
return false;
};
const getQuery = input => {
const atPos = input.lastIndexOf('@');
if (atPos === -1) {
return null;
}
// Whitespace is required right before @
if (atPos > 0 && /\w/.test(input.substr(atPos - 1, 1))) {
return null;
}
const query = input.substr(atPos + 1);
// No whitespaces allowed in a query
if (/\s/.test(query)) {
return null;
}
return query;
};
const query = getQuery(event.target.value);
// TODO: for now, extract members from the conversation,
// but change to use a server endpoint in the future
let allMembers = this.model.messageCollection.models.map(
d => d.propsForMessage
);
allMembers = allMembers.filter(d => !!d);
allMembers = _.uniq(allMembers, true, d => d.authorPhoneNumber);
let membersToShow = [];
if (query) {
membersToShow =
query !== ''
? allMembers.filter(m => filterMembers(query, m))
: allMembers;
}
this.memberView.updateMembers(membersToShow);
},
forceUpdateMessageFieldSize(event) {
if (this.isHidden()) {
return;

@ -292,6 +292,7 @@
$target.toggleClass('section-toggle-visible');
},
async openConversation(id, messageId) {
const conversationExists = await ConversationController.get(id);
const conversation = await ConversationController.getOrCreateAndWait(
id,
'private'
@ -302,6 +303,19 @@
}
if (conversation) {
if (conversation.isRss()) {
window.mixpanel.track('RSS Feed Opened');
}
if (conversation.isPublic()) {
window.mixpanel.track('Loki Public Chat Opened');
}
if (conversation.isPrivate()) {
if (conversation.isMe()) {
window.mixpanel.track('Note To Self Opened');
} else if (conversationExists) {
window.mixpanel.track('Conversation Opened');
}
}
conversation.updateProfileName();
}

@ -0,0 +1,64 @@
/* global _, Whisper, */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.MemberListView = Whisper.View.extend({
initialize(options) {
this.member_list = [];
this.selected_idx = 0;
this.onClicked = options.onClicked;
this.render();
},
render() {
if (this.memberView) {
this.memberView.remove();
this.memberView = null;
}
this.memberView = new Whisper.ReactWrapperView({
className: 'member-list',
Component: window.Signal.Components.MemberList,
props: {
members: this.member_list,
selected: this.selectedMember(),
onMemberClicked: this.handleMemberClicked.bind(this),
},
});
this.$el.append(this.memberView.el);
return this;
},
handleMemberClicked(member) {
this.onClicked(member);
},
updateMembers(members) {
if (!_.isEqual(this.member_list, members)) {
// Whenever the list is updated, we reset the selection
this.selected_idx = 0;
this.member_list = members;
this.render();
}
},
membersShown() {
return this.member_list.length !== 0;
},
selectUp() {
this.selected_idx = Math.max(this.selected_idx - 1, 0);
this.render();
},
selectDown() {
this.selected_idx = Math.min(
this.selected_idx + 1,
this.member_list.length - 1
);
this.render();
},
selectedMember() {
return this.member_list[this.selected_idx];
},
});
})();

@ -22,6 +22,9 @@
(function() {
window.textsecure = window.textsecure || {};
// set up mixpanel
window.mixpanel = window.mixpanel || new window.LokiMixpanelAPI();
const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
function AccountManager(username, password) {
@ -136,8 +139,10 @@
).toArrayBuffer();
return libsignal.Curve.async.createKeyPair(privKey);
};
window.mixpanel.track('Seed Restored');
} else {
generateKeypair = libsignal.KeyHelper.generateIdentityKeyPair;
window.mixpanel.track('Seed Created');
}
return this.queueTask(() =>
generateKeypair().then(async identityKeyPair =>

@ -85,6 +85,7 @@
"libsodium-wrappers": "^0.7.4",
"linkify-it": "2.0.3",
"lodash": "4.17.11",
"mixpanel": "^0.10.2",
"mkdirp": "0.5.1",
"moment": "2.21.0",
"mustache": "2.3.0",

@ -333,6 +333,10 @@ window.LokiFileServerAPI = LokiFileServerAPIWrapper(config.defaultFileServer);
window.LokiRssAPI = require('./js/modules/loki_rss_api');
const LokiMixpanelAPI = require('./js/modules/loki_mixpanel.js');
window.mixpanel = new LokiMixpanelAPI();
window.LocalLokiServer = require('./libloki/modules/local_loki_server');
window.localServerPort = config.localServerPort;

@ -0,0 +1,49 @@
.member-list-container {
margin: 0;
padding: 0;
max-height: 240px;
overflow-y: scroll;
.member-item {
padding: 4px;
user-select: none;
&:hover:not(.member-selected) {
background-color: $color-light-20;
}
background-color: $color-light-10;
.name-part {
font-weight: 300;
margin-left: 6px;
}
.pubkey-part {
margin-left: 6px;
}
}
.member-selected {
background-color: $color-light-35;
}
}
.dark-theme {
.member-list-container {
.member-item {
&:hover:not(.member-selected) {
background-color: $color-dark-55;
}
background-color: $color-dark-70;
color: white;
}
.member-selected {
background-color: $color-dark-60;
}
}
}

@ -114,6 +114,7 @@ $color-white-08: rgba($color-white, 0.8);
$color-white-085: rgba($color-white, 0.85);
$color-light-02: #f9fafa;
$color-light-10: #eeefef;
$color-light-20: #c1c5cd;
$color-light-35: #a4a6a9;
$color-light-45: #8b8e91;
$color-light-60: #62656a;

@ -10,6 +10,7 @@
@import 'lightbox';
@import 'recorder';
@import 'emoji';
@import 'mentions';
@import 'settings';
@import 'password';

@ -555,6 +555,7 @@
<script type='text/javascript' src='../js/views/message_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/key_verification_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/message_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/member_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/group_member_list_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/recorder_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/conversation_view.js' data-cover></script>

@ -11,6 +11,8 @@ import { StartNewConversation } from './StartNewConversation';
import { LocalizerType } from '../types/Util';
declare var mixpanel: any;
export type PropsData = {
contacts: Array<ConversationListItemPropsType>;
friends: Array<ConversationListItemPropsType>;
@ -36,7 +38,7 @@ type Props = PropsData & PropsHousekeeping;
export class SearchResults extends React.Component<Props> {
public handleStartNewConversation = () => {
const { regionCode, searchTerm, startNewConversation } = this.props;
mixpanel.track('New Conversation Started');
startNewConversation(searchTerm, { regionCode });
};

@ -0,0 +1,93 @@
import React from 'react';
import classNames from 'classnames';
import { Avatar } from '../Avatar';
interface MemberItemProps {
member: any;
selected: Boolean;
onClicked: any;
}
class MemberItem extends React.Component<MemberItemProps> {
constructor(props: any) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
public render() {
const name = this.props.member.authorProfileName;
const pubkey = this.props.member.authorPhoneNumber;
const selected = this.props.selected;
return (
<div
role="button"
className={classNames(
'member-item',
selected ? 'member-selected' : null
)}
onClick={this.handleClick}
>
{this.renderAvatar()}
<span className="name-part">{name}</span>
<span className="pubkey-part">{pubkey}</span>
</div>
);
}
private handleClick() {
this.props.onClicked(this.props.member);
}
private renderAvatar() {
return (
<Avatar
avatarPath={this.props.member.authorAvatarPath}
color={this.props.member.authorColor}
conversationType="direct"
i18n={this.props.member.i18n}
name={this.props.member.authorName}
phoneNumber={this.props.member.authorPhoneNumber}
profileName={this.props.member.authorProfileName}
size={28}
/>
);
}
}
interface MemberListProps {
members: [any];
selected: any;
onMemberClicked: any;
}
export class MemberList extends React.Component<MemberListProps> {
constructor(props: any) {
super(props);
this.handleMemberClicked = this.handleMemberClicked.bind(this);
}
public render() {
const { members } = this.props;
const itemList = members.map(item => {
const selected = item === this.props.selected;
return (
<MemberItem
key={item.id}
member={item}
selected={selected}
onClicked={this.handleMemberClicked}
/>
);
});
return <div>{itemList}</div>;
}
private handleMemberClicked(member: any) {
this.props.onMemberClicked(member);
}
}

@ -4466,7 +4466,7 @@ https-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73"
https-proxy-agent@^2.2.1:
https-proxy-agent@2.2.1, https-proxy-agent@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
@ -6022,6 +6022,13 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mixpanel@^0.10.2:
version "0.10.2"
resolved "https://registry.yarnpkg.com/mixpanel/-/mixpanel-0.10.2.tgz#10ff6cd76034b262d469094ad3d8c99039345376"
integrity sha512-+zbBQGd/Q5LLRooqJ2iyEDzKz2/ly4TipH5tE9te0BDMJpROxUMGffPulyHbh4FtMcbJuPmIUSIfy//JhhnlnA==
dependencies:
https-proxy-agent "2.2.1"
mkdirp@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.0.tgz#1d73076a6df986cd9344e15e71fcc05a4c9abf12"

Loading…
Cancel
Save