@jsxc/jsxc
Version:
Real-time XMPP chat application with video calls, file transfer and encrypted communication
396 lines (316 loc) • 14.7 kB
text/typescript
import Dialog from '../Dialog';
import Client from '../../Client';
import Page from '../DialogPage';
import Section from '../DialogSection';
import Navigation from '../DialogNavigation';
import List from '../DialogList';
import ListItem from '../DialogListItem';
import AvatarSet from '../AvatarSet';
import Log from '../../util/Log';
import Translation from '@util/Translation';
import Account from '@src/Account';
const ENOUGH_BITS_OF_ENTROPY = 50;
export default function () {
let dialog = new Dialog('', false, 'settings');
let dom = dialog.open();
let navigation = new Navigation(dom);
navigation.goTo(new StartPage(navigation));
}
class StartPage extends Page {
constructor(navigation: Navigation) {
super(navigation, Translation.t('Settings'));
}
//@REVIEW could also return Page or getDOM interface?
protected generateContentElement(): JQuery | JQuery[] {
return [new ClientSection(this.navigation).getDOM(), new AccountOverviewSection(this.navigation).getDOM()];
}
}
class ClientSection extends Section {
protected generateContentElement(): JQuery {
let contentElement = new List();
contentElement.append(
new ListItem(
Translation.t('Language'),
Translation.t('After_changing_this_option_you_have_to_reload_the_page'),
undefined,
undefined,
this.getLanguageSelectionElement()
)
);
contentElement.append(
new ListItem(
Translation.t('trusted_domains'),
Translation.t('one_domain_per_line'),
undefined,
undefined,
this.getTrustedDomainsElement()
)
);
return contentElement.getDOM();
}
private getLanguageSelectionElement() {
let currentLang = Client.getOption('lang');
let element = $('<select>');
element.append('<option value=""></option>');
__LANGS__.forEach(lang => {
let optionElement = $('<option>');
optionElement.text(lang);
optionElement.appendTo(element);
if (lang === currentLang) {
optionElement.attr('selected', 'selected');
}
});
element.on('change', ev => {
let value = $(ev.target).val();
Client.setOption('lang', value ? value : undefined);
});
if (element.find('[selected]').length === 0) {
element.find('option:eq(0)').attr('selected', 'selected');
}
return element;
}
private getTrustedDomainsElement(): JQuery {
let element = $('<textarea style="margin-left:10px;">');
element.on('change', () => {
let value = element
.val()
.toString()
.split('\n')
.map(line => line.trim());
Client.setOption('trustedDomains', value ? value : undefined);
});
let trustedDomains = Client.getOption('trustedDomains', []);
element.val(trustedDomains.join('\n'));
return element;
}
}
class AccountOverviewSection extends Section {
constructor(navigation: Navigation) {
super(navigation, Translation.t('Accounts'));
}
protected generateContentElement(): JQuery {
let accounts = Client.getAccountManager().getAccounts();
let contentElement = new List();
for (let account of accounts) {
let jid = account.getJID();
let name = jid.bare;
let avatarElement = $('<div>');
avatarElement.addClass('jsxc-avatar');
AvatarSet.setPlaceholder(avatarElement, name, jid);
let actionHandler = () => this.navigation.goTo(new AccountPage(this.navigation, account));
let accountElement = new ListItem(name, undefined, actionHandler, avatarElement);
contentElement.append(accountElement);
}
return contentElement.getDOM();
}
}
class AccountPage extends Page {
constructor(navigation: Navigation, private account: Account) {
super(navigation, account.getJID().bare);
}
protected generateContentElement(): JQuery {
let contentElement = $('<div>');
contentElement.append(new ConnectionSection(this.navigation, this.account).getDOM());
contentElement.append(new MainAppSection(this.navigation).getDOM());
contentElement.append(new PluginSection(this.navigation, this.account).getDOM());
return contentElement;
}
}
//@REVIEW priorities? Are they still needed/used?
class ConnectionSection extends Section {
constructor(navigation: Navigation, private account: Account) {
super(navigation, Translation.t('Connection'));
}
protected generateContentElement(): JQuery {
let jid = this.account.getJID();
let contentElement = new List();
let changePasswordActionHandler = () => this.navigation.goTo(new PasswordPage(this.navigation, this.account));
contentElement.append(new ListItem('Jabber ID', jid.bare));
contentElement.append(new ListItem(Translation.t('Resource'), jid.resource));
contentElement.append(new ListItem('BOSH url', this.account.getConnectionUrl()));
contentElement.append(new ListItem(Translation.t('Change_password'), undefined, changePasswordActionHandler));
return contentElement.getDOM();
}
}
class PasswordPage extends Page {
constructor(navigation: Navigation, private account: Account) {
super(navigation, Translation.t('Password'));
}
protected generateContentElement(): JQuery {
//@REVIEW maybe template
let contentElement = $('<form>');
contentElement.addClass('form-horizontal');
contentElement.css('marginTop', '30px'); //@REVIEW
let explanationElement = $(`<p class="jsxc-explanation">${Translation.t('password_explanation')}</p>`);
let passwordAElement = $(`<div class="form-group">
<label class="col-sm-4 control-label" for="jsxc-password-A">${Translation.t('Password')}</label>
<div class="col-sm-8">
<input type="password" name="password-A" id="jsxc-password-A" class="form-control" required="required">
<p class="jsxc-inputinfo"></p>
</div>
</div>`);
let passwordBElement = $(`<div class="form-group">
<label class="col-sm-4 control-label" for="jsxc-password-B">${Translation.t('Control')}</label>
<div class="col-sm-8">
<input type="password" name="password-B" id="jsxc-password-B" class="form-control" required="required">
<p class="jsxc-inputinfo jsxc-hidden"></p>
</div>
</div>`);
let submitElement = $(`<div class="form-group">
<div class="col-sm-offset-4 col-sm-8">
<button disabled="disabled" class="jsxc-button jsxc-button--primary">${Translation.t(
'Change_password'
)}</button>
</div>
</div>`);
let errorElement = $(`<div class="jsxc-alert jsxc-alert--warning jsxc-hidden"></div>`);
contentElement.append(explanationElement);
contentElement.append(passwordAElement);
contentElement.append(passwordBElement);
contentElement.append(submitElement);
contentElement.append(errorElement);
passwordAElement.find('input').on('input', function () {
let value = <string>$(this).val();
let numberOfPossibleCharacters = 0;
if (/[a-z]/.test(value)) {
numberOfPossibleCharacters += 26;
}
if (/[A-Z]/.test(value)) {
numberOfPossibleCharacters += 26;
}
if (/[0-9]/.test(value)) {
numberOfPossibleCharacters += 10;
}
if (/[^a-zA-Z0-9]/.test(value)) {
numberOfPossibleCharacters += 15; // most common
}
let entropy = Math.pow(numberOfPossibleCharacters, value.length);
let bitsOfEntropy = Math.log2(entropy);
let strength = Math.min(100, Math.round((bitsOfEntropy / ENOUGH_BITS_OF_ENTROPY) * 100));
passwordAElement.find('.jsxc-inputinfo').text(`${Translation.t('Strength')}: ${strength}%`);
});
contentElement.find('input').on('input', function () {
let passwordA = passwordAElement.find('input').val();
let passwordB = passwordBElement.find('input').val();
submitElement.find('button').prop('disabled', passwordA !== passwordB);
});
contentElement.submit(ev => {
ev.preventDefault();
let passwordA = passwordAElement.find('input').val() as string;
let passwordB = passwordBElement.find('input').val() as string;
if (passwordA !== passwordB) {
return;
}
this.account
.getConnection()
.changePassword(passwordA)
.then(() => {
Log.debug('Password was changed');
})
.catch(errStanza => {
//@TODO check for error 401 and form (https://xmpp.org/extensions/xep-0077.html#usecases-changepw)
errorElement.removeClass('jsxc-hidden');
errorElement.text('Server error. Password was not changed.');
});
});
return contentElement;
}
}
class MainAppSection extends Section {
constructor(navigation: Navigation) {
super(navigation, Translation.t('General'), true);
}
protected generateContentElement(): JQuery {
let contentElement = new List();
contentElement.append(this.getListItemForData('RFC6120', 'XMPP Core', '', ''));
contentElement.append(this.getListItemForData('RFC6121', 'XMPP IM', '', ''));
contentElement.append(this.getListItemForData('', 'Off-the-Record Messaging', '', ''));
contentElement.append(this.getListItemForData('', 'Data Forms', '0030', ''));
contentElement.append(this.getListItemForData('', 'Service Discovery', '0163', '1.2.1'));
contentElement.append(this.getListItemForData('', 'vcard-temp', '0054', ''));
contentElement.append(this.getListItemForData('', 'Software Version', '0115', ''));
contentElement.append(this.getListItemForData('', 'Entity Capabilities', '0163', '1.2.1'));
contentElement.append(this.getListItemForData('', 'URI Scheme Query', '0147', ''));
contentElement.append(this.getListItemForData('', 'Jingle', '0166', ''));
contentElement.append(this.getListItemForData('', 'Jingle RTP Sessions', '0167', ''));
contentElement.append(this.getListItemForData('', 'Jingle File Transfer', '0234', ''));
contentElement.append(this.getListItemForData('', 'Delayed Delivery', '0203', ''));
contentElement.append(this.getListItemForData('', 'XMPP Over BOSH', '0206', ''));
contentElement.append(this.getListItemForData('', 'Bidirectional-streams Over Synchronous HTTP', '0124', ''));
contentElement.append(this.getListItemForData('', 'Stanza Forwarding', '0297', ''));
contentElement.append(this.getListItemForData('', 'Multi-User Chat', '0045', ''));
contentElement.append(this.getListItemForData('', 'Jabber Search', '0055', '1.3'));
contentElement.append(this.getListItemForData('', 'Publish-Subscribe', '0060', '1.2.1'));
contentElement.append(this.getListItemForData('', 'Personal Eventing Protocol', '0163', '1.2.1'));
return contentElement.getDOM();
}
private getListItemForData(description, xepname, xepid, xepversion) {
let checkboxElement = $('<input>');
checkboxElement.attr('type', 'checkbox');
checkboxElement.prop('checked', true);
checkboxElement.prop('disabled', true);
let listItem = new ListItem(xepname, description, undefined, undefined, checkboxElement);
let listItemElement = listItem.getDOM();
if (xepid && xepid.length) {
let xepElement = $('<a target="_blank" rel="noreferrer noopener">');
xepElement.addClass('jsxc-badge');
xepElement.text('XEP-' + xepid + (xepversion && xepversion.length > 0 ? '@' + xepversion : ''));
xepElement.attr('title', xepname);
xepElement.attr('href', 'https://xmpp.org/extensions/xep-' + xepid + '.html');
xepElement.appendTo(listItemElement.find('.jsxc-list__text__primary'));
}
return listItem;
}
}
class PluginSection extends Section {
constructor(navigation: Navigation, private account: Account) {
super(navigation, Translation.t('Plugins'), true);
}
protected generateContentElement(): JQuery {
let contentElement = new List();
let disabledPlugins = this.account.getOption('disabledPlugins') || [];
let pluginRepository = this.account.getPluginRepository();
for (let plugin of pluginRepository.getAllRegisteredPlugins()) {
let id = plugin.getId();
let name = plugin.getName();
let metaData = typeof plugin.getMetaData === 'function' ? plugin.getMetaData() : {};
let description =
typeof (<any>plugin).getDescription === 'function' ? (<any>plugin).getDescription() : metaData.description;
let checkboxElement = $('<input>');
checkboxElement.attr('type', 'checkbox');
checkboxElement.attr('id', id);
checkboxElement.attr('name', name);
checkboxElement.prop('checked', disabledPlugins.indexOf(id) < 0);
//check if libsignal is available
if (id === 'omemo' && typeof (<any>window).libsignal === 'undefined') {
checkboxElement.prop('checked', false);
checkboxElement.prop('disabled', true);
}
checkboxElement.on('change', ev => {
let isEnabled = $(ev.target).prop('checked');
let id = $(ev.target).attr('id');
let disabledPlugins = this.account.getOption('disabledPlugins') || [];
if (isEnabled) {
disabledPlugins = disabledPlugins.filter(v => v !== id);
} else {
disabledPlugins.push(id);
}
this.account.setOption('disabledPlugins', disabledPlugins);
});
let listItem = new ListItem(name, description, undefined, undefined, checkboxElement);
let listItemElement = listItem.getDOM();
if (Array.isArray(metaData.xeps)) {
metaData.xeps.forEach(xep => {
let xepElement = $('<a target="_blank" rel="noreferrer noopener">');
xepElement.addClass('jsxc-badge');
xepElement.text(xep.id + '@' + xep.version);
xepElement.attr('title', xep.name);
xepElement.attr('href', `https://xmpp.org/extensions/${xep.id.toLowerCase()}.html`);
xepElement.appendTo(listItemElement.find('.jsxc-list__text__primary'));
});
}
contentElement.append(listItem);
}
return contentElement.getDOM();
}
}