Merge branch 'clearnet' into multi-device

This commit is contained in:
Beaudan Brown 2019-10-23 16:34:56 +11:00
commit d97eced37b
31 changed files with 1134 additions and 947 deletions

View File

@ -10,7 +10,7 @@ linux:
- yarn install --frozen-lockfile
- export SIGNAL_ENV=production
- yarn generate
- $(yarn bin)/build --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
- $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
cache:
paths:
- node_modules/
@ -27,7 +27,7 @@ osx:
- yarn install --frozen-lockfile
- export SIGNAL_ENV=production
- yarn generate
- $(yarn bin)/build --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
- $(yarn bin)/electron-builder --config.extraMetadata.environment=$SIGNAL_ENV --config.mac.bundleVersion='$CI_COMMIT_REF_SLUG' --publish=never --config.directories.output=release
cache:
paths:
- node_modules/
@ -51,7 +51,7 @@ windows:
- call yarn generate
- call node build\grunt.js
- call yarn prepare-beta-build
- call node_modules\.bin\build --config.extraMetadata.environment=%SIGNAL_ENV% --publish=never --config.directories.output=release
- call node_modules\.bin\electron-builder --config.extraMetadata.environment=%SIGNAL_ENV% --publish=never --config.directories.output=release
- call node build\grunt.js test-release:win
cache:
paths:

View File

