mirror of
https://github.com/oxen-io/session-desktop.git
synced 2023-12-14 02:12:57 +01:00
Merge branch 'clearnet' into multi-device
This commit is contained in:
commit
d97eced37b
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
9
build/entitlements.mac.plist
Normal file
9
build/entitlements.mac.plist
Normal 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>
|
|
@ -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
28
build/notarize.js
Normal 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,
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
})();
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
90
js/views/add_server_dialog_view.js
Normal file
90
js/views/add_server_dialog_view.js
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -224,5 +224,9 @@
|
|||
});
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
showAddServerDialog() {
|
||||
const dialog = new Whisper.AddServerDialogView();
|
||||
this.el.append(dialog.el);
|
||||
},
|
||||
});
|
||||
})();
|
||||
|
|
83
js/views/connecting_to_server_dialog_view.js
Normal file
83
js/views/connecting_to_server_dialog_view.js
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
})();
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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 = () => {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
})();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
24
package.json
24
package.json
|
@ -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",
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -335,6 +335,7 @@ message AttachmentPointer {
|
|||
optional uint32 width = 9;
|
||||
optional uint32 height = 10;
|
||||
optional string caption = 11;
|
||||
optional string url = 101;
|
||||
}
|
||||
|
||||
message GroupContext {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
Loading…
Reference in a new issue