Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into webrtc_settings

and i18nize webrtc stufffs

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

# Conflicts:
#	src/components/structures/UserSettings.js
This commit is contained in:
Michael Telatynski 2017-06-01 22:56:31 +01:00
commit 04b86e5d1d
135 changed files with 8102 additions and 1115 deletions

View file

@ -29,7 +29,10 @@ const GeminiScrollbar = require('react-gemini-scrollbar');
const Email = require('../../email');
const AddThreepid = require('../../AddThreepid');
const SdkConfig = require('../../SdkConfig');
import Analytics from '../../Analytics';
import AccessibleButton from '../views/elements/AccessibleButton';
import { _t } from '../../languageHandler';
import * as languageHandler from '../../languageHandler';
import * as FormattingUtils from '../../utils/FormattingUtils';
// if this looks like a release, use the 'version' from package.json; else use
@ -54,6 +57,8 @@ const gHVersionLabel = function(repo, token='') {
// Enumerate some simple 'flip a bit' UI settings (if any).
// 'id' gives the key name in the im.vector.web.settings account data event
// 'label' is how we describe it in the UI.
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const SETTINGS_LABELS = [
{
id: 'autoplayGifsAndVideos',
@ -67,7 +72,6 @@ const SETTINGS_LABELS = [
id: 'dontSendTypingNotifications',
label: "Don't send typing notifications",
},
/*
{
id: 'alwaysShowTimestamps',
label: 'Always show message timestamps',
@ -80,6 +84,7 @@ const SETTINGS_LABELS = [
id: 'useCompactLayout',
label: 'Use compact timeline layout',
},
/*
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
@ -87,10 +92,25 @@ const SETTINGS_LABELS = [
*/
];
const ANALYTICS_SETTINGS_LABELS = [
{
id: 'analyticsOptOut',
label: 'Opt out of analytics',
fn: function(checked) {
Analytics[checked ? 'disable' : 'enable']();
},
},
];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json,
// since they will be translated when rendered.
const CRYPTO_SETTINGS_LABELS = [
{
id: 'blacklistUnverifiedDevices',
label: 'Never send encrypted messages to unverified devices from this device',
fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
},
},
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
@ -200,6 +220,17 @@ module.exports = React.createClass({
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.on('settings', this._electronSettings);
ipcRenderer.send('settings_get');
}
this.setState({
language: languageHandler.getCurrentLanguage(),
});
},
componentDidMount: function() {
@ -219,6 +250,15 @@ module.exports = React.createClass({
if (cli) {
cli.removeListener("RoomMember.membership", this._onInviteStateChange);
}
if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron');
ipcRenderer.removeListener('settings', this._electronSettings);
}
},
_electronSettings: function(ev, settings) {
this.setState({ electron_settings: settings });
},
_refreshMediaDevices: function() {
@ -249,8 +289,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to load user settings: " + error);
Modal.createDialog(ErrorDialog, {
title: "Can't load user settings",
description: ((error && error.message) ? error.message : "Server may be unavailable or overloaded"),
title: _t("Can't load user settings"),
description: ((error && error.message) ? error.message : _t("Server may be unavailable or overloaded")),
});
});
},
@ -265,8 +305,8 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't set avatars. Please register.",
title: _t("Please Register"),
description: _t("Guests can't set avatars. Please register."),
});
return;
}
@ -291,8 +331,8 @@ module.exports = React.createClass({
console.error("Failed to set avatar: " + err);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Failed to set avatar",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Failed to set avatar."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
},
@ -300,19 +340,19 @@ module.exports = React.createClass({
onLogoutClicked: function(ev) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Sign out?",
title: _t("Sign out"),
description:
<div>
For security, logging out will delete any end-to-end encryption keys from this browser.
If you want to be able to decrypt your conversation history from future Riot sessions,
please export your room keys for safe-keeping.
{ _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.") }.
</div>,
button: "Sign out",
button: _t("Sign out"),
extraButtons: [
<button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
{ _t("Export E2E room keys") }
</button>,
],
onFinished: (confirmed) => {
@ -329,14 +369,14 @@ module.exports = React.createClass({
onPasswordChangeError: function(err) {
let errMsg = err.error || "";
if (err.httpStatus === 403) {
errMsg = "Failed to change password. Is your password correct?";
errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) {
errMsg += ` (HTTP status ${err.httpStatus})`;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to change password: " + errMsg);
Modal.createDialog(ErrorDialog, {
title: "Error",
title: _t("Error"),
description: errMsg,
});
},
@ -344,10 +384,8 @@ module.exports = React.createClass({
onPasswordChanged: function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
title: "Success",
description: `Your password was successfully changed. You will not
receive push notifications on other devices until you
log back in to them.`,
title: _t("Success"),
description: _t("Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them") + ".",
});
},
@ -373,8 +411,8 @@ module.exports = React.createClass({
const emailAddress = this.refs.add_email_input.value;
if (!Email.looksValid(emailAddress)) {
Modal.createDialog(ErrorDialog, {
title: "Invalid Email Address",
description: "This doesn't appear to be a valid email address",
title: _t("Invalid Email Address"),
description: _t("This doesn't appear to be a valid email address"),
});
return;
}
@ -383,17 +421,17 @@ module.exports = React.createClass({
// same here.
this._addThreepid.addEmailAddress(emailAddress, true).done(() => {
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
description: "Please check your email and click on the link it contains. Once this is done, click continue.",
button: 'Continue',
title: _t("Verification Pending"),
description: _t("Please check your email and click on the link it contains. Once this is done, click continue."),
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
}, (err) => {
this.setState({email_add_pending: false});
console.error("Unable to add email address " + emailAddress + " " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to add email address",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to add email address"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
ReactDOM.findDOMNode(this.refs.add_email_input).blur();
@ -403,9 +441,9 @@ module.exports = React.createClass({
onRemoveThreepidClicked: function(threepid) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createDialog(QuestionDialog, {
title: "Remove Contact Information?",
description: "Remove " + threepid.address + "?",
button: 'Remove',
title: _t("Remove Contact Information?"),
description: _t("Remove %(threePid)s?", { threePid : threepid.address }),
button: _t('Remove'),
onFinished: (submit) => {
if (submit) {
this.setState({
@ -417,8 +455,8 @@ module.exports = React.createClass({
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to remove contact information: " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to remove contact information",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to remove contact information"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}).done();
}
@ -444,22 +482,22 @@ module.exports = React.createClass({
this.setState({email_add_pending: false});
}, (err) => {
this.setState({email_add_pending: false});
if (err.errcode === 'M_THREEPID_AUTH_FAILED') {
if (err.errcode == 'M_THREEPID_AUTH_FAILED') {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
let message = "Unable to verify email address. ";
message += "Please check your email and click on the link it contains. Once this is done, click continue.";
let message = _t("Unable to verify email address.") + " " +
_t("Please check your email and click on the link it contains. Once this is done, click continue.");
Modal.createDialog(QuestionDialog, {
title: "Verification Pending",
title: _t("Verification Pending"),
description: message,
button: 'Continue',
button: _t('Continue'),
onFinished: this.onEmailDialogFinished,
});
} else {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Unable to verify email address: " + err);
Modal.createDialog(ErrorDialog, {
title: "Unable to verify email address",
description: ((err && err.message) ? err.message : "Operation failed"),
title: _t("Unable to verify email address."),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
}
});
@ -549,20 +587,43 @@ module.exports = React.createClass({
<div>
<h3>Referral</h3>
<div className="mx_UserSettings_section">
Refer a friend to Riot: <a href={href}>{href}</a>
{_t("Refer a friend to Riot:")} <a href={href}>{href}</a>
</div>
</div>
);
},
onLanguageChange: function(newLang) {
if(this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang);
this.setState({
language: newLang,
});
PlatformPeg.get().reload();
}
},
_renderLanguageSetting: function () {
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
return <div>
<label htmlFor="languageSelector">{_t('Interface Language')}</label>
<LanguageDropdown ref="language" onOptionChange={this.onLanguageChange}
className="mx_UserSettings_language"
value={this.state.language}
/>
</div>;
},
_renderUserInterfaceSettings: function() {
return (
<div>
<h3>User Interface</h3>
<h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) }
{ THEMES.map( this._renderThemeSelector ) }
{ this._renderLanguageSetting() }
</div>
</div>
);
@ -576,7 +637,7 @@ module.exports = React.createClass({
onChange={ (e) => UserSettingsStore.setUrlPreviewsDisabled(e.target.checked) }
/>
<label htmlFor="urlPreviewsDisabled">
Disable inline URL previews by default
{ _t("Disable inline URL previews by default") }
</label>
</div>;
},
@ -586,10 +647,15 @@ module.exports = React.createClass({
<input id={ setting.id }
type="checkbox"
defaultChecked={ this._syncedSettings[setting.id] }
onChange={ (e) => UserSettingsStore.setSyncedSetting(setting.id, e.target.checked) }
onChange={
(e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
}
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
{ _t(setting.label) }
</label>
</div>;
},
@ -623,7 +689,7 @@ module.exports = React.createClass({
const deviceId = client.deviceId;
let identityKey = client.getDeviceEd25519Key();
if (!identityKey) {
identityKey = "<not supported>";
identityKey = _t("<not supported>");
} else {
identityKey = FormattingUtils.formatCryptoKey(identityKey);
}
@ -635,18 +701,18 @@ module.exports = React.createClass({
<div className="mx_UserSettings_importExportButtons">
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onExportE2eKeysClicked}>
Export E2E room keys
{ _t("Export E2E room keys") }
</AccessibleButton>
<AccessibleButton className="mx_UserSettings_button"
onClick={this._onImportE2eKeysClicked}>
Import E2E room keys
{ _t("Import E2E room keys") }
</AccessibleButton>
</div>
);
}
return (
<div>
<h3>Cryptography</h3>
<h3>{ _t("Cryptography") }</h3>
<div className="mx_UserSettings_section mx_UserSettings_cryptoSection">
<ul>
<li><label>Device ID:</label> <span><code>{deviceId}</code></span></li>
@ -662,7 +728,6 @@ module.exports = React.createClass({
},
_renderLocalSetting: function(setting) {
const client = MatrixClientPeg.get();
return <div className="mx_UserSettings_toggle" key={ setting.id }>
<input id={ setting.id }
type="checkbox"
@ -670,14 +735,12 @@ module.exports = React.createClass({
onChange={
(e) => {
UserSettingsStore.setLocalSetting(setting.id, e.target.checked);
if (setting.id === 'blacklistUnverifiedDevices') { // XXX: this is a bit ugly
client.setGlobalBlacklistUnverifiedDevices(e.target.checked);
}
if (setting.fn) setting.fn(e.target.checked);
}
}
}
/>
<label htmlFor={ setting.id }>
{ setting.label }
{ _t(setting.label) }
</label>
</div>;
},
@ -698,20 +761,31 @@ module.exports = React.createClass({
}
return (
<div>
<h3>Bug Report</h3>
<h3>{ _t("Bug Report") }</h3>
<div className="mx_UserSettings_section">
<p>Found a bug?</p>
<p>{ _t("Found a bug?") }</p>
<button className="mx_UserSettings_button danger"
onClick={this._onBugReportClicked}>Report it
onClick={this._onBugReportClicked}>{_t('Report it')}
</button>
</div>
</div>
);
},
_renderAnalyticsControl: function() {
return <div>
<h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section">
{_t('Riot collects anonymous analytics to allow us to improve the application.')}
{ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting )}
</div>
</div>;
},
_renderLabs: function() {
// default to enabled if undefined
if (this.props.enableLabs === false) return null;
UserSettingsStore.doTranslations();
const features = UserSettingsStore.LABS_FEATURES.map((feature) => (
<div key={feature.id} className="mx_UserSettings_toggle">
@ -725,8 +799,8 @@ module.exports = React.createClass({
e.target.checked = false;
const NeedToRegisterDialog = sdk.getComponent("dialogs.NeedToRegisterDialog");
Modal.createDialog(NeedToRegisterDialog, {
title: "Please Register",
description: "Guests can't use labs features. Please register.",
title: _t("Please Register"),
description: _t("Guests can't use labs features. Please register."),
});
return;
}
@ -739,9 +813,9 @@ module.exports = React.createClass({
));
return (
<div>
<h3>Labs</h3>
<h3>{ _t("Labs") }</h3>
<div className="mx_UserSettings_section">
<p>These are experimental features that may break in unexpected ways. Use with caution.</p>
<p>{ _t("These are experimental features that may break in unexpected ways") }. { _t("Use with caution") }.</p>
{features}
</div>
</div>
@ -753,10 +827,10 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) return null;
return <div>
<h3>Deactivate Account</h3>
<h3>{ _t("Deactivate Account") }</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onDeactivateAccountClicked}>Deactivate my account
onClick={this._onDeactivateAccountClicked}> { _t("Deactivate my account") }
</AccessibleButton>
</div>
</div>;
@ -764,11 +838,11 @@ module.exports = React.createClass({
_renderClearCache: function() {
return <div>
<h3>Clear Cache</h3>
<h3>{ _t("Clear Cache") }</h3>
<div className="mx_UserSettings_section">
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onClearCacheClicked}>
Clear Cache and Reload
{ _t("Clear Cache and Reload") }
</AccessibleButton>
</div>
</div>;
@ -791,19 +865,42 @@ module.exports = React.createClass({
reject = (
<AccessibleButton className="mx_UserSettings_button danger"
onClick={this._onRejectAllInvitesClicked.bind(this, invitedRooms)}>
Reject all {invitedRooms.length} invites
{_t("Reject all %(invitedRooms)s invites", {invitedRooms: invitedRooms.length})}
</AccessibleButton>
);
}
return <div>
<h3>Bulk Options</h3>
<h3>{ _t("Bulk Options") }</h3>
<div className="mx_UserSettings_section">
{reject}
</div>
</div>;
},
_renderElectronSettings: function() {
const settings = this.state.electron_settings;
if (!settings) return;
const {ipcRenderer} = require('electron');
return <div>
<h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_toggle">
<input type="checkbox"
name="auto-launch"
defaultChecked={settings['auto-launch']}
onChange={(e) => {
ipcRenderer.send('settings_set', 'auto-launch', e.target.checked);
}}
/>
<label htmlFor="auto-launch">{_t('Start automatically after system login')}</label>
</div>
</div>
</div>;
},
_mapWebRtcDevicesToSpans: function(devices) {
return Object.keys(devices).map(
(deviceId) => <span key={deviceId}>{devices[deviceId]}</span>
@ -904,7 +1001,8 @@ module.exports = React.createClass({
},
nameForMedium: function(medium) {
if (medium === 'msisdn') return 'Phone';
if (medium === 'msisdn') return _t('Phone');
if (medium === 'email') return _t('Email');
return medium[0].toUpperCase() + medium.slice(1);
},
@ -953,7 +1051,7 @@ module.exports = React.createClass({
/>
</div>
<div className="mx_UserSettings_threepidButton mx_filterFlipColor">
<img src="img/cancel-small.svg" width="14" height="14" alt="Remove" onClick={this.onRemoveThreepidClicked.bind(this, val)} />
<img src="img/cancel-small.svg" width="14" height="14" alt={ _t("Remove") } onClick={this.onRemoveThreepidClicked.bind(this, val)} />
</div>
</div>
);
@ -965,14 +1063,14 @@ module.exports = React.createClass({
addEmailSection = (
<div className="mx_UserSettings_profileTableRow" key="_newEmail">
<div className="mx_UserSettings_profileLabelCell">
<label>Email</label>
<label>{_t('Email')}</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<EditableText
ref="add_email_input"
className="mx_UserSettings_editable"
placeholderClassName="mx_UserSettings_threepidPlaceholder"
placeholder={ "Add email address" }
placeholder={ _t("Add email address") }
blurToCancel={ false }
onValueChanged={ this._onAddEmailEditFinished } />
</div>
@ -994,7 +1092,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get().isGuest()) {
accountJsx = (
<div className="mx_UserSettings_button" onClick={this.onUpgradeClicked}>
Create an account
{ _t("Create an account") }
</div>
);
} else {
@ -1012,7 +1110,7 @@ module.exports = React.createClass({
let notificationArea;
if (!MatrixClientPeg.get().isGuest() && this.state.threepids !== undefined) {
notificationArea = (<div>
<h3>Notifications</h3>
<h3>{ _t("Notifications") }</h3>
<div className="mx_UserSettings_section">
<Notifications threepids={this.state.threepids} brand={this.props.brand} />
@ -1031,7 +1129,7 @@ module.exports = React.createClass({
return (
<div className="mx_UserSettings">
<SimpleRoomHeader
title="Settings"
title={ _t("Settings") }
collapsedRhs={ this.props.collapsedRhs }
onCancelClick={ this.props.onClose }
/>
@ -1039,13 +1137,13 @@ module.exports = React.createClass({
<GeminiScrollbar className="mx_UserSettings_body"
autoshow={true}>
<h3>Profile</h3>
<h3>{ _t("Profile") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_profileTable">
<div className="mx_UserSettings_profileTableRow">
<div className="mx_UserSettings_profileLabelCell">
<label htmlFor="displayName">Display name</label>
<label htmlFor="displayName">{ _t('Display name') }</label>
</div>
<div className="mx_UserSettings_profileInputCell">
<ChangeDisplayName />
@ -1062,7 +1160,7 @@ module.exports = React.createClass({
<div className="mx_UserSettings_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg" className="mx_filterFlipColor"
alt="Upload avatar" title="Upload avatar"
alt={ _t("Upload avatar") } title={ _t("Upload avatar") }
width="17" height="15" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected}/>
@ -1070,12 +1168,12 @@ module.exports = React.createClass({
</div>
</div>
<h3>Account</h3>
<h3>{ _t("Account") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_section cadcampoHide">
<AccessibleButton className="mx_UserSettings_logout mx_UserSettings_button" onClick={this.onLogoutClicked}>
Sign out
{ _t("Sign out") }
</AccessibleButton>
{accountJsx}
@ -1093,34 +1191,35 @@ module.exports = React.createClass({
{this._renderBulkOptions()}
{this._renderBugReport()}
<h3>Advanced</h3>
{PlatformPeg.get().isElectron() && this._renderElectronSettings()}
{this._renderAnalyticsControl()}
<h3>{ _t("Advanced") }</h3>
<div className="mx_UserSettings_section">
<div className="mx_UserSettings_advanced">
Logged in as {this._me}
{ _t("Logged in as:") } {this._me}
</div>
<div className="mx_UserSettings_advanced">
Access Token: <span className="mx_UserSettings_advanced_spoiler"
onClick={this._showSpoiler}
data-spoiler={ MatrixClientPeg.get().getAccessToken() }
>&lt;click to reveal&gt;</span>
{_t('Access Token:')} <span className="mx_UserSettings_advanced_spoiler" onClick={this._showSpoiler} data-spoiler={ MatrixClientPeg.get().getAccessToken() }>&lt;{ _t("click to reveal") }&gt;</span>
</div>
<div className="mx_UserSettings_advanced">
Homeserver is { MatrixClientPeg.get().getHomeserverUrl() }
{ _t("Homeserver is") } { MatrixClientPeg.get().getHomeserverUrl() }
</div>
<div className="mx_UserSettings_advanced">
Identity Server is { MatrixClientPeg.get().getIdentityServerUrl() }
{ _t("Identity Server is") } { MatrixClientPeg.get().getIdentityServerUrl() }
</div>
<div className="mx_UserSettings_advanced">
matrix-react-sdk version: {(REACT_SDK_VERSION !== '<local>')
{_t('matrix-react-sdk version:')} {(REACT_SDK_VERSION !== '<local>')
? gHVersionLabel('matrix-org/matrix-react-sdk', REACT_SDK_VERSION)
: REACT_SDK_VERSION
}<br/>
riot-web version: {(this.state.vectorVersion !== undefined)
{_t('riot-web version:')} {(this.state.vectorVersion !== undefined)
? gHVersionLabel('vector-im/riot-web', this.state.vectorVersion)
: 'unknown'
}<br/>
olm version: {olmVersionString}<br/>
{ _t("olm version:") } {olmVersionString}<br/>
</div>
</div>