@ -166,6 +166,11 @@
"description":
"Only available on development modes, menu option to open up the standalone device setup sequence"
},
"connectingLoad": {
"message": "Connecting...",
"description":
"Message shown on the as a loading screen while we are connecting to something"
},
"loading": {
"message": "Loading...",
"description":
@ -1253,8 +1258,7 @@
"Additional detail provided for Link Previews option in settings screen"
},
"linkPreviewsSettingDescription": {
"message":
"Enable local link previews (Restart for changes to take effect).",
"message": "Enable local link previews",
"description": "Description shown for the Link Preview option "
},
"spellCheckDescription": {
@ -1923,7 +1927,8 @@
"Shown in the settings page as the heading for the blocked user settings"
},
"editProfileTitle": {
"message": "Change your own display name",
"message":
"Change your own display name (alphanumeric characters and underscores only)",
"description": "The title shown when user edits their own profile"
},
"editProfileDisplayNameWarning": {
@ -1964,6 +1969,16 @@
"message": "Show QR code",
"description": "Button action that the user can click to view their QR code"
},
"showAddServer": {
"message": "Add public server",
"description":
"Button action that the user can click to connect to a new public server"
},
"addServerDialogTitle": {
"message": "Connect to new public server",
"description":
"Title for the dialog box used to connect to a new public server"
},
"seedViewTitle": {
"message":
@ -2015,6 +2030,15 @@
"passwordsDoNotMatch": {
"message": "Passwords do not match"
},
"publicChatExists": {
"message": "You are already connected to this public channel"
},
"connectToServerFail": {
"message": "Failed to connect to server. Check URL"
},
"connectToServerSuccess": {
"message": "Successfully connected to new public chat server"
},
"setPasswordFail": {
"message": "Failed to set password"
},

View File

@ -136,9 +136,9 @@
<div class='capture-audio hide'>
<button class='microphone' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
</div>
<div class='choose-file hide'>
<div id='choose-file' class='choose-file'>
<button class='paperclip thumbnail' {{#disable-inputs}} disabled="disabled" {{/disable-inputs}}></button>
<input type='file' class='file-input' multiple='multiple'>
<input type='file' class='file-input' multiple='multiple' accept='video/* image/*'>
</div>
</div>
</form>
@ -328,6 +328,29 @@
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='connecting-to-server-template'>
<div class="content">
{{ #title }}
<h4>{{ title }}</h4>
{{ /title }}
<div class='buttons'>
<button class='cancel' tabindex='2'>{{ cancel }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='add-server-template'>
<div class="content">
{{ #title }}
<h4>{{ title }}</h4>
{{ /title }}
<input type='text' id='server-url' placeholder='Server Url' autofocus>
<div class='error'></div>
<div class='buttons'>
<button class='cancel' tabindex='2'>{{ cancel }}</button>
<button class='ok' tabindex='1'>{{ ok }}</button>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='qr-code-template'>
<div class="content">
<div id="qr">
@ -688,7 +711,7 @@
<!-- Profile -->
<div class='page'>
<div class='display-name-input'>
<div class='input-header'>Enter your public display name (alphanumeric characters and spaces only)</div>
<div class='input-header'>Enter your public display name (alphanumeric characters and underscores only)</div>
<input class='form-control' type='text' id='display-name' placeholder='Display Name (optional)' autocomplete='off' spellcheck='false' maxlength='25'>
</div>
<div class='password-inputs'>
@ -733,7 +756,6 @@
<script type='text/javascript' src='js/expire.js'></script>
<script type='text/javascript' src='js/conversation_controller.js'></script>
<script type='text/javascript' src='js/blocked_number_controller.js'></script>
<script type='text/javascript' src='js/link_previews_helper.js'></script>
<script type='text/javascript' src='js/message_controller.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
@ -759,6 +781,8 @@
<script type='text/javascript' src='js/views/password_dialog_view.js'></script>
<script type='text/javascript' src='js/views/seed_dialog_view.js'></script>
<script type='text/javascript' src='js/views/qr_dialog_view.js'></script>
<script type='text/javascript' src='js/views/connecting_to_server_dialog_view.js'></script>
<script type='text/javascript' src='js/views/add_server_dialog_view.js'></script>
<script type='text/javascript' src='js/views/beta_release_disclaimer_view.js'></script>
<script type='text/javascript' src='js/views/identicon_svg_view.js'></script>
<script type='text/javascript' src='js/views/install_view.js'></script>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Mac distribution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@ -2,6 +2,7 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Mac app store -->
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.network.client</key>

28
build/notarize.js Normal file
View File

@ -0,0 +1,28 @@
const { notarize } = require('electron-notarize');
/*
Pre-requisites: https://github.com/electron/electron-notarize#prerequisites
1. Generate an app specific password
2. Export SIGNING_APPLE_ID, SIGNING_APP_PASSWORD, SIGNING_TEAM_ID environment variables
*/
/*
Notarizing: https://kilianvalkhof.com/2019/electron/notarizing-your-electron-application/
*/
exports.default = async function notarizing(context) {
const { electronPlatformName, appOutDir } = context;
if (electronPlatformName !== 'darwin') {
return;
}
const appName = context.packager.appInfo.productFilename;
return notarize({
appBundleId: 'com.loki-project.messenger-desktop',
appPath: `${appOutDir}/${appName}.app`,
appleId: process.env.SIGNING_APPLE_ID,
appleIdPassword: process.env.SIGNING_APP_PASSWORD,
ascProvider: process.env.SIGNING_TEAM_ID,
});
};

View File

@ -245,6 +245,9 @@
window.lokiPublicChatAPI = new window.LokiPublicChatAPI(ourKey);
// singleton to interface the File server
window.lokiFileServerAPI = new window.LokiFileServerAPI(ourKey);
await window.lokiFileServerAPI.establishConnection(
window.getDefaultFileServer()
);
// are there limits on tracking, is this unneeded?
// window.mixpanel.track("Desktop boot");
window.lokiP2pAPI = new window.LokiP2pAPI(ourKey);
@ -624,6 +627,11 @@
Whisper.events.on('registration_done', async () => {
window.log.info('handling registration event');
// Enable link previews as default
storage.onready(async () => {
storage.put('linkPreviews', true);
});
// listeners
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
// window.Signal.RefreshSenderCertificate.initialize({
@ -772,6 +780,12 @@
}
});
Whisper.events.on('showAddServerDialog', async options => {
if (appView) {
appView.showAddServerDialog(options);
}
});
Whisper.events.on('showQRDialog', async () => {
if (appView) {
const ourNumber = textsecure.storage.user.getNumber();

View File

@ -1,168 +0,0 @@
/* global
Signal,
textsecure,
StringView
*/
/* eslint-disable no-bitwise */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Signal = window.Signal || {};
window.Signal.LinkPreviews = window.Signal.LinkPreviews || {};
// A cache mapping url to fetched previews
const previewCache = {};
async function makeChunkedRequest(url) {
const PARALLELISM = 3;
const size = await textsecure.messaging.getProxiedSize(url);
const chunks = await Signal.LinkPreviews.getChunkPattern(size);
let results = [];
const jobs = chunks.map(chunk => async () => {
const { start, end } = chunk;
const result = await textsecure.messaging.makeProxiedRequest(url, {
start,
end,
returnArrayBuffer: true,
});
return {
...chunk,
...result,
};
});
while (jobs.length > 0) {
const activeJobs = [];
for (let i = 0, max = PARALLELISM; i < max; i += 1) {
if (!jobs.length) {
break;
}
const job = jobs.shift();
activeJobs.push(job());
}
// eslint-disable-next-line no-await-in-loop
results = results.concat(await Promise.all(activeJobs));
}
if (!results.length) {
throw new Error('No responses received');
}
const { contentType } = results[0];
const data = Signal.LinkPreviews.assembleChunks(results);
return {
contentType,
data,
};
}
async function sha256(string) {
const arraybuffer = new TextEncoder('utf-8').encode(string);
const digest = await window.crypto.subtle.digest('SHA-256', arraybuffer);
return StringView.arrayBufferToHex(digest);
}
// Wrapper function which utilizes cache
async function getPreview(url, skipCache = false) {
// If we have a request cached then use that
if (!skipCache && url in previewCache) {
return previewCache[url];
}
// Start the request
const promise = _getPreview(url).catch(e => {
window.log.error(e);
// If we get an error then we can purge the cache
if (url in previewCache) {
delete previewCache[url];
}
return null;
});
previewCache[url] = promise;
return promise;
}
async function _getPreview(url) {
let html;
try {
html = await textsecure.messaging.makeProxiedRequest(url);
} catch (error) {
if (error.code >= 300) {
throw new Error(`Failed to fetch html: ${error}`);
}
}
const title = Signal.LinkPreviews.getTitleMetaTag(html);
const imageUrl = Signal.LinkPreviews.getImageMetaTag(html);
let image;
let objectUrl;
try {
if (imageUrl) {
if (!Signal.LinkPreviews.isMediaLinkInWhitelist(imageUrl)) {
const primaryDomain = Signal.LinkPreviews.getDomain(url);
const imageDomain = Signal.LinkPreviews.getDomain(imageUrl);
throw new Error(
`imageUrl for domain ${primaryDomain} did not match media whitelist. Domain: ${imageDomain}`
);
}
const data = await makeChunkedRequest(imageUrl);
// Calculate dimensions
const file = new Blob([data.data], {
type: data.contentType,
});
objectUrl = URL.createObjectURL(file);
const dimensions = await Signal.Types.VisualAttachment.getImageDimensions(
{
objectUrl,
logger: window.log,
}
);
image = {
...data,
...dimensions,
contentType: file.type,
};
}
} catch (error) {
// We still want to show the preview if we failed to get an image
window.log.error(
'getPreview failed to get image for link preview:',
error.message
);
} finally {
if (objectUrl) {
URL.revokeObjectURL(objectUrl);
}
}
const hash = await sha256(url);
return {
title,
url,
image,
hash,
};
}
window.Signal.LinkPreviews.helper = {
getPreview,
};
})();

View File

@ -1421,7 +1421,7 @@
options.messageType = message.get('type');
options.isPublic = this.isPublic();
if (options.isPublic) {
options.publicSendData = this.getPublicSendData();
options.publicSendData = await this.getPublicSendData();
}
const groupNumbers = this.getRecipients();
@ -2122,6 +2122,21 @@
};
},
// maybe "Backend" instead of "Source"?
async setPublicSource(newServer, newChannelId) {
if (!this.isPublic()) {
return;
}
if (
this.get('server') !== newServer ||
this.get('channelId') !== newChannelId
) {
this.set({ server: newServer });
this.set({ channelId: newChannelId });
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
}
},
getPublicSource() {
if (!this.isPublic()) {
return null;
@ -2132,10 +2147,18 @@
conversationId: this.get('id'),
};
},
getPublicSendData() {
const serverAPI = lokiPublicChatAPI.findOrCreateServer(
async getPublicSendData() {
const serverAPI = await lokiPublicChatAPI.findOrCreateServer(
this.get('server')
);
if (!serverAPI) {
window.log.warn(
`Failed to get serverAPI (${this.get('server')}) for conversation (${
this.id
})`
);
return null;
}
const channelAPI = serverAPI.findOrCreateChannel(
this.get('channelId'),
this.id
@ -2397,6 +2420,9 @@
async deletePublicMessage(message) {
const channelAPI = this.getPublicSendData();
if (!channelAPI) {
return false;
}
const success = await channelAPI.deleteMessage(message.getServerId());
if (success) {
this.removeMessage(message.id);

View File

@ -22,13 +22,7 @@
window.Whisper = window.Whisper || {};
const {
Message: TypedMessage,
Contact,
PhoneNumber,
Attachment,
Errors,
} = Signal.Types;
const { Message: TypedMessage, Contact, PhoneNumber, Errors } = Signal.Types;
const {
deleteExternalMessageFiles,
@ -36,7 +30,6 @@
loadAttachmentData,
loadQuoteData,
loadPreviewData,
writeAttachment,
upgradeMessageSchema,
} = window.Signal.Migrations;
const { bytesFromString } = window.Signal.Crypto;
@ -97,9 +90,6 @@
this.on('unload', this.unload);
this.on('expired', this.onExpired);
this.setToExpire();
this.updatePreview();
// Keep props ready
const generateProps = () => {
if (this.isExpirationTimerUpdate()) {
@ -162,73 +152,6 @@
// eslint-disable-next-line no-bitwise
return !!(this.get('flags') & flag);
},
async updatePreview() {
// Don't generate link previews if user has turned them off
if (!storage.get('linkPreviews', false)) {
return;
}
if (this.updatingPreview) {
return;
}
// Only update the preview if we don't have any set
const preview = this.get('preview');
if (!_.isEmpty(preview)) {
return;
}
// Make sure we have links we can preview
const links = Signal.LinkPreviews.findLinks(this.get('body'));
const firstLink = links.find(link =>
Signal.LinkPreviews.isLinkInWhitelist(link)
);
if (!firstLink) {
return;
}
this.updatingPreview = true;
try {
const result = await Signal.LinkPreviews.helper.getPreview(firstLink);
const { image, title, hash } = result;
// A link preview isn't worth showing unless we have either a title or an image
if (!result || !(image || title)) {
this.updatingPreview = false;
return;
}
// Save the image to disk
const { data } = image;
const extension = Attachment.getFileExtension(image);
if (data && extension) {
const hash32 = hash.substring(0, 32);
try {
const filePath = await writeAttachment({
data,
path: `previews/${hash32}.${extension}`,
});
// return the image without the data
result.image = _.omit({ ...image, path: filePath }, 'data');
} catch (e) {
window.log.warn('Failed to write preview to disk', e);
}
}
// Save it!!
this.set({ preview: [result] });
await window.Signal.Data.saveMessage(this.attributes, {
Message: Whisper.Message,
});
} catch (e) {
window.log.warn(`Failed to load previews for message: ${this.id}`);
} finally {
this.updatingPreview = false;
}
},
getEndSessionTranslationKey() {
const sessionType = this.get('endSessionType');
if (sessionType === 'ongoing') {
@ -1891,9 +1814,6 @@
schemaVersion: dataMessage.schemaVersion,
});
// Update the previews if we need to
message.updatePreview();
if (type === 'outgoing') {
const receipts = Whisper.DeliveryReceipts.forMessage(
conversation,
@ -1990,7 +1910,10 @@
} else {
const ourNumber = textsecure.storage.user.getNumber();
if (message.attributes.body.indexOf(`@${ourNumber}`) !== -1) {
if (
message.attributes.body &&
message.attributes.body.indexOf(`@${ourNumber}`) !== -1
) {
conversation.set({ mentionedUs: true });
}
@ -2086,6 +2009,14 @@
}
} else {
await conversation.onFriendRequestAccepted();
// We need to return for these types of messages because android struggles
if (
!message.get('body') &&
!message.get('attachments').length &&
!message.get('preview').length
) {
return;
}
}
const id = await window.Signal.Data.saveMessage(message.attributes, {
Message: Whisper.Message,

View File

@ -1,8 +1,9 @@
/* global log, textsecure, libloki, Signal, Whisper, Headers, ConversationController,
clearTimeout, MessageController, libsignal, StringView, window, _ */
clearTimeout, MessageController, libsignal, StringView, window, _, dcodeIO, Buffer */
const EventEmitter = require('events');
const nodeFetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const FormData = require('form-data');
// Can't be less than 1200 if we have unauth'd requests
const PUBLICCHAT_MSG_POLL_EVERY = 1.5 * 1000; // 1.5s
@ -11,6 +12,10 @@ 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
const ATTACHMENT_TYPE = 'net.app.core.oembed';
const LOKI_ATTACHMENT_TYPE = 'attachment';
const LOKI_PREVIEW_TYPE = 'preview';
class LokiAppDotNetAPI extends EventEmitter {
constructor(ourKey) {
super();
@ -32,21 +37,32 @@ class LokiAppDotNetAPI extends EventEmitter {
}
// server getter/factory
findOrCreateServer(serverUrl) {
async findOrCreateServer(serverUrl) {
let thisServer = this.servers.find(
server => server.baseServerUrl === serverUrl
);
if (!thisServer) {
log.info(`LokiAppDotNetAPI creating ${serverUrl}`);
thisServer = new LokiAppDotNetServerAPI(this, serverUrl);
const gotToken = await thisServer.getOrRefreshServerToken();
if (!gotToken) {
log.warn(`Invalid server ${serverUrl}`);
return null;
}
log.info(`set token ${thisServer.token}`);
this.servers.push(thisServer);
}
return thisServer;
}
// channel getter/factory
findOrCreateChannel(serverUrl, channelId, conversationId) {
const server = this.findOrCreateServer(serverUrl);
async findOrCreateChannel(serverUrl, channelId, conversationId) {
const server = await this.findOrCreateServer(serverUrl);
if (!server) {
log.error(`Failed to create server for: ${serverUrl}`);
return null;
}
return server.findOrCreateChannel(channelId, conversationId);
}
@ -86,11 +102,6 @@ class LokiAppDotNetServerAPI {
this.channels = [];
this.tokenPromise = null;
this.baseServerUrl = url;
const ref = this;
(async function justToEnableAsyncToGetToken() {
ref.token = await ref.getOrRefreshServerToken();
log.info(`set token ${ref.token}`);
})();
}
async close() {
@ -185,14 +196,14 @@ class LokiAppDotNetServerAPI {
// request an token from the server
async requestToken() {
const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`);
const params = {
pubKey: this.chatAPI.ourKey,
};
url.search = new URLSearchParams(params);
let res;
try {
const url = new URL(`${this.baseServerUrl}/loki/v1/get_challenge`);
const params = {
pubKey: this.chatAPI.ourKey,
};
url.search = new URLSearchParams(params);
res = await nodeFetch(url);
} catch (e) {
return null;
@ -231,21 +242,24 @@ class LokiAppDotNetServerAPI {
// make a request to the server
async serverRequest(endpoint, options = {}) {
const { params = {}, method, objBody, forceFreshToken = false } = options;
const {
params = {},
method,
rawBody,
objBody,
forceFreshToken = false,
} = options;
const url = new URL(`${this.baseServerUrl}/${endpoint}`);
if (params) {
url.search = new URLSearchParams(params);
}
let result;
let { token } = this;
const token = await this.getOrRefreshServerToken();
if (!token) {
token = await this.getOrRefreshServerToken();
if (!token) {
log.error('NO TOKEN');
return {
err: 'noToken',
};
}
log.error('NO TOKEN');
return {
err: 'noToken',
};
}
try {
const fetchOptions = {};
@ -258,6 +272,8 @@ class LokiAppDotNetServerAPI {
if (objBody) {
headers['Content-Type'] = 'application/json';
fetchOptions.body = JSON.stringify(objBody);
} else if (rawBody) {
fetchOptions.body = rawBody;
}
fetchOptions.headers = new Headers(headers);
result = await nodeFetch(url, fetchOptions || undefined);
@ -344,6 +360,43 @@ class LokiAppDotNetServerAPI {
return false;
}
async uploadData(data) {
const endpoint = 'files';
const options = {
method: 'POST',
rawBody: data,
};
const { statusCode, response } = await this.serverRequest(
endpoint,
options
);
if (statusCode !== 200) {
log.warn('Failed to upload data to fileserver');
return null;
}
const url = response.data && response.data.url;
const id = response.data && response.data.id;
return {
url,
id,
};
}
putAttachment(attachmentBin) {
const formData = new FormData();
const buffer = Buffer.from(attachmentBin);
formData.append('type', 'network.loki');
formData.append('content', buffer, {
contentType: 'application/octet-stream',
name: 'content',
filename: 'attachment',
});
return this.uploadData(formData);
}
}
class LokiPublicChannelAPI {
@ -606,6 +659,32 @@ class LokiPublicChannelAPI {
}
}
static getSigData(
sigVer,
noteValue,
attachmentAnnotations,
previewAnnotations,
adnMessage
) {
let sigString = '';
sigString += adnMessage.text.trim();
sigString += noteValue.timestamp;
if (noteValue.quote) {
sigString += noteValue.quote.id;
sigString += noteValue.quote.author;
sigString += noteValue.quote.text.trim();
if (adnMessage.reply_to) {
sigString += adnMessage.reply_to;
}
}
sigString += [...attachmentAnnotations, ...previewAnnotations]
.map(data => data.id || data.image.id)
.sort()
.join();
sigString += sigVer;
return dcodeIO.ByteBuffer.wrap(sigString, 'utf8').toArrayBuffer();
}
async getMessengerData(adnMessage) {
if (
!Array.isArray(adnMessage.annotations) ||
@ -624,31 +703,33 @@ class LokiPublicChannelAPI {
const { timestamp, quote } = noteValue;
if (quote) {
// TODO: Enable quote attachments again using proper ADN style
quote.attachments = [];
}
// try to verify signature
const { sig, sigver } = noteValue;
const annoCopy = [...adnMessage.annotations];
const attachments = annoCopy
.filter(anno => anno.value.lokiType === LOKI_ATTACHMENT_TYPE)
.map(attachment => ({ isRaw: true, ...attachment.value }));
const preview = annoCopy
.filter(anno => anno.value.lokiType === LOKI_PREVIEW_TYPE)
.map(LokiPublicChannelAPI.getPreviewFromAnnotation);
// strip out sig and sigver
annoCopy[0] = _.omit(annoCopy[0], ['value.sig', 'value.sigver']);
const verifyObj = {
text: adnMessage.text,
version: sigver,
annotations: annoCopy,
};
if (adnMessage.reply_to) {
verifyObj.reply_to = adnMessage.reply_to;
}
const sigData = LokiPublicChannelAPI.getSigData(
sigver,
noteValue,
attachments,
preview,
adnMessage
);
const pubKeyBin = StringView.hexToArrayBuffer(adnMessage.user.username);
const sigBin = StringView.hexToArrayBuffer(sig);
try {
await libsignal.Curve.async.verifySignature(
pubKeyBin,
JSON.stringify(verifyObj),
sigBin
);
await libsignal.Curve.async.verifySignature(pubKeyBin, sigData, sigBin);
} catch (e) {
if (e.message === 'Invalid signature') {
// keep noise out of the logs, once per start up is enough
@ -679,6 +760,8 @@ class LokiPublicChannelAPI {
return {
timestamp,
attachments,
preview,
quote,
};
}
@ -727,7 +810,6 @@ class LokiPublicChannelAPI {
!adnMessage.id ||
!adnMessage.user ||
!adnMessage.user.username || // pubKey lives in the username field
!adnMessage.user.name || // profileName lives in the name field
!adnMessage.text ||
adnMessage.is_deleted
) {
@ -739,7 +821,7 @@ class LokiPublicChannelAPI {
return;
}
const { timestamp, quote } = messengerData;
const { timestamp, quote, attachments, preview } = messengerData;
if (!timestamp) {
return; // Invalid message
}
@ -772,7 +854,7 @@ class LokiPublicChannelAPI {
},
].splice(-5);
const from = adnMessage.user.name; // profileName
const from = adnMessage.user.name || 'Anonymous'; // profileName
const messageData = {
serverId: adnMessage.id,
@ -785,8 +867,9 @@ class LokiPublicChannelAPI {
receivedAt,
isPublic: true,
message: {
body: adnMessage.text,
attachments: [],
body:
adnMessage.text === timestamp.toString() ? '' : adnMessage.text,
attachments,
group: {
id: this.conversationId,
type: textsecure.protobuf.GroupContext.Type.DELIVER,
@ -799,7 +882,7 @@ class LokiPublicChannelAPI {
sent_at: timestamp,
quote,
contact: [],
preview: [],
preview,
profile: {
displayName: from,
},
@ -818,8 +901,85 @@ class LokiPublicChannelAPI {
}
}
static getPreviewFromAnnotation(annotation) {
const preview = {
title: annotation.value.linkPreviewTitle,
url: annotation.value.linkPreviewUrl,
image: {
isRaw: true,
caption: annotation.value.caption,
contentType: annotation.value.contentType,
digest: annotation.value.digest,
fileName: annotation.value.fileName,
flags: annotation.value.flags,
height: annotation.value.height,
id: annotation.value.id,
key: annotation.value.key,
size: annotation.value.size,
thumbnail: annotation.value.thumbnail,
url: annotation.value.url,
width: annotation.value.width,
},
};
return preview;
}
static getAnnotationFromPreview(preview) {
const annotation = {
type: ATTACHMENT_TYPE,
value: {
// Mandatory ADN fields
version: '1.0',
lokiType: LOKI_PREVIEW_TYPE,
// Signal stuff we actually care about
linkPreviewTitle: preview.title,
linkPreviewUrl: preview.url,
caption: preview.image.caption,
contentType: preview.image.contentType,
digest: preview.image.digest,
fileName: preview.image.fileName,
flags: preview.image.flags,
height: preview.image.height,
id: preview.image.id,
key: preview.image.key,
size: preview.image.size,
thumbnail: preview.image.thumbnail,
url: preview.image.url,
width: preview.image.width,
},
};
return annotation;
}
static getAnnotationFromAttachment(attachment) {
const type = attachment.contentType.match(/^image/) ? 'photo' : 'video';
const annotation = {
type: ATTACHMENT_TYPE,
value: {
// Mandatory ADN fields
version: '1.0',
type,
lokiType: LOKI_ATTACHMENT_TYPE,
// Signal stuff we actually care about
...attachment,
},
};
return annotation;
}
// create a message in the channel
async sendMessage(text, quote, messageTimeStamp) {
async sendMessage(data, messageTimeStamp) {
const { quote, attachments, preview } = data;
const text = data.body || messageTimeStamp.toString();
const attachmentAnnotations = attachments.map(
LokiPublicChannelAPI.getAnnotationFromAttachment
);
const previewAnnotations = preview.map(
LokiPublicChannelAPI.getAnnotationFromPreview
);
const payload = {
text,
annotations: [
@ -829,6 +989,8 @@ class LokiPublicChannelAPI {
timestamp: messageTimeStamp,
},
},
...attachmentAnnotations,
...previewAnnotations,
],
};
if (quote && quote.id) {
@ -853,20 +1015,24 @@ class LokiPublicChannelAPI {
}
}
const privKey = await this.serverAPI.chatAPI.getPrivateKey();
const objToSign = {
version: 1,
text,
annotations: payload.annotations,
};
const sigVer = 1;
const mockAdnMessage = { text };
if (payload.reply_to) {
objToSign.reply_to = payload.reply_to;
mockAdnMessage.reply_to = payload.reply_to;
}
const sigData = LokiPublicChannelAPI.getSigData(
sigVer,
payload.annotations[0].value,
attachmentAnnotations.map(anno => anno.value),
previewAnnotations.map(anno => anno.value),
mockAdnMessage
);
const sig = await libsignal.Curve.async.calculateSignature(
privKey,
JSON.stringify(objToSign)
sigData
);
payload.annotations[0].value.sig = StringView.arrayBufferToHex(sig);
payload.annotations[0].value.sigver = objToSign.version;
payload.annotations[0].value.sigver = sigVer;
const res = await this.serverRequest(`${this.baseChannelUrl}/messages`, {
method: 'POST',
objBody: payload,

View File

@ -1,20 +1,23 @@
/* global storage: false */
/* global Signal: false */
/* global log: false */
const LokiAppDotNetAPI = require('./loki_app_dot_net_api');
const DEVICE_MAPPING_ANNOTATION_KEY = 'network.loki.messenger.devicemapping';
// returns the LokiFileServerAPI constructor with the serverUrl already consumed
function LokiFileServerAPIWrapper(serverUrl) {
return LokiFileServerAPI.bind(null, serverUrl);
}
class LokiFileServerAPI {
constructor(serverUrl, ourKey) {
constructor(ourKey) {
this.ourKey = ourKey;
this._adnApi = new LokiAppDotNetAPI(ourKey);
this._server = this._adnApi.findOrCreateServer(serverUrl);
}
async establishConnection(serverUrl) {
this._server = await this._adnApi.findOrCreateServer(serverUrl);
// TODO: Handle this failure gracefully
if (!this._server) {
log.error('Failed to establish connection to file server');
}
}
async getUserDeviceMapping(pubKey) {
@ -50,6 +53,10 @@ class LokiFileServerAPI {
content
);
}
uploadPrivateAttachment(data) {
return this._server.uploadData(data);
}
}
module.exports = LokiFileServerAPIWrapper;
module.exports = LokiFileServerAPI;

View File

@ -91,11 +91,12 @@ class LokiMessageAPI {
};
if (isPublic) {
const res = await publicSendData.sendMessage(
data.body,
data.quote,
messageTimeStamp
);
if (!publicSendData) {
throw new window.textsecure.PublicChatError(
'Missing public send data for public chat message'
);
}
const res = await publicSendData.sendMessage(data, messageTimeStamp);
if (res === false) {
throw new window.textsecure.PublicChatError(
'Failed to send public chat message'

View File

@ -2,10 +2,11 @@ const WebSocket = require('websocket').w3cwebsocket;
const fetch = require('node-fetch');
const ProxyAgent = require('proxy-agent');
const { Agent } = require('https');
const FormData = require('form-data');
const is = require('@sindresorhus/is');
/* global Buffer, setTimeout, log, _ */
/* global Buffer, setTimeout, log, _, lokiFileServerAPI */
/* eslint-disable more/no-then, no-bitwise, no-nested-ternary */
@ -843,41 +844,27 @@ function initialize({
});
}
function getAttachment(id) {
return _ajax({
call: 'attachment',
httpType: 'GET',
urlParameters: `/${id}`,
responseType: 'json',
validateResponse: { location: 'string' },
}).then(response =>
// Using _outerAJAX, since it's not hardcoded to the Signal Server
_outerAjax(response.location, {
contentType: 'application/octet-stream',
proxyUrl,
responseType: 'arraybuffer',
timeout: 0,
type: 'GET',
})
);
function getAttachment(fileUrl) {
return _outerAjax(fileUrl, {
contentType: 'application/octet-stream',
proxyUrl,
responseType: 'arraybuffer',
timeout: 0,
type: 'GET',
});
}
function putAttachment(encryptedBin) {
return _ajax({
call: 'attachment',
httpType: 'GET',
responseType: 'json',
}).then(response =>
// Using _outerAJAX, since it's not hardcoded to the Signal Server
_outerAjax(response.location, {
contentType: 'application/octet-stream',
data: encryptedBin,
processData: false,
proxyUrl,
timeout: 0,
type: 'PUT',
}).then(() => response.idString)
);
const formData = new FormData();
const buffer = Buffer.from(encryptedBin);
formData.append('type', 'network.loki');
formData.append('content', buffer, {
contentType: 'application/octet-stream',
name: 'content',
filename: 'attachment',
});
return lokiFileServerAPI.uploadPrivateAttachment(formData);
}
// eslint-disable-next-line no-shadow

View File

@ -0,0 +1,90 @@
/* global Whisper, i18n, _ */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.AddServerDialogView = Whisper.View.extend({
templateName: 'add-server-template',
className: 'loki-dialog add-server modal',
initialize(options = {}) {
this.title = i18n('addServerDialogTitle');
this.okText = options.okText || i18n('ok');
this.cancelText = options.cancelText || i18n('cancel');
this.$('input').focus();
this.render();
},
events: {
keyup: 'onKeyup',
'click .ok': 'confirm',
'click .cancel': 'close',
},
render_attributes() {
return {
title: this.title,
ok: this.okText,
cancel: this.cancelText,
};
},
confirm() {
// Remove error if there is one
this.showError(null);
const serverUrl = this.$('#server-url')
.val()
.toLowerCase();
// TODO: Make this not hard coded
const channelId = 1;
const dialog = new Whisper.ConnectingToServerDialogView({
serverUrl,
channelId,
});
const dialogDelayTimer = setTimeout(() => {
this.el.append(dialog.el);
}, 200);
dialog.once('connectionResult', result => {
clearTimeout(dialogDelayTimer);
if (result.cancelled) {
this.showError(null);
return;
}
if (result.errorCode) {
this.showError(result.errorCode);
return;
}
window.Whisper.events.trigger('showToast', {
message: i18n('connectToServerSuccess'),
});
this.close();
});
dialog.trigger('attemptConnection');
},
close() {
this.remove();
},
showError(message) {
if (_.isEmpty(message)) {
this.$('.error').text('');
this.$('.error').hide();
} else {
this.$('.error').text(`Error: ${message}`);
this.$('.error').show();
}
this.$('input').focus();
},
onKeyup(event) {
switch (event.key) {
case 'Enter':
this.confirm();
break;
case 'Escape':
case 'Esc':
this.close();
break;
default:
break;
}
},
});
})();

View File

@ -224,5 +224,9 @@
});
this.el.append(dialog.el);
},
showAddServerDialog() {
const dialog = new Whisper.AddServerDialogView();
this.el.append(dialog.el);
},
});
})();

View File

@ -0,0 +1,83 @@
/* global Whisper, i18n, lokiPublicChatAPI, ConversationController, friends */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
Whisper.ConnectingToServerDialogView = Whisper.View.extend({
templateName: 'connecting-to-server-template',
className: 'loki-dialog connecting-to-server modal',
initialize(options = {}) {
this.title = i18n('connectingLoad');
this.cancelText = options.cancelText || i18n('cancel');
this.serverUrl = options.serverUrl;
this.channelId = options.channelId;
this.once('attemptConnection', () =>
this.attemptConnection(options.serverUrl, options.channelId)
);
this.render();
},
events: {
keyup: 'onKeyup',
'click .cancel': 'close',
},
async attemptConnection(serverUrl, channelId) {
const rawServerUrl = serverUrl
.replace(/^https?:\/\//i, '')
.replace(/[/\\]+$/i, '');
const sslServerUrl = `https://${rawServerUrl}`;
const conversationId = `publicChat:${channelId}@${rawServerUrl}`;
const conversationExists = ConversationController.get(conversationId);
if (conversationExists) {
// We are already a member of this public chat
return this.resolveWith({ errorCode: i18n('publicChatExists') });
}
const serverAPI = await lokiPublicChatAPI.findOrCreateServer(
sslServerUrl
);
if (!serverAPI) {
// Url incorrect or server not compatible
return this.resolveWith({ errorCode: i18n('connectToServerFail') });
}
const conversation = await ConversationController.getOrCreateAndWait(
conversationId,
'group'
);
serverAPI.findOrCreateChannel(channelId, conversationId);
await conversation.setPublicSource(sslServerUrl, channelId);
await conversation.setFriendRequestStatus(
friends.friendRequestStatusEnum.friends
);
return this.resolveWith({ conversation });
},
resolveWith(result) {
this.trigger('connectionResult', result);
this.remove();
},
render_attributes() {
return {
title: this.title,
cancel: this.cancelText,
};
},
close() {
this.trigger('connectionResult', { cancelled: true });
this.remove();
},
onKeyup(event) {
switch (event.key) {
case 'Escape':
case 'Esc':
this.close();
break;
default:
break;
}
},
});
})();

View File

@ -93,6 +93,11 @@
initialize(options) {
this.listenTo(this.model, 'destroy', this.stopListening);
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(
this.model,
'change:friendRequestStatus',
this.onFriendStatusChange
);
this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'prune', this.onPrune);
@ -160,6 +165,7 @@
);
this.render();
this.onFriendStatusChange();
this.model.updateTextInputState();
@ -621,6 +627,14 @@
}
},
onFriendStatusChange() {
if (this.model.isPrivate() && !this.model.isFriend()) {
this.$('#choose-file').hide();
} else {
this.$('#choose-file').show();
}
},
toggleMicrophone() {
// ALWAYS HIDE until we support audio
this.$('.capture-audio').hide();

View File

@ -31,6 +31,22 @@
this.$input.focus();
this.validateNickname();
const sanitiseNameInput = () => {
const oldVal = this.$input.val();
this.$input.val(oldVal.replace(/[^a-zA-Z0-9_]/g, ''));
};
this.$input[0].oninput = () => {
sanitiseNameInput();
};
this.$input[0].onpaste = () => {
// Sanitise data immediately after paste because it's easier
setTimeout(() => {
sanitiseNameInput();
});
};
},
events: {
keyup: 'onKeyup',

View File

@ -75,7 +75,7 @@
);
const sanitiseNameInput = () => {
const oldVal = this.$('#display-name').val();
this.$('#display-name').val(oldVal.replace(/[^a-zA-Z0-9 ]/g, ''));
this.$('#display-name').val(oldVal.replace(/[^a-zA-Z0-9_]/g, ''));
};
this.$('#display-name').get(0).oninput = () => {

View File

@ -6,6 +6,7 @@
Multibase,
TextEncoder,
TextDecoder,
crypto,
dcodeIO
*/
@ -298,6 +299,8 @@
}
const snodeCipher = new LokiSnodeChannel();
const sha512 = data => crypto.subtle.digest('SHA-512', data);
window.libloki.crypto = {
DHEncrypt,
DHDecrypt,
@ -311,5 +314,6 @@
// for testing
_LokiSnodeChannel: LokiSnodeChannel,
_decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,
sha512,
};
})();

View File

@ -20,6 +20,7 @@
/* global feeds: false */
/* global Whisper: false */
/* global lokiFileServerAPI: false */
/* global WebAPI: false */
/* eslint-disable more/no-then */
/* eslint-disable no-unreachable */
@ -30,6 +31,7 @@ function MessageReceiver(username, password, signalingKey, options = {}) {
this.signalingKey = signalingKey;
this.username = username;
this.password = password;
this.server = WebAPI.connect({ username, password });
if (!options.serverTrustRoot) {
throw new Error('Server trust root is required!');
@ -1627,24 +1629,24 @@ MessageReceiver.prototype.extend({
};
},
async downloadAttachment(attachment) {
// window.log.info('Not downloading attachments.');
// return Promise.reject();
// The attachment id is actually just the absolute url of the attachment
let data = await this.server.getAttachment(attachment.url);
if (!attachment.isRaw) {
const { key, digest, size } = attachment;
const encrypted = await this.server.getAttachment(attachment.id);
const { key, digest, size } = attachment;
const data = await textsecure.crypto.decryptAttachment(
encrypted,
window.Signal.Crypto.base64ToArrayBuffer(key),
window.Signal.Crypto.base64ToArrayBuffer(digest)
);
if (!size || size !== data.byteLength) {
throw new Error(
`downloadAttachment: Size ${size} did not match downloaded attachment size ${
data.byteLength
}`
data = await textsecure.crypto.decryptAttachment(
data,
window.Signal.Crypto.base64ToArrayBuffer(key),
window.Signal.Crypto.base64ToArrayBuffer(digest)
);
if (!size || size !== data.byteLength) {
throw new Error(
`downloadAttachment: Size ${size} did not match downloaded attachment size ${
data.byteLength
}`
);
}
}
return {

View File

@ -168,7 +168,7 @@ MessageSender.prototype = {
constructor: MessageSender,
// makeAttachmentPointer :: Attachment -> Promise AttachmentPointerProto
makeAttachmentPointer(attachment) {
async makeAttachmentPointer(attachment, publicServer = null) {
if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined);
}
@ -185,39 +185,49 @@ MessageSender.prototype = {
}
const proto = new textsecure.protobuf.AttachmentPointer();
proto.key = libsignal.crypto.getRandomBytes(64);
const iv = libsignal.crypto.getRandomBytes(16);
return textsecure.crypto
.encryptAttachment(attachment.data, proto.key, iv)
.then(result =>
this.server.putAttachment(result.ciphertext).then(id => {
proto.id = id;
proto.contentType = attachment.contentType;
proto.digest = result.digest;
if (attachment.size) {
proto.size = attachment.size;
}
if (attachment.fileName) {
proto.fileName = attachment.fileName;
}
if (attachment.flags) {
proto.flags = attachment.flags;
}
if (attachment.width) {
proto.width = attachment.width;
}
if (attachment.height) {
proto.height = attachment.height;
}
if (attachment.caption) {
proto.caption = attachment.caption;
}
return proto;
})
let attachmentData;
let server;
if (publicServer) {
attachmentData = attachment.data;
server = publicServer;
} else {
proto.key = libsignal.crypto.getRandomBytes(64);
const iv = libsignal.crypto.getRandomBytes(16);
const result = await textsecure.crypto.encryptAttachment(
attachment.data,
proto.key,
iv
);
proto.digest = result.digest;
attachmentData = result.ciphertext;
({ server } = this);
}
const { url, id } = await server.putAttachment(attachmentData);
proto.id = id;
proto.url = url;
proto.contentType = attachment.contentType;
if (attachment.size) {
proto.size = attachment.size;
}
if (attachment.fileName) {
proto.fileName = attachment.fileName;
}
if (attachment.flags) {
proto.flags = attachment.flags;
}
if (attachment.width) {
proto.width = attachment.width;
}
if (attachment.height) {
proto.height = attachment.height;
}
if (attachment.caption) {
proto.caption = attachment.caption;
}
return proto;
},
queueJobForNumber(number, runJob) {
@ -240,9 +250,11 @@ MessageSender.prototype = {
});
},
uploadAttachments(message) {
uploadAttachments(message, publicServer) {
return Promise.all(
message.attachments.map(this.makeAttachmentPointer.bind(this))
message.attachments.map(attachment =>
this.makeAttachmentPointer(attachment, publicServer)
)
)
.then(attachmentPointers => {
// eslint-disable-next-line no-param-reassign
@ -257,12 +269,12 @@ MessageSender.prototype = {
});
},
async uploadLinkPreviews(message) {
async uploadLinkPreviews(message, publicServer) {
try {
const preview = await Promise.all(
(message.preview || []).map(async item => ({
...item,
image: await this.makeAttachmentPointer(item.image),
image: await this.makeAttachmentPointer(item.image, publicServer),
}))
);
// eslint-disable-next-line no-param-reassign
@ -276,7 +288,7 @@ MessageSender.prototype = {
}
},
uploadThumbnails(message) {
uploadThumbnails(message, publicServer) {
const makePointer = this.makeAttachmentPointer.bind(this);
const { quote } = message;
@ -291,7 +303,7 @@ MessageSender.prototype = {
return null;
}
return makePointer(thumbnail).then(pointer => {
return makePointer(thumbnail, publicServer).then(pointer => {
// eslint-disable-next-line no-param-reassign
attachment.attachmentPointer = pointer;
});
@ -308,19 +320,13 @@ MessageSender.prototype = {
sendMessage(attrs, options) {
const message = new Message(attrs);
const silent = false;
// Remove this when we add support for attachments
message.attachments = [];
message.attachmentPointers = [];
message.preview = [];
if (message.quote) {
message.quote.attachments = [];
}
const publicServer =
options.publicSendData && options.publicSendData.serverAPI;
return Promise.all([
this.uploadAttachments(message),
this.uploadThumbnails(message),
this.uploadLinkPreviews(message),
this.uploadAttachments(message, publicServer),
this.uploadThumbnails(message, publicServer),
this.uploadLinkPreviews(message, publicServer),
]).then(
() =>
new Promise((resolve, reject) => {

View File

@ -3,7 +3,7 @@
"productName": "Loki Messenger",
"description": "Private messaging from your desktop",
"repository": "https://github.com/loki-project/loki-messenger.git",
"version": "1.0.0-beta5",
"version": "1.0.0-beta6",
"license": "GPL-3.0",
"author": {
"name": "Loki Project",
@ -20,7 +20,7 @@
"grunt": "grunt",
"icon-gen": "electron-icon-maker --input=images/icon_1024.png --output=./build",
"generate": "yarn icon-gen && yarn grunt",
"build": "build --config.extraMetadata.environment=$SIGNAL_ENV",
"build": "electron-builder --config.extraMetadata.environment=$SIGNAL_ENV",
"build-release": "export SIGNAL_ENV=production && npm run build -- --config.directories.output=release",
"sign-release": "node ts/updater/generateSignature.js",
"build-module-protobuf": "pbjs --target static-module --wrap commonjs --out ts/protobuf/compiled.js protos/*.proto && pbts --out ts/protobuf/compiled.d.ts ts/protobuf/compiled.js",
@ -155,8 +155,9 @@
"chai": "4.1.2",
"dashdash": "1.14.1",
"electron": "4.1.2",
"electron-builder": "20.39.0",
"electron-builder": "21.2.0",
"electron-icon-maker": "0.0.3",
"electron-notarize": "^0.1.1",
"eslint": "4.14.0",
"eslint-config-airbnb-base": "12.1.0",
"eslint-config-prettier": "2.9.0",
@ -193,21 +194,20 @@
"node": "10.13.0"
},
"build": {
"appId": "org.loki.messenger-desktop",
"appId": "com.loki-project.messenger-desktop",
"afterSign": "build/notarize.js",
"mac": {
"artifactName": "${name}-mac-${version}.${ext}",
"category": "public.app-category.social-networking",
"icon": "build/icons/mac/icon.icns",
"publish": [
{
"provider": "generic",
"url": "https://updates.signal.org/desktop"
}
],
"target": [
"zip"
"7z"
],
"bundleVersion": "1"
"bundleVersion": "1",
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.plist"
},
"win": {
"asarUnpack": "node_modules/spellchecker/vendor/hunspell_dictionaries",

View File

@ -41,6 +41,7 @@ window.isBehindProxy = () => Boolean(config.proxyUrl);
window.JobQueue = JobQueue;
window.getStoragePubKey = key =>
window.isDev() ? key.substring(0, key.length - 2) : key;
window.getDefaultFileServer = () => config.defaultFileServer;
window.isBeforeVersion = (toCheck, baseVersion) => {
try {
@ -328,10 +329,7 @@ window.LokiMessageAPI = require('./js/modules/loki_message_api');
window.LokiPublicChatAPI = require('./js/modules/loki_public_chat_api');
const LokiFileServerAPIWrapper = require('./js/modules/loki_file_server_api');
// bind first argument as we have it here already
window.LokiFileServerAPI = LokiFileServerAPIWrapper(config.defaultFileServer);
window.LokiFileServerAPI = require('./js/modules/loki_file_server_api');
window.LokiRssAPI = require('./js/modules/loki_rss_api');

View File

@ -31,8 +31,8 @@ const PRODUCTION_PRODUCT_NAME = 'Loki Messenger';
const BETA_PRODUCT_NAME = 'Loki Messenger Beta';
const APP_ID_PATH = 'build.appId';
const PRODUCTION_APP_ID = 'org.loki.messenger-desktop';
const BETA_APP_ID = 'org.loki.messenger-desktop-beta';
const PRODUCTION_APP_ID = 'com.loki-project.messenger-desktop';
const BETA_APP_ID = 'com.loki-project.messenger-desktop-beta';
const STARTUP_WM_CLASS_PATH = 'build.linux.desktop.StartupWMClass';
const PRODUCTION_STARTUP_WM_CLASS = 'Loki Messenger';

View File

@ -335,6 +335,7 @@ message AttachmentPointer {
optional uint32 width = 9;
optional uint32 height = 10;
optional string caption = 11;
optional string url = 101;
}
message GroupContext {

View File

@ -14,6 +14,9 @@
}
background-color: $color-light-10;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
.name-part {
font-weight: 300;
@ -22,6 +25,7 @@
.pubkey-part {
margin-left: 6px;
color: darkslategrey;
}
}
@ -60,6 +64,10 @@
background-color: $color-dark-70;
color: white;
.pubkey-part {
color: rgb(230, 230, 230);
}
}
.member-selected {

View File

@ -567,7 +567,8 @@
<script type='text/javascript' src='../js/views/nickname_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/password_dialog_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/seed_dialog_view.js' data-cover></script>
<script type='text/javascript' src='js/views/qr_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/qr_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/add_server_dialog_view.js'></script>
<script type='text/javascript' src='../js/views/identicon_svg_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/last_seen_indicator_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/scroll_down_button_view.js' data-cover></script>

View File

@ -342,6 +342,13 @@ export class MainHeader extends React.Component<Props, any> {
trigger('showQRDialog');
},
},
{
id: 'showAddServer',
name: i18n('showAddServer'),
onClick: () => {
trigger('showAddServerDialog');
},
},
];
const passItem = (type: string) => ({

903
yarn.lock

File diff suppressed because it is too large Load Diff