Merge branch 'clearnet' into publicchat_signed

pull/517/head
Ryan Tharp 6 years ago committed by GitHub
commit 71fde0b9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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>

@ -230,6 +230,8 @@
window.feeds = [];
window.lokiMessageAPI = new window.LokiMessageAPI(ourKey);
window.lokiPublicChatAPI = new window.LokiPublicChatAPI(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;

@ -171,6 +171,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
@ -184,6 +185,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'
@ -248,6 +250,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;

@ -1,5 +1,5 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController,
clearTimeout, MessageController, libsignal, StringView */
clearTimeout, MessageController, libsignal, StringView, 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
// singleton to relay events to libtextsecure/message_receiver
class LokiPublicChatAPI extends EventEmitter {
@ -223,6 +224,10 @@ class LokiPublicChannelAPI {
this.timers = {};
this.running = true;
this.logMop = {};
// Cache for duplicate checking
this.lastMessagesCache = [];
// end properties
log.info(`registered LokiPublicChannel ${channelId}`);
@ -663,6 +668,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,
clientVerified: sigValid,
@ -765,8 +798,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;
}
}

@ -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",

@ -328,6 +328,10 @@ window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api');
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