merge develop
This commit is contained in:
commit
efdc5430d7
176 changed files with 7537 additions and 3401 deletions
|
@ -39,9 +39,17 @@ function getRedactedHash(hash) {
|
|||
return hash.replace(hashRegex, "#/$1");
|
||||
}
|
||||
|
||||
// Return the current origin and hash separated with a `/`. This does not include query parameters.
|
||||
// Return the current origin, path and hash separated with a `/`. This does
|
||||
// not include query parameters.
|
||||
function getRedactedUrl() {
|
||||
const { origin, pathname, hash } = window.location;
|
||||
const { origin, hash } = window.location;
|
||||
let { pathname } = window.location;
|
||||
|
||||
// Redact paths which could contain unexpected PII
|
||||
if (origin.startsWith('file://')) {
|
||||
pathname = "/<redacted>/";
|
||||
}
|
||||
|
||||
return origin + pathname + getRedactedHash(hash);
|
||||
}
|
||||
|
||||
|
@ -49,34 +57,42 @@ const customVariables = {
|
|||
'App Platform': {
|
||||
id: 1,
|
||||
expl: _td('The platform you\'re on'),
|
||||
example: 'Electron Platform',
|
||||
},
|
||||
'App Version': {
|
||||
id: 2,
|
||||
expl: _td('The version of Riot.im'),
|
||||
example: '15.0.0',
|
||||
},
|
||||
'User Type': {
|
||||
id: 3,
|
||||
expl: _td('Whether or not you\'re logged in (we don\'t record your user name)'),
|
||||
example: 'Logged In',
|
||||
},
|
||||
'Chosen Language': {
|
||||
id: 4,
|
||||
expl: _td('Your language of choice'),
|
||||
example: 'en',
|
||||
},
|
||||
'Instance': {
|
||||
id: 5,
|
||||
expl: _td('Which officially provided instance you are using, if any'),
|
||||
example: 'app',
|
||||
},
|
||||
'RTE: Uses Richtext Mode': {
|
||||
id: 6,
|
||||
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||
example: 'off',
|
||||
},
|
||||
'Homeserver URL': {
|
||||
id: 7,
|
||||
expl: _td('Your homeserver\'s URL'),
|
||||
example: 'https://matrix.org',
|
||||
},
|
||||
'Identity Server URL': {
|
||||
id: 8,
|
||||
expl: _td('Your identity server\'s URL'),
|
||||
example: 'https://vector.im',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -183,9 +199,9 @@ class Analytics {
|
|||
this._paq.push(['trackPageView']);
|
||||
}
|
||||
|
||||
trackEvent(category, action, name) {
|
||||
trackEvent(category, action, name, value) {
|
||||
if (this.disabled) return;
|
||||
this._paq.push(['trackEvent', category, action, name]);
|
||||
this._paq.push(['trackEvent', category, action, name, value]);
|
||||
}
|
||||
|
||||
logout() {
|
||||
|
@ -218,8 +234,19 @@ class Analytics {
|
|||
}
|
||||
|
||||
showDetailsModal() {
|
||||
const Tracker = window.Piwik.getAsyncTracker();
|
||||
const rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||
let rows = [];
|
||||
if (window.Piwik) {
|
||||
const Tracker = window.Piwik.getAsyncTracker();
|
||||
rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
||||
} else {
|
||||
// Piwik may not have been enabled, so show example values
|
||||
rows = Object.keys(customVariables).map(
|
||||
(k) => [
|
||||
k,
|
||||
_t('e.g. %(exampleValue)s', { exampleValue: customVariables[k].example }),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
const resolution = `${window.screen.width}x${window.screen.height}`;
|
||||
const otherVariables = [
|
||||
|
@ -247,7 +274,7 @@ class Analytics {
|
|||
<table>
|
||||
{ rows.map((row) => <tr key={row[0]}>
|
||||
<td>{ _t(customVariables[row[0]].expl) }</td>
|
||||
<td><code>{ row[1] }</code></td>
|
||||
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
|
||||
</tr>) }
|
||||
{ otherVariables.map((item, index) =>
|
||||
<tr key={index}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -60,6 +60,8 @@ import { _t } from './languageHandler';
|
|||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import { showUnknownDeviceDialogForCalls } from './cryptodevices';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
|
||||
global.mxCalls = {
|
||||
//room_id: MatrixCall
|
||||
|
@ -123,7 +125,7 @@ function _setCallListeners(call) {
|
|||
description: _t(
|
||||
"There are unknown devices in this room: "+
|
||||
"if you proceed without verifying them, it will be "+
|
||||
"possible for someone to eavesdrop on your call."
|
||||
"possible for someone to eavesdrop on your call.",
|
||||
),
|
||||
button: _t('Review Devices'),
|
||||
onFinished: function(confirmed) {
|
||||
|
@ -246,66 +248,58 @@ function _onAction(payload) {
|
|||
|
||||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Existing Call'),
|
||||
description: _t('You are already in a call.'),
|
||||
});
|
||||
return; // don't allow >1 call to be placed.
|
||||
}
|
||||
{
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Existing Call'),
|
||||
description: _t('You are already in a call.'),
|
||||
});
|
||||
return; // don't allow >1 call to be placed.
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
return;
|
||||
}
|
||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
return;
|
||||
}
|
||||
|
||||
var members = room.getJoinedMembers();
|
||||
if (members.length <= 1) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||
description: _t('You cannot place a call with yourself.'),
|
||||
});
|
||||
return;
|
||||
} else if (members.length === 2) {
|
||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||
placeCall(call);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
const members = room.getJoinedMembers();
|
||||
if (members.length <= 1) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||
description: _t('You cannot place a call with yourself.'),
|
||||
});
|
||||
return;
|
||||
} else if (members.length === 2) {
|
||||
console.log("Place %s call in %s", payload.type, payload.room_id);
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||
placeCall(call);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'place_conference_call':
|
||||
console.log("Place conference call in %s", payload.room_id);
|
||||
if (!ConferenceHandler) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||
description: _t('Conference calls are not supported in this client'),
|
||||
});
|
||||
} else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
} else if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||
|
||||
if (MatrixClientPeg.get().isRoomEncrypted(payload.room_id)) {
|
||||
// Conference calls are implemented by sending the media to central
|
||||
// server which combines the audio from all the participants together
|
||||
// into a single stream. This is incompatible with end-to-end encryption
|
||||
|
@ -316,47 +310,75 @@ function _onAction(payload) {
|
|||
Modal.createTrackedDialog('Call Handler', 'Conference calls unsupported e2e', ErrorDialog, {
|
||||
description: _t('Conference calls are not supported in encrypted rooms'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (SettingsStore.isFeatureEnabled('feature_jitsi')) {
|
||||
_startCallApp(payload.room_id, payload.type);
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description: _t('Conference calling is in development and may not be reliable.'),
|
||||
onFinished: (confirm)=>{
|
||||
if (confirm) {
|
||||
ConferenceHandler.createNewMatrixCall(
|
||||
MatrixClientPeg.get(), payload.room_id,
|
||||
).done(function(call) {
|
||||
placeCall(call);
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Conference call failed: " + err);
|
||||
Modal.createTrackedDialog('Call Handler', 'Failed to set up conference call', ErrorDialog, {
|
||||
title: _t('Failed to set up conference call'),
|
||||
description: _t('Conference call failed.') + ' ' + ((err && err.message) ? err.message : ''),
|
||||
if (!ConferenceHandler) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference call unsupported client', ErrorDialog, {
|
||||
description: _t('Conference calls are not supported in this client'),
|
||||
});
|
||||
} else if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Conference calling in development', QuestionDialog, {
|
||||
title: _t('Warning!'),
|
||||
description: _t('Conference calling is in development and may not be reliable.'),
|
||||
onFinished: (confirm)=>{
|
||||
if (confirm) {
|
||||
ConferenceHandler.createNewMatrixCall(
|
||||
MatrixClientPeg.get(), payload.room_id,
|
||||
).done(function(call) {
|
||||
placeCall(call);
|
||||
}, function(err) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Conference call failed: " + err);
|
||||
Modal.createTrackedDialog(
|
||||
'Call Handler',
|
||||
'Failed to set up conference call',
|
||||
ErrorDialog,
|
||||
{
|
||||
title: _t('Failed to set up conference call'),
|
||||
description: (
|
||||
_t('Conference call failed.') +
|
||||
' ' + ((err && err.message) ? err.message : '')
|
||||
),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'incoming_call':
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||
// in future we could signal a "local busy" as a warning to the caller.
|
||||
// see https://github.com/vector-im/vector-web/issues/1964
|
||||
return;
|
||||
}
|
||||
{
|
||||
if (module.exports.getAnyActiveCall()) {
|
||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||
// in future we could signal a "local busy" as a warning to the caller.
|
||||
// see https://github.com/vector-im/vector-web/issues/1964
|
||||
return;
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, stop here.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
return;
|
||||
}
|
||||
// if the runtime env doesn't do VoIP, stop here.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var call = payload.call;
|
||||
_setCallListeners(call);
|
||||
_setCallState(call, call.roomId, "ringing");
|
||||
const call = payload.call;
|
||||
_setCallListeners(call);
|
||||
_setCallState(call, call.roomId, "ringing");
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
if (!calls[payload.room_id]) {
|
||||
|
@ -378,6 +400,71 @@ function _onAction(payload) {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function _startCallApp(roomId, type) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
console.error("Attempted to start conference call widget in unknown room: " + roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgets(room).filter((ev) => {
|
||||
return ev.getContent().type === 'jitsi';
|
||||
});
|
||||
if (currentJitsiWidgets.length > 0) {
|
||||
console.warn(
|
||||
"Refusing to start conference call widget in " + roomId +
|
||||
" a conference call widget is already present",
|
||||
);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is already in progress!'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// This inherits its poor naming from the field of the same name that goes into
|
||||
// the event. It's just a random string to make the Jitsi URLs unique.
|
||||
const widgetSessionId = Math.random().toString(36).substring(2);
|
||||
const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId;
|
||||
// NB. we can't just encodeURICompoent all of these because the $ signs need to be there
|
||||
// (but currently the only thing that needs encoding is the confId)
|
||||
const queryString = [
|
||||
'confId='+encodeURIComponent(confId),
|
||||
'isAudioConf='+(type === 'voice' ? 'true' : 'false'),
|
||||
'displayName=$matrix_display_name',
|
||||
'avatarUrl=$matrix_avatar_url',
|
||||
'email=$matrix_user_id',
|
||||
].join('&');
|
||||
const widgetUrl = (
|
||||
'https://scalar.vector.im/api/widgets' +
|
||||
'/jitsi.html?' +
|
||||
queryString
|
||||
);
|
||||
|
||||
const widgetData = { widgetSessionId };
|
||||
|
||||
const widgetId = (
|
||||
'jitsi_' +
|
||||
MatrixClientPeg.get().credentials.userId +
|
||||
'_' +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
WidgetUtils.setRoomWidget(roomId, widgetId, 'jitsi', widgetUrl, 'Jitsi', widgetData).then(() => {
|
||||
console.log('Jitsi widget added');
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: Nasty way of making sure we only register
|
||||
// with the dispatcher once
|
||||
if (!global.mxCallHandler) {
|
||||
|
|
|
@ -22,34 +22,44 @@ export default {
|
|||
// Only needed for Electron atm, though should work in modern browsers
|
||||
// once permission has been granted to the webapp
|
||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
const audioIn = [];
|
||||
const videoIn = [];
|
||||
const audiooutput = [];
|
||||
const audioinput = [];
|
||||
const videoinput = [];
|
||||
|
||||
if (devices.some((device) => !device.label)) return false;
|
||||
|
||||
devices.forEach((device) => {
|
||||
switch (device.kind) {
|
||||
case 'audioinput': audioIn.push(device); break;
|
||||
case 'videoinput': videoIn.push(device); break;
|
||||
case 'audiooutput': audiooutput.push(device); break;
|
||||
case 'audioinput': audioinput.push(device); break;
|
||||
case 'videoinput': videoinput.push(device); break;
|
||||
}
|
||||
});
|
||||
|
||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
||||
return {
|
||||
audioinput: audioIn,
|
||||
videoinput: videoIn,
|
||||
audiooutput,
|
||||
audioinput,
|
||||
videoinput,
|
||||
};
|
||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
||||
},
|
||||
|
||||
loadDevices: function() {
|
||||
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
|
||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||
|
||||
Matrix.setMatrixCallAudioOutput(audioOutDeviceId);
|
||||
Matrix.setMatrixCallAudioInput(audioDeviceId);
|
||||
Matrix.setMatrixCallVideoInput(videoDeviceId);
|
||||
},
|
||||
|
||||
setAudioOutput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioOutput(deviceId);
|
||||
},
|
||||
|
||||
setAudioInput: function(deviceId) {
|
||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||
Matrix.setMatrixCallAudioInput(deviceId);
|
||||
|
|
|
@ -243,6 +243,7 @@ function uploadFile(matrixClient, roomId, file, progressHandler) {
|
|||
const blob = new Blob([encryptResult.data]);
|
||||
return matrixClient.uploadContent(blob, {
|
||||
progressHandler: progressHandler,
|
||||
includeFilename: false,
|
||||
}).then(function(url) {
|
||||
// If the attachment is encrypted then bundle the URL along
|
||||
// with the information needed to decrypt the attachment and
|
||||
|
|
202
src/DecryptionFailureTracker.js
Normal file
202
src/DecryptionFailureTracker.js
Normal file
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class DecryptionFailure {
|
||||
constructor(failedEventId, errorCode) {
|
||||
this.failedEventId = failedEventId;
|
||||
this.errorCode = errorCode;
|
||||
this.ts = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
export class DecryptionFailureTracker {
|
||||
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
|
||||
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
|
||||
// are accumulated in `failureCounts`.
|
||||
failures = [];
|
||||
|
||||
// A histogram of the number of failures that will be tracked at the next tracking
|
||||
// interval, split by failure error code.
|
||||
failureCounts = {
|
||||
// [errorCode]: 42
|
||||
};
|
||||
|
||||
// Event IDs of failures that were tracked previously
|
||||
trackedEventHashMap = {
|
||||
// [eventId]: true
|
||||
};
|
||||
|
||||
// Set to an interval ID when `start` is called
|
||||
checkInterval = null;
|
||||
trackInterval = null;
|
||||
|
||||
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
||||
static TRACK_INTERVAL_MS = 60000;
|
||||
|
||||
// Call `checkFailures` every `CHECK_INTERVAL_MS`.
|
||||
static CHECK_INTERVAL_MS = 5000;
|
||||
|
||||
// Give events a chance to be decrypted by waiting `GRACE_PERIOD_MS` before counting
|
||||
// the failure in `failureCounts`.
|
||||
static GRACE_PERIOD_MS = 60000;
|
||||
|
||||
/**
|
||||
* Create a new DecryptionFailureTracker.
|
||||
*
|
||||
* Call `eventDecrypted(event, err)` on this instance when an event is decrypted.
|
||||
*
|
||||
* Call `start()` to start the tracker, and `stop()` to stop tracking.
|
||||
*
|
||||
* @param {function} fn The tracking function, which will be called when failures
|
||||
* are tracked. The function should have a signature `(count, trackedErrorCode) => {...}`,
|
||||
* where `count` is the number of failures and `errorCode` matches the `.code` of
|
||||
* provided DecryptionError errors (by default, unless `errorCodeMapFn` is specified.
|
||||
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
||||
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
||||
*/
|
||||
constructor(fn, errorCodeMapFn) {
|
||||
if (!fn || typeof fn !== 'function') {
|
||||
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||
}
|
||||
|
||||
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
|
||||
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
|
||||
}
|
||||
|
||||
this._trackDecryptionFailure = fn;
|
||||
this._mapErrorCode = errorCodeMapFn;
|
||||
}
|
||||
|
||||
// loadTrackedEventHashMap() {
|
||||
// this.trackedEventHashMap = JSON.parse(localStorage.getItem('mx-decryption-failure-event-id-hashes')) || {};
|
||||
// }
|
||||
|
||||
// saveTrackedEventHashMap() {
|
||||
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
|
||||
// }
|
||||
|
||||
eventDecrypted(e, err) {
|
||||
if (err) {
|
||||
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
|
||||
} else {
|
||||
// Could be an event in the failures, remove it
|
||||
this.removeDecryptionFailuresForEvent(e);
|
||||
}
|
||||
}
|
||||
|
||||
addDecryptionFailure(failure) {
|
||||
this.failures.push(failure);
|
||||
}
|
||||
|
||||
removeDecryptionFailuresForEvent(e) {
|
||||
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Start checking for and tracking failures.
|
||||
*/
|
||||
start() {
|
||||
this.checkInterval = setInterval(
|
||||
() => this.checkFailures(Date.now()),
|
||||
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||
);
|
||||
|
||||
this.trackInterval = setInterval(
|
||||
() => this.trackFailures(),
|
||||
DecryptionFailureTracker.TRACK_INTERVAL_MS,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear state and stop checking for and tracking failures.
|
||||
*/
|
||||
stop() {
|
||||
clearInterval(this.checkInterval);
|
||||
clearInterval(this.trackInterval);
|
||||
|
||||
this.failures = [];
|
||||
this.failureCounts = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
|
||||
* tracked. Only mark one failure per event ID.
|
||||
* @param {number} nowTs the timestamp that represents the time now.
|
||||
*/
|
||||
checkFailures(nowTs) {
|
||||
const failuresGivenGrace = [];
|
||||
const failuresNotReady = [];
|
||||
while (this.failures.length > 0) {
|
||||
const f = this.failures.shift();
|
||||
if (nowTs > f.ts + DecryptionFailureTracker.GRACE_PERIOD_MS) {
|
||||
failuresGivenGrace.push(f);
|
||||
} else {
|
||||
failuresNotReady.push(f);
|
||||
}
|
||||
}
|
||||
this.failures = failuresNotReady;
|
||||
|
||||
// Only track one failure per event
|
||||
const dedupedFailuresMap = failuresGivenGrace.reduce(
|
||||
(map, failure) => {
|
||||
if (!this.trackedEventHashMap[failure.failedEventId]) {
|
||||
return map.set(failure.failedEventId, failure);
|
||||
} else {
|
||||
return map;
|
||||
}
|
||||
},
|
||||
// Use a map to preseve key ordering
|
||||
new Map(),
|
||||
);
|
||||
|
||||
const trackedEventIds = [...dedupedFailuresMap.keys()];
|
||||
|
||||
this.trackedEventHashMap = trackedEventIds.reduce(
|
||||
(result, eventId) => ({...result, [eventId]: true}),
|
||||
this.trackedEventHashMap,
|
||||
);
|
||||
|
||||
// Commented out for now for expediency, we need to consider unbound nature of storing
|
||||
// this in localStorage
|
||||
// this.saveTrackedEventHashMap();
|
||||
|
||||
const dedupedFailures = dedupedFailuresMap.values();
|
||||
|
||||
this._aggregateFailures(dedupedFailures);
|
||||
}
|
||||
|
||||
_aggregateFailures(failures) {
|
||||
for (const failure of failures) {
|
||||
const errorCode = failure.errorCode;
|
||||
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
||||
* function with the number of failures that should be tracked.
|
||||
*/
|
||||
trackFailures() {
|
||||
for (const errorCode of Object.keys(this.failureCounts)) {
|
||||
if (this.failureCounts[errorCode] > 0) {
|
||||
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
|
||||
|
||||
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
|
||||
this.failureCounts[errorCode] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Modal from './Modal';
|
||||
import sdk from './';
|
||||
import MultiInviter from './utils/MultiInviter';
|
||||
|
|
|
@ -217,10 +217,17 @@ const sanitizeHtmlParams = {
|
|||
m = attribs.href.match(linkifyMatrix.MATRIXTO_URL_PATTERN);
|
||||
if (m) {
|
||||
const entity = m[1];
|
||||
if (entity[0] === '@') {
|
||||
attribs.href = '#/user/' + entity;
|
||||
} else if (entity[0] === '#' || entity[0] === '!') {
|
||||
attribs.href = '#/room/' + entity;
|
||||
switch (entity[0]) {
|
||||
case '@':
|
||||
attribs.href = '#/user/' + entity;
|
||||
break;
|
||||
case '+':
|
||||
attribs.href = '#/group/' + entity;
|
||||
break;
|
||||
case '#':
|
||||
case '!':
|
||||
attribs.href = '#/room/' + entity;
|
||||
break;
|
||||
}
|
||||
delete attribs.target;
|
||||
}
|
||||
|
|
44
src/Modal.js
44
src/Modal.js
|
@ -81,7 +81,11 @@ class ModalManager {
|
|||
constructor() {
|
||||
this._counter = 0;
|
||||
|
||||
/** list of the modals we have stacked up, with the most recent at [0] */
|
||||
// The modal to prioritise over all others. If this is set, only show
|
||||
// this modal. Remove all other modals from the stack when this modal
|
||||
// is closed.
|
||||
this._priorityModal = null;
|
||||
// A list of the modals we have stacked up, with the most recent at [0]
|
||||
this._modals = [
|
||||
/* {
|
||||
elem: React component for this dialog
|
||||
|
@ -105,18 +109,18 @@ class ModalManager {
|
|||
return container;
|
||||
}
|
||||
|
||||
createTrackedDialog(analyticsAction, analyticsInfo, Element, props, className) {
|
||||
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) {
|
||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||
return this.createDialog(Element, props, className);
|
||||
return this.createDialog(...rest);
|
||||
}
|
||||
|
||||
createDialog(Element, props, className) {
|
||||
return this.createDialogAsync((cb) => {cb(Element);}, props, className);
|
||||
createDialog(Element, ...rest) {
|
||||
return this.createDialogAsync((cb) => {cb(Element);}, ...rest);
|
||||
}
|
||||
|
||||
createTrackedDialogAsync(analyticsAction, analyticsInfo, loader, props, className) {
|
||||
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) {
|
||||
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
|
||||
return this.createDialogAsync(loader, props, className);
|
||||
return this.createDialogAsync(...rest);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -137,8 +141,13 @@ class ModalManager {
|
|||
* component. (We will also pass an 'onFinished' property.)
|
||||
*
|
||||
* @param {String} className CSS class to apply to the modal wrapper
|
||||
*
|
||||
* @param {boolean} isPriorityModal if true, this modal will be displayed regardless
|
||||
* of other modals that are currently in the stack.
|
||||
* Also, when closed, all modals will be removed
|
||||
* from the stack.
|
||||
*/
|
||||
createDialogAsync(loader, props, className) {
|
||||
createDialogAsync(loader, props, className, isPriorityModal) {
|
||||
const self = this;
|
||||
const modal = {};
|
||||
|
||||
|
@ -151,6 +160,14 @@ class ModalManager {
|
|||
if (i >= 0) {
|
||||
self._modals.splice(i, 1);
|
||||
}
|
||||
|
||||
if (self._priorityModal === modal) {
|
||||
self._priorityModal = null;
|
||||
|
||||
// XXX: This is destructive
|
||||
self._modals = [];
|
||||
}
|
||||
|
||||
self._reRender();
|
||||
};
|
||||
|
||||
|
@ -167,7 +184,12 @@ class ModalManager {
|
|||
modal.onFinished = props ? props.onFinished : null;
|
||||
modal.className = className;
|
||||
|
||||
this._modals.unshift(modal);
|
||||
if (isPriorityModal) {
|
||||
// XXX: This is destructive
|
||||
this._priorityModal = modal;
|
||||
} else {
|
||||
this._modals.unshift(modal);
|
||||
}
|
||||
|
||||
this._reRender();
|
||||
return {close: closeDialog};
|
||||
|
@ -188,7 +210,7 @@ class ModalManager {
|
|||
}
|
||||
|
||||
_reRender() {
|
||||
if (this._modals.length == 0) {
|
||||
if (this._modals.length == 0 && !this._priorityModal) {
|
||||
// If there is no modal to render, make all of Riot available
|
||||
// to screen reader users again
|
||||
dis.dispatch({
|
||||
|
@ -205,7 +227,7 @@ class ModalManager {
|
|||
action: 'aria_hide_main_app',
|
||||
});
|
||||
|
||||
const modal = this._modals[0];
|
||||
const modal = this._priorityModal ? this._priorityModal : this._modals[0];
|
||||
const dialog = (
|
||||
<div className={"mx_Dialog_wrapper " + (modal.className ? modal.className : '')}>
|
||||
<div className="mx_Dialog">
|
||||
|
|
|
@ -170,15 +170,15 @@ const Notifier = {
|
|||
value: true,
|
||||
});
|
||||
});
|
||||
// clear the notifications_hidden flag, so that if notifications are
|
||||
// disabled again in the future, we will show the banner again.
|
||||
this.setToolbarHidden(true);
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: "notifier_enabled",
|
||||
value: false,
|
||||
});
|
||||
}
|
||||
// set the notifications_hidden flag, as the user has knowingly interacted
|
||||
// with the setting we shouldn't nag them any further
|
||||
this.setToolbarHidden(true);
|
||||
},
|
||||
|
||||
isEnabled: function() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -231,11 +232,12 @@ Example:
|
|||
}
|
||||
*/
|
||||
|
||||
const SdkConfig = require('./SdkConfig');
|
||||
const MatrixClientPeg = require("./MatrixClientPeg");
|
||||
const MatrixEvent = require("matrix-js-sdk").MatrixEvent;
|
||||
const dis = require("./dispatcher");
|
||||
const Widgets = require('./utils/widgets');
|
||||
import SdkConfig from './SdkConfig';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import { MatrixEvent } from 'matrix-js-sdk';
|
||||
import dis from './dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import RoomViewStore from './stores/RoomViewStore';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
function sendResponse(event, res) {
|
||||
|
@ -286,51 +288,6 @@ function inviteUser(event, roomId, userId) {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when a widget with the given
|
||||
* ID has been added as a user widget (ie. the accountData event
|
||||
* arrives) or rejects after a timeout
|
||||
*
|
||||
* @param {string} widgetId The ID of the widget to wait for
|
||||
* @param {boolean} add True to wait for the widget to be added,
|
||||
* false to wait for it to be deleted.
|
||||
* @returns {Promise} that resolves when the widget is available
|
||||
*/
|
||||
function waitForUserWidget(widgetId, add) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const currentAccountDataEvent = MatrixClientPeg.get().getAccountData('m.widgets');
|
||||
|
||||
// Tests an account data event, returning true if it's in the state
|
||||
// we're waiting for it to be in
|
||||
function eventInIntendedState(ev) {
|
||||
if (!ev || !currentAccountDataEvent.getContent()) return false;
|
||||
if (add) {
|
||||
return ev.getContent()[widgetId] !== undefined;
|
||||
} else {
|
||||
return ev.getContent()[widgetId] === undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (eventInIntendedState(currentAccountDataEvent)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function onAccountData(ev) {
|
||||
if (eventInIntendedState(currentAccountDataEvent)) {
|
||||
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
||||
clearTimeout(timerId);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
const timerId = setTimeout(() => {
|
||||
MatrixClientPeg.get().removeListener('accountData', onAccountData);
|
||||
reject(new Error("Timed out waiting for widget ID " + widgetId + " to appear"));
|
||||
}, 10000);
|
||||
MatrixClientPeg.get().on('accountData', onAccountData);
|
||||
});
|
||||
}
|
||||
|
||||
function setWidget(event, roomId) {
|
||||
const widgetId = event.data.widget_id;
|
||||
const widgetType = event.data.type;
|
||||
|
@ -339,12 +296,6 @@ function setWidget(event, roomId) {
|
|||
const widgetData = event.data.data; // optional
|
||||
const userWidget = event.data.userWidget;
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
sendError(event, _t('You need to be logged in.'));
|
||||
return;
|
||||
}
|
||||
|
||||
// both adding/removing widgets need these checks
|
||||
if (!widgetId || widgetUrl === undefined) {
|
||||
sendError(event, _t("Unable to create widget."), new Error("Missing required widget fields."));
|
||||
|
@ -371,42 +322,8 @@ function setWidget(event, roomId) {
|
|||
}
|
||||
}
|
||||
|
||||
let content = {
|
||||
type: widgetType,
|
||||
url: widgetUrl,
|
||||
name: widgetName,
|
||||
data: widgetData,
|
||||
};
|
||||
|
||||
if (userWidget) {
|
||||
const client = MatrixClientPeg.get();
|
||||
const userWidgets = Widgets.getUserWidgets();
|
||||
|
||||
// Delete existing widget with ID
|
||||
try {
|
||||
delete userWidgets[widgetId];
|
||||
} catch (e) {
|
||||
console.error(`$widgetId is non-configurable`);
|
||||
}
|
||||
|
||||
// Add new widget / update
|
||||
if (widgetUrl !== null) {
|
||||
userWidgets[widgetId] = {
|
||||
content: content,
|
||||
sender: client.getUserId(),
|
||||
state_key: widgetId,
|
||||
type: 'm.widget',
|
||||
id: widgetId,
|
||||
};
|
||||
}
|
||||
|
||||
// This starts listening for when the echo comes back from the server
|
||||
// since the widget won't appear added until this happens. If we don't
|
||||
// wait for this, the action will complete but if the user is fast enough,
|
||||
// the widget still won't actually be there.
|
||||
client.setAccountData('m.widgets', userWidgets).then(() => {
|
||||
return waitForUserWidget(widgetId, widgetUrl !== null);
|
||||
}).then(() => {
|
||||
WidgetUtils.setUserWidget(widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
|
||||
sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
|
@ -419,15 +336,7 @@ function setWidget(event, roomId) {
|
|||
if (!roomId) {
|
||||
sendError(event, _t('Missing roomId.'), null);
|
||||
}
|
||||
|
||||
if (widgetUrl === null) { // widget is being deleted
|
||||
content = {};
|
||||
}
|
||||
// TODO - Room widgets need to be moved to 'm.widget' state events
|
||||
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
||||
client.sendStateEvent(roomId, "im.vector.modular.widgets", content, widgetId).done(() => {
|
||||
// XXX: We should probably wait for the echo of the state event to come back from the server,
|
||||
// as we do with user widgets.
|
||||
WidgetUtils.setRoomWidget(roomId, widgetId, widgetType, widgetUrl, widgetName, widgetData).then(() => {
|
||||
sendResponse(event, {
|
||||
success: true,
|
||||
});
|
||||
|
@ -451,21 +360,13 @@ function getWidgets(event, roomId) {
|
|||
sendError(event, _t('This room is not recognised.'));
|
||||
return;
|
||||
}
|
||||
// TODO - Room widgets need to be moved to 'm.widget' state events
|
||||
// https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing
|
||||
const stateEvents = room.currentState.getStateEvents("im.vector.modular.widgets");
|
||||
// Only return widgets which have required fields
|
||||
if (room) {
|
||||
stateEvents.forEach((ev) => {
|
||||
if (ev.getContent().type && ev.getContent().url) {
|
||||
widgetStateEvents.push(ev.event); // return the raw event
|
||||
}
|
||||
});
|
||||
}
|
||||
// XXX: This gets the raw event object (I think because we can't
|
||||
// send the MatrixEvent over postMessage?)
|
||||
widgetStateEvents = WidgetUtils.getRoomWidgets(room).map((ev) => ev.event);
|
||||
}
|
||||
|
||||
// Add user widgets (not linked to a specific room)
|
||||
const userWidgets = Widgets.getUserWidgetsArray();
|
||||
const userWidgets = WidgetUtils.getUserWidgetsArray();
|
||||
widgetStateEvents = widgetStateEvents.concat(userWidgets);
|
||||
|
||||
sendResponse(event, widgetStateEvents);
|
||||
|
@ -637,19 +538,6 @@ function returnStateEvent(event, roomId, eventType, stateKey) {
|
|||
sendResponse(event, stateEvent.getContent());
|
||||
}
|
||||
|
||||
let currentRoomId = null;
|
||||
let currentRoomAlias = null;
|
||||
|
||||
// Listen for when a room is viewed
|
||||
dis.register(onAction);
|
||||
function onAction(payload) {
|
||||
if (payload.action !== "view_room") {
|
||||
return;
|
||||
}
|
||||
currentRoomId = payload.room_id;
|
||||
currentRoomAlias = payload.room_alias;
|
||||
}
|
||||
|
||||
const onMessage = function(event) {
|
||||
if (!event.origin) { // stupid chrome
|
||||
event.origin = event.originalEvent.origin;
|
||||
|
@ -700,80 +588,63 @@ const onMessage = function(event) {
|
|||
return;
|
||||
}
|
||||
}
|
||||
let promise = Promise.resolve(currentRoomId);
|
||||
if (!currentRoomId) {
|
||||
if (!currentRoomAlias) {
|
||||
sendError(event, _t('Must be viewing a room'));
|
||||
return;
|
||||
}
|
||||
// no room ID but there is an alias, look it up.
|
||||
console.log("Looking up alias " + currentRoomAlias);
|
||||
promise = MatrixClientPeg.get().getRoomIdForAlias(currentRoomAlias).then((res) => {
|
||||
return res.room_id;
|
||||
});
|
||||
|
||||
if (roomId !== RoomViewStore.getRoomId()) {
|
||||
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
||||
return;
|
||||
}
|
||||
|
||||
promise.then((viewingRoomId) => {
|
||||
if (roomId !== viewingRoomId) {
|
||||
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
||||
return;
|
||||
}
|
||||
// Get and set room-based widgets
|
||||
if (event.data.action === "get_widgets") {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
setWidget(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get and set room-based widgets
|
||||
if (event.data.action === "get_widgets") {
|
||||
getWidgets(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_widget") {
|
||||
setWidget(event, roomId);
|
||||
return;
|
||||
}
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_plumbing_state") {
|
||||
setPlumbingState(event, roomId, event.data.status);
|
||||
return;
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
getMembershipCount(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_room_enc_state") {
|
||||
getRoomEncState(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
// These APIs don't require userId
|
||||
if (event.data.action === "join_rules_state") {
|
||||
getJoinRules(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "set_plumbing_state") {
|
||||
setPlumbingState(event, roomId, event.data.status);
|
||||
return;
|
||||
} else if (event.data.action === "get_membership_count") {
|
||||
getMembershipCount(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "get_room_enc_state") {
|
||||
getRoomEncState(event, roomId);
|
||||
return;
|
||||
} else if (event.data.action === "can_send_event") {
|
||||
canSendEvent(event, roomId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
sendError(event, _t('Missing user_id in request'));
|
||||
return;
|
||||
}
|
||||
switch (event.data.action) {
|
||||
case "membership_state":
|
||||
getMembershipState(event, roomId, userId);
|
||||
break;
|
||||
case "invite":
|
||||
inviteUser(event, roomId, userId);
|
||||
break;
|
||||
case "bot_options":
|
||||
botOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_options":
|
||||
setBotOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_power":
|
||||
setBotPower(event, roomId, userId, event.data.level);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||
break;
|
||||
}
|
||||
}, (err) => {
|
||||
console.error(err);
|
||||
sendError(event, _t('Failed to lookup current room') + '.');
|
||||
});
|
||||
if (!userId) {
|
||||
sendError(event, _t('Missing user_id in request'));
|
||||
return;
|
||||
}
|
||||
switch (event.data.action) {
|
||||
case "membership_state":
|
||||
getMembershipState(event, roomId, userId);
|
||||
break;
|
||||
case "invite":
|
||||
inviteUser(event, roomId, userId);
|
||||
break;
|
||||
case "bot_options":
|
||||
botOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_options":
|
||||
setBotOptions(event, roomId, userId);
|
||||
break;
|
||||
case "set_bot_power":
|
||||
setBotPower(event, roomId, userId, event.data.level);
|
||||
break;
|
||||
default:
|
||||
console.warn("Unhandled postMessage event with action '" + event.data.action +"'");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let listenerCount = 0;
|
||||
|
|
|
@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MatrixClientPeg from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher";
|
||||
import Tinter from "./Tinter";
|
||||
|
||||
import React from 'react';
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
import dis from './dispatcher';
|
||||
import Tinter from './Tinter';
|
||||
import sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import {_t, _td} from './languageHandler';
|
||||
import Modal from './Modal';
|
||||
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
|
||||
import SettingsStore, {SettingLevel} from './settings/SettingsStore';
|
||||
|
||||
|
||||
class Command {
|
||||
constructor(name, paramArgs, runFn) {
|
||||
this.name = name;
|
||||
this.paramArgs = paramArgs;
|
||||
constructor({name, args='', description, runFn}) {
|
||||
this.command = '/' + name;
|
||||
this.args = args;
|
||||
this.description = description;
|
||||
this.runFn = runFn;
|
||||
}
|
||||
|
||||
getCommand() {
|
||||
return "/" + this.name;
|
||||
return this.command;
|
||||
}
|
||||
|
||||
getCommandWithArgs() {
|
||||
return this.getCommand() + " " + this.paramArgs;
|
||||
return this.getCommand() + " " + this.args;
|
||||
}
|
||||
|
||||
run(roomId, args) {
|
||||
|
@ -47,16 +50,12 @@ class Command {
|
|||
}
|
||||
}
|
||||
|
||||
function reject(msg) {
|
||||
return {
|
||||
error: msg,
|
||||
};
|
||||
function reject(error) {
|
||||
return {error};
|
||||
}
|
||||
|
||||
function success(promise) {
|
||||
return {
|
||||
promise: promise,
|
||||
};
|
||||
return {promise};
|
||||
}
|
||||
|
||||
/* Disable the "unexpected this" error for these commands - all of the run
|
||||
|
@ -65,352 +64,408 @@ function success(promise) {
|
|||
|
||||
/* eslint-disable babel/no-invalid-this */
|
||||
|
||||
const commands = {
|
||||
ddg: new Command("ddg", "<query>", function(roomId, args) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
// TODO Don't explain this away, actually show a search UI here.
|
||||
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||
title: _t('/ddg is not a command'),
|
||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||
});
|
||||
return success();
|
||||
export const CommandMap = {
|
||||
ddg: new Command({
|
||||
name: 'ddg',
|
||||
args: '<query>',
|
||||
description: _td('Searches DuckDuckGo for results'),
|
||||
runFn: function(roomId, args) {
|
||||
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
|
||||
// TODO Don't explain this away, actually show a search UI here.
|
||||
Modal.createTrackedDialog('Slash Commands', '/ddg is not a command', ErrorDialog, {
|
||||
title: _t('/ddg is not a command'),
|
||||
description: _t('To use it, just wait for autocomplete results to load and tab through them.'),
|
||||
});
|
||||
return success();
|
||||
},
|
||||
}),
|
||||
|
||||
// Change your nickname
|
||||
nick: new Command("nick", "<display_name>", function(roomId, args) {
|
||||
if (args) {
|
||||
return success(
|
||||
MatrixClientPeg.get().setDisplayName(args),
|
||||
);
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Changes the colorscheme of your current room
|
||||
tint: new Command("tint", "<color1> [<color2>]", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}))( +(#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})))?$/);
|
||||
if (matches) {
|
||||
Tinter.tint(matches[1], matches[4]);
|
||||
const colorScheme = {};
|
||||
colorScheme.primary_color = matches[1];
|
||||
if (matches[4]) {
|
||||
colorScheme.secondary_color = matches[4];
|
||||
} else {
|
||||
colorScheme.secondary_color = colorScheme.primary_color;
|
||||
}
|
||||
return success(
|
||||
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||
);
|
||||
nick: new Command({
|
||||
name: 'nick',
|
||||
args: '<display_name>',
|
||||
description: _td('Changes your display nickname'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
return success(MatrixClientPeg.get().setDisplayName(args));
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Change the room topic
|
||||
topic: new Command("topic", "<topic>", function(roomId, args) {
|
||||
if (args) {
|
||||
return success(
|
||||
MatrixClientPeg.get().setRoomTopic(roomId, args),
|
||||
);
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Invite a user
|
||||
invite: new Command("invite", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
return success(
|
||||
MatrixClientPeg.get().invite(roomId, matches[1]),
|
||||
);
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
// Join a room
|
||||
join: new Command("join", "#alias:domain", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
let roomAlias = matches[1];
|
||||
if (roomAlias[0] !== '#') {
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
if (!roomAlias.match(/:/)) {
|
||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_alias: roomAlias,
|
||||
auto_join: true,
|
||||
});
|
||||
|
||||
return success();
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
}),
|
||||
|
||||
part: new Command("part", "[#alias:domain]", function(roomId, args) {
|
||||
let targetRoomId;
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
let roomAlias = matches[1];
|
||||
if (roomAlias[0] !== '#') {
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
if (!roomAlias.match(/:/)) {
|
||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
const rooms = MatrixClientPeg.get().getRooms();
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
const aliasEvents = rooms[i].currentState.getStateEvents(
|
||||
"m.room.aliases",
|
||||
);
|
||||
for (let j = 0; j < aliasEvents.length; j++) {
|
||||
const aliases = aliasEvents[j].getContent().aliases || [];
|
||||
for (let k = 0; k < aliases.length; k++) {
|
||||
if (aliases[k] === roomAlias) {
|
||||
targetRoomId = rooms[i].roomId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetRoomId) { break; }
|
||||
tint: new Command({
|
||||
name: 'tint',
|
||||
args: '<color1> [<color2>]',
|
||||
description: _td('Changes colour scheme of current room'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(#([\da-fA-F]{3}|[\da-fA-F]{6}))( +(#([\da-fA-F]{3}|[\da-fA-F]{6})))?$/);
|
||||
if (matches) {
|
||||
Tinter.tint(matches[1], matches[4]);
|
||||
const colorScheme = {};
|
||||
colorScheme.primary_color = matches[1];
|
||||
if (matches[4]) {
|
||||
colorScheme.secondary_color = matches[4];
|
||||
} else {
|
||||
colorScheme.secondary_color = colorScheme.primary_color;
|
||||
}
|
||||
if (targetRoomId) { break; }
|
||||
}
|
||||
if (!targetRoomId) {
|
||||
return reject(_t("Unrecognised room alias:") + ' ' + roomAlias);
|
||||
return success(
|
||||
SettingsStore.setValue('roomColor', roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!targetRoomId) targetRoomId = roomId;
|
||||
return success(
|
||||
MatrixClientPeg.get().leave(targetRoomId).then(
|
||||
function() {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
},
|
||||
),
|
||||
);
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Kick a user from the room with an optional reason
|
||||
kick: new Command("kick", "<userId> [<reason>]", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
if (matches) {
|
||||
return success(
|
||||
MatrixClientPeg.get().kick(roomId, matches[1], matches[3]),
|
||||
);
|
||||
topic: new Command({
|
||||
name: 'topic',
|
||||
args: '<topic>',
|
||||
description: _td('Sets the room topic'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
return success(MatrixClientPeg.get().setRoomTopic(roomId, args));
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
invite: new Command({
|
||||
name: 'invite',
|
||||
args: '<user-id>',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
return success(MatrixClientPeg.get().invite(roomId, matches[1]));
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
join: new Command({
|
||||
name: 'join',
|
||||
args: '<room-alias>',
|
||||
description: _td('Joins room with given alias'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
let roomAlias = matches[1];
|
||||
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||
|
||||
if (!roomAlias.includes(':')) {
|
||||
roomAlias += ':' + MatrixClientPeg.get().getDomain();
|
||||
}
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_alias: roomAlias,
|
||||
auto_join: true,
|
||||
});
|
||||
|
||||
return success();
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
part: new Command({
|
||||
name: 'part',
|
||||
args: '[<room-alias>]',
|
||||
description: _td('Leave room'),
|
||||
runFn: function(roomId, args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
let targetRoomId;
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
let roomAlias = matches[1];
|
||||
if (roomAlias[0] !== '#') return reject(this.getUsage());
|
||||
|
||||
if (!roomAlias.includes(':')) {
|
||||
roomAlias += ':' + cli.getDomain();
|
||||
}
|
||||
|
||||
// Try to find a room with this alias
|
||||
const rooms = cli.getRooms();
|
||||
for (let i = 0; i < rooms.length; i++) {
|
||||
const aliasEvents = rooms[i].currentState.getStateEvents('m.room.aliases');
|
||||
for (let j = 0; j < aliasEvents.length; j++) {
|
||||
const aliases = aliasEvents[j].getContent().aliases || [];
|
||||
for (let k = 0; k < aliases.length; k++) {
|
||||
if (aliases[k] === roomAlias) {
|
||||
targetRoomId = rooms[i].roomId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetRoomId) break;
|
||||
}
|
||||
if (targetRoomId) break;
|
||||
}
|
||||
if (!targetRoomId) return reject(_t('Unrecognised room alias:') + ' ' + roomAlias);
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetRoomId) targetRoomId = roomId;
|
||||
return success(
|
||||
cli.leave(targetRoomId).then(function() {
|
||||
dis.dispatch({action: 'view_next_room'});
|
||||
}),
|
||||
);
|
||||
},
|
||||
}),
|
||||
|
||||
kick: new Command({
|
||||
name: 'kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Kicks user with given id'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
if (matches) {
|
||||
return success(MatrixClientPeg.get().kick(roomId, matches[1], matches[3]));
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Ban a user from the room with an optional reason
|
||||
ban: new Command("ban", "<userId> [<reason>]", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
if (matches) {
|
||||
return success(
|
||||
MatrixClientPeg.get().ban(roomId, matches[1], matches[3]),
|
||||
);
|
||||
ban: new Command({
|
||||
name: 'ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Bans user with given id'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||
if (matches) {
|
||||
return success(MatrixClientPeg.get().ban(roomId, matches[1], matches[3]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Unban a user from the room
|
||||
unban: new Command("unban", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
// Reset the user membership to "leave" to unban him
|
||||
return success(
|
||||
MatrixClientPeg.get().unban(roomId, matches[1]),
|
||||
);
|
||||
// Unban a user from ythe room
|
||||
unban: new Command({
|
||||
name: 'unban',
|
||||
args: '<user-id>',
|
||||
description: _td('Unbans user with given id'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
// Reset the user membership to "leave" to unban him
|
||||
return success(MatrixClientPeg.get().unban(roomId, matches[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
ignore: new Command("ignore", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||
return success(
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||
title: _t("Ignored user"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t("You are now ignoring %(userId)s", {userId: userId}) }</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
ignore: new Command({
|
||||
name: 'ignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Ignores a user, hiding their messages from you'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
ignoredUsers.push(userId); // de-duped internally in the js-sdk
|
||||
return success(
|
||||
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', QuestionDialog, {
|
||||
title: _t('Ignored user'),
|
||||
description: <div>
|
||||
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
unignore: new Command("unignore", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
return success(
|
||||
MatrixClientPeg.get().setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||
title: _t("Unignored user"),
|
||||
description: (
|
||||
<div>
|
||||
<p>{ _t("You are no longer ignoring %(userId)s", {userId: userId}) }</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
unignore: new Command({
|
||||
name: 'unignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const ignoredUsers = cli.getIgnoredUsers();
|
||||
const index = ignoredUsers.indexOf(userId);
|
||||
if (index !== -1) ignoredUsers.splice(index, 1);
|
||||
return success(
|
||||
cli.setIgnoredUsers(ignoredUsers).then(() => {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', QuestionDialog, {
|
||||
title: _t('Unignored user'),
|
||||
description: <div>
|
||||
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Define the power level of a user
|
||||
op: new Command("op", "<userId> [<power level>]", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||
let powerLevel = 50; // default power level for op
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
if (matches.length === 4 && undefined !== matches[3]) {
|
||||
powerLevel = parseInt(matches[3]);
|
||||
}
|
||||
if (!isNaN(powerLevel)) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
return reject("Bad room ID: " + roomId);
|
||||
op: new Command({
|
||||
name: 'op',
|
||||
args: '<user-id> [<power-level>]',
|
||||
description: _td('Define the power level of a user'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+?)( +(-?\d+))?$/);
|
||||
let powerLevel = 50; // default power level for op
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
if (matches.length === 4 && undefined !== matches[3]) {
|
||||
powerLevel = parseInt(matches[3]);
|
||||
}
|
||||
if (!isNaN(powerLevel)) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
if (!room) return reject('Bad room ID: ' + roomId);
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
return success(cli.setPowerLevel(roomId, userId, powerLevel, powerLevelEvent));
|
||||
}
|
||||
const powerLevelEvent = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", "",
|
||||
);
|
||||
return success(
|
||||
MatrixClientPeg.get().setPowerLevel(
|
||||
roomId, userId, powerLevel, powerLevelEvent,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Reset the power level of a user
|
||||
deop: new Command("deop", "<userId>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (!room) {
|
||||
return reject("Bad room ID: " + roomId);
|
||||
}
|
||||
deop: new Command({
|
||||
name: 'deop',
|
||||
args: '<user-id>',
|
||||
description: _td('Deops user with given id'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+)$/);
|
||||
if (matches) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const room = cli.getRoom(roomId);
|
||||
if (!room) return reject('Bad room ID: ' + roomId);
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents(
|
||||
"m.room.power_levels", "",
|
||||
);
|
||||
return success(
|
||||
MatrixClientPeg.get().setPowerLevel(
|
||||
roomId, args, undefined, powerLevelEvent,
|
||||
),
|
||||
);
|
||||
const powerLevelEvent = room.currentState.getStateEvents('m.room.power_levels', '');
|
||||
return success(cli.setPowerLevel(roomId, args, undefined, powerLevelEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Open developer tools
|
||||
devtools: new Command("devtools", "", function(roomId) {
|
||||
const DevtoolsDialog = sdk.getComponent("dialogs.DevtoolsDialog");
|
||||
Modal.createDialog(DevtoolsDialog, { roomId });
|
||||
return success();
|
||||
devtools: new Command({
|
||||
name: 'devtools',
|
||||
description: _td('Opens the Developer Tools dialog'),
|
||||
runFn: function(roomId) {
|
||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||
Modal.createDialog(DevtoolsDialog, {roomId});
|
||||
return success();
|
||||
},
|
||||
}),
|
||||
|
||||
// Verify a user, device, and pubkey tuple
|
||||
verify: new Command("verify", "<userId> <deviceId> <deviceSigningKey>", function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||
if (matches) {
|
||||
const userId = matches[1];
|
||||
const deviceId = matches[2];
|
||||
const fingerprint = matches[3];
|
||||
verify: new Command({
|
||||
name: 'verify',
|
||||
args: '<user-id> <device-id> <device-signing-key>',
|
||||
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||
runFn: function(roomId, args) {
|
||||
if (args) {
|
||||
const matches = args.match(/^(\S+) +(\S+) +(\S+)$/);
|
||||
if (matches) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
return success(
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(MatrixClientPeg.get().getStoredDevice(userId, deviceId)).then((device) => {
|
||||
if (!device) {
|
||||
throw new Error(_t(`Unknown (user, device) pair:`) + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
const userId = matches[1];
|
||||
const deviceId = matches[2];
|
||||
const fingerprint = matches[3];
|
||||
|
||||
if (device.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new Error(_t(`Device already verified!`));
|
||||
} else {
|
||||
throw new Error(_t(`WARNING: Device already verified, but keys do NOT MATCH!`));
|
||||
return success(
|
||||
// Promise.resolve to handle transition from static result to promise; can be removed
|
||||
// in future
|
||||
Promise.resolve(cli.getStoredDevice(userId, deviceId)).then((device) => {
|
||||
if (!device) {
|
||||
throw new Error(_t('Unknown (user, device) pair:') + ` (${userId}, ${deviceId})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new Error(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key' +
|
||||
' "%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{deviceId: deviceId, fprint: fprint, userId: userId, fingerprint: fingerprint}));
|
||||
}
|
||||
if (device.isVerified()) {
|
||||
if (device.getFingerprint() === fingerprint) {
|
||||
throw new Error(_t('Device already verified!'));
|
||||
} else {
|
||||
throw new Error(_t('WARNING: Device already verified, but keys do NOT MATCH!'));
|
||||
}
|
||||
}
|
||||
|
||||
return MatrixClientPeg.get().setDeviceVerified(userId, deviceId, true);
|
||||
}).then(() => {
|
||||
// Tell the user we verified everything
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||
title: _t("Verified key"),
|
||||
description: (
|
||||
<div>
|
||||
<p>
|
||||
{
|
||||
_t("The signing key you provided matches the signing key you received " +
|
||||
"from %(userId)s's device %(deviceId)s. Device marked as verified.",
|
||||
{userId: userId, deviceId: deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
if (device.getFingerprint() !== fingerprint) {
|
||||
const fprint = device.getFingerprint();
|
||||
throw new Error(
|
||||
_t('WARNING: KEY VERIFICATION FAILED! The signing key for %(userId)s and device' +
|
||||
' %(deviceId)s is "%(fprint)s" which does not match the provided key ' +
|
||||
'"%(fingerprint)s". This could mean your communications are being intercepted!',
|
||||
{
|
||||
fprint,
|
||||
userId,
|
||||
deviceId,
|
||||
fingerprint,
|
||||
}));
|
||||
}
|
||||
|
||||
return cli.setDeviceVerified(userId, deviceId, true);
|
||||
}).then(() => {
|
||||
// Tell the user we verified everything
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Slash Commands', 'Verified key', QuestionDialog, {
|
||||
title: _t('Verified key'),
|
||||
description: <div>
|
||||
<p>
|
||||
{
|
||||
_t('The signing key you provided matches the signing key you received ' +
|
||||
'from %(userId)s\'s device %(deviceId)s. Device marked as verified.',
|
||||
{userId, deviceId})
|
||||
}
|
||||
</p>
|
||||
</div>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return reject(this.getUsage());
|
||||
return reject(this.getUsage());
|
||||
},
|
||||
}),
|
||||
|
||||
// Command definitions for autocompletion ONLY:
|
||||
|
||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||
me: new Command({
|
||||
name: 'me',
|
||||
args: '<message>',
|
||||
description: _td('Displays action'),
|
||||
}),
|
||||
};
|
||||
/* eslint-enable babel/no-invalid-this */
|
||||
|
@ -421,50 +476,40 @@ const aliases = {
|
|||
j: "join",
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Process the given text for /commands and perform them.
|
||||
* @param {string} roomId The room in which the command was performed.
|
||||
* @param {string} input The raw text input by the user.
|
||||
* @return {Object|null} An object with the property 'error' if there was an error
|
||||
* processing the command, or 'promise' if a request was sent out.
|
||||
* Returns null if the input didn't match a command.
|
||||
*/
|
||||
processInput: function(roomId, input) {
|
||||
// trim any trailing whitespace, as it can confuse the parser for
|
||||
// IRC-style commands
|
||||
input = input.replace(/\s+$/, "");
|
||||
if (input[0] === "/") {
|
||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||
let cmd;
|
||||
let args;
|
||||
if (bits) {
|
||||
cmd = bits[1].substring(1).toLowerCase();
|
||||
args = bits[3];
|
||||
} else {
|
||||
cmd = input;
|
||||
}
|
||||
if (cmd === "me") return null;
|
||||
if (aliases[cmd]) {
|
||||
cmd = aliases[cmd];
|
||||
}
|
||||
if (commands[cmd]) {
|
||||
return commands[cmd].run(roomId, args);
|
||||
} else {
|
||||
return reject(_t("Unrecognised command:") + ' ' + input);
|
||||
}
|
||||
}
|
||||
return null; // not a command
|
||||
},
|
||||
|
||||
getCommandList: function() {
|
||||
// Return all the commands plus /me and /markdown which aren't handled like normal commands
|
||||
const cmds = Object.keys(commands).sort().map(function(cmdKey) {
|
||||
return commands[cmdKey];
|
||||
});
|
||||
cmds.push(new Command("me", "<action>", function() {}));
|
||||
cmds.push(new Command("markdown", "<on|off>", function() {}));
|
||||
/**
|
||||
* Process the given text for /commands and perform them.
|
||||
* @param {string} roomId The room in which the command was performed.
|
||||
* @param {string} input The raw text input by the user.
|
||||
* @return {Object|null} An object with the property 'error' if there was an error
|
||||
* processing the command, or 'promise' if a request was sent out.
|
||||
* Returns null if the input didn't match a command.
|
||||
*/
|
||||
export function processCommandInput(roomId, input) {
|
||||
// trim any trailing whitespace, as it can confuse the parser for
|
||||
// IRC-style commands
|
||||
input = input.replace(/\s+$/, '');
|
||||
if (input[0] !== '/') return null; // not a command
|
||||
|
||||
return cmds;
|
||||
},
|
||||
};
|
||||
const bits = input.match(/^(\S+?)( +((.|\n)*))?$/);
|
||||
let cmd;
|
||||
let args;
|
||||
if (bits) {
|
||||
cmd = bits[1].substring(1).toLowerCase();
|
||||
args = bits[3];
|
||||
} else {
|
||||
cmd = input;
|
||||
}
|
||||
|
||||
if (aliases[cmd]) {
|
||||
cmd = aliases[cmd];
|
||||
}
|
||||
if (CommandMap[cmd]) {
|
||||
// if it has no runFn then its an ignored/nop command (autocomplete only) e.g `/me`
|
||||
if (!CommandMap[cmd].runFn) return null;
|
||||
|
||||
return CommandMap[cmd].run(roomId, args);
|
||||
} else {
|
||||
return reject(_t('Unrecognised command:') + ' ' + input);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -129,6 +129,64 @@ function textForRoomNameEvent(ev) {
|
|||
});
|
||||
}
|
||||
|
||||
function textForServerACLEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const changes = [];
|
||||
const current = ev.getContent();
|
||||
const prev = {
|
||||
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
|
||||
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
|
||||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||
};
|
||||
let text = "";
|
||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||
text = `${senderDisplayName} set server ACLs for this room: `;
|
||||
} else {
|
||||
text = `${senderDisplayName} changed the server ACLs for this room: `;
|
||||
}
|
||||
|
||||
if (!Array.isArray(current.allow)) {
|
||||
current.allow = [];
|
||||
}
|
||||
/* If we know for sure everyone is banned, don't bother showing the diff view */
|
||||
if (current.allow.length === 0) {
|
||||
return text + "🎉 All servers are banned from participating! This room can no longer be used.";
|
||||
}
|
||||
|
||||
if (!Array.isArray(current.deny)) {
|
||||
current.deny = [];
|
||||
}
|
||||
|
||||
const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv));
|
||||
const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv));
|
||||
const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv));
|
||||
const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv));
|
||||
|
||||
if (bannedServers.length > 0) {
|
||||
changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`);
|
||||
}
|
||||
|
||||
if (unbannedServers.length > 0) {
|
||||
changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`);
|
||||
}
|
||||
|
||||
if (allowedServers.length > 0) {
|
||||
changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`);
|
||||
}
|
||||
|
||||
if (unallowedServers.length > 0) {
|
||||
changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`);
|
||||
}
|
||||
|
||||
if (prev.allow_ip_literals !== current.allow_ip_literals) {
|
||||
const allowban = current.allow_ip_literals ? "allowed" : "banned";
|
||||
changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`);
|
||||
}
|
||||
|
||||
return text + changes.join(" ");
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
let message = senderDisplayName + ': ' + ev.getContent().body;
|
||||
|
@ -309,6 +367,7 @@ const stateHandlers = {
|
|||
'm.room.encryption': textForEncryptionEvent,
|
||||
'm.room.power_levels': textForPowerEvent,
|
||||
'm.room.pinned_events': textForPinnedEvent,
|
||||
'm.room.server_acl': textForServerACLEvent,
|
||||
|
||||
'im.vector.modular.widgets': textForWidgetEvent,
|
||||
};
|
||||
|
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import MatrixClientPeg from './MatrixClientPeg';
|
||||
|
||||
export default class WidgetUtils {
|
||||
/* Returns true if user is able to send state events to modify widgets in this room
|
||||
* (Does not apply to non-room-based / user widgets)
|
||||
* @param roomId -- The ID of the room to check
|
||||
* @return Boolean -- true if the user can modify widgets in this room
|
||||
* @throws Error -- specifies the error reason
|
||||
*/
|
||||
static canUserModifyWidgets(roomId) {
|
||||
if (!roomId) {
|
||||
console.warn('No room ID specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
console.warn('User must be be logged in');
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = client.getRoom(roomId);
|
||||
if (!room) {
|
||||
console.warn(`Room ID ${roomId} is not recognised`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const me = client.credentials.userId;
|
||||
if (!me) {
|
||||
console.warn('Failed to get user ID');
|
||||
return false;
|
||||
}
|
||||
|
||||
const member = room.getMember(me);
|
||||
if (!member || member.membership !== "join") {
|
||||
console.warn(`User ${me} is not in room ${roomId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return room.currentState.maySendStateEvent('im.vector.modular.widgets', me);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -57,14 +57,14 @@ export default class AutocompleteProvider {
|
|||
|
||||
let match;
|
||||
while ((match = commandRegex.exec(query)) != null) {
|
||||
let matchStart = match.index,
|
||||
matchEnd = matchStart + match[0].length;
|
||||
if (selection.start <= matchEnd && selection.end >= matchStart) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
if (selection.start <= end && selection.end >= start) {
|
||||
return {
|
||||
command: match,
|
||||
range: {
|
||||
start: matchStart,
|
||||
end: matchEnd,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,7 +18,9 @@ limitations under the License.
|
|||
// @flow
|
||||
|
||||
import type {Component} from 'react';
|
||||
import {Room} from 'matrix-js-sdk';
|
||||
import CommandProvider from './CommandProvider';
|
||||
import CommunityProvider from './CommunityProvider';
|
||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||
import RoomProvider from './RoomProvider';
|
||||
import UserProvider from './UserProvider';
|
||||
|
@ -48,6 +50,7 @@ const PROVIDERS = [
|
|||
EmojiProvider,
|
||||
NotifProvider,
|
||||
CommandProvider,
|
||||
CommunityProvider,
|
||||
DuckDuckGoProvider,
|
||||
];
|
||||
|
||||
|
@ -55,7 +58,7 @@ const PROVIDERS = [
|
|||
const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||
|
||||
export default class Autocompleter {
|
||||
constructor(room) {
|
||||
constructor(room: Room) {
|
||||
this.room = room;
|
||||
this.providers = PROVIDERS.map((p) => {
|
||||
return new p(room);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,104 +18,16 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t, _td } from '../languageHandler';
|
||||
import {_t} from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {TextualCompletion} from './Components';
|
||||
import type {SelectionRange} from './Autocompleter';
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
import {CommandMap} from '../SlashCommands';
|
||||
|
||||
// TODO merge this with the factory mechanics of SlashCommands?
|
||||
// Warning: Since the description string will be translated in _t(result.description), all these strings below must be in i18n/strings/en_EN.json file
|
||||
const COMMANDS = [
|
||||
{
|
||||
command: '/me',
|
||||
args: '<message>',
|
||||
description: _td('Displays action'),
|
||||
},
|
||||
{
|
||||
command: '/ban',
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Bans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/unban',
|
||||
args: '<user-id>',
|
||||
description: _td('Unbans user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/op',
|
||||
args: '<user-id> [<power-level>]',
|
||||
description: _td('Define the power level of a user'),
|
||||
},
|
||||
{
|
||||
command: '/deop',
|
||||
args: '<user-id>',
|
||||
description: _td('Deops user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/invite',
|
||||
args: '<user-id>',
|
||||
description: _td('Invites user with given id to current room'),
|
||||
},
|
||||
{
|
||||
command: '/join',
|
||||
args: '<room-alias>',
|
||||
description: _td('Joins room with given alias'),
|
||||
},
|
||||
{
|
||||
command: '/part',
|
||||
args: '[<room-alias>]',
|
||||
description: _td('Leave room'),
|
||||
},
|
||||
{
|
||||
command: '/topic',
|
||||
args: '<topic>',
|
||||
description: _td('Sets the room topic'),
|
||||
},
|
||||
{
|
||||
command: '/kick',
|
||||
args: '<user-id> [reason]',
|
||||
description: _td('Kicks user with given id'),
|
||||
},
|
||||
{
|
||||
command: '/nick',
|
||||
args: '<display-name>',
|
||||
description: _td('Changes your display nickname'),
|
||||
},
|
||||
{
|
||||
command: '/ddg',
|
||||
args: '<query>',
|
||||
description: _td('Searches DuckDuckGo for results'),
|
||||
},
|
||||
{
|
||||
command: '/tint',
|
||||
args: '<color1> [<color2>]',
|
||||
description: _td('Changes colour scheme of current room'),
|
||||
},
|
||||
{
|
||||
command: '/verify',
|
||||
args: '<user-id> <device-id> <device-signing-key>',
|
||||
description: _td('Verifies a user, device, and pubkey tuple'),
|
||||
},
|
||||
{
|
||||
command: '/ignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Ignores a user, hiding their messages from you'),
|
||||
},
|
||||
{
|
||||
command: '/unignore',
|
||||
args: '<user-id>',
|
||||
description: _td('Stops ignoring a user, showing their messages going forward'),
|
||||
},
|
||||
{
|
||||
command: '/devtools',
|
||||
args: '',
|
||||
description: _td('Opens the Developer Tools dialog'),
|
||||
},
|
||||
// Omitting `/markdown` as it only seems to apply to OldComposer
|
||||
];
|
||||
const COMMANDS = Object.values(CommandMap);
|
||||
|
||||
const COMMAND_RE = /(^\/\w*)/g;
|
||||
const COMMAND_RE = /(^\/\w*)(?: .*)?/g;
|
||||
|
||||
export default class CommandProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
|
@ -124,30 +37,37 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
let completions = [];
|
||||
if (!selection.beginning) return completions;
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (command) {
|
||||
let results;
|
||||
if (command[0] == '/') {
|
||||
results = COMMANDS;
|
||||
} else {
|
||||
results = this.matcher.match(command[0]);
|
||||
if (!command) return [];
|
||||
|
||||
let matches = [];
|
||||
// check if the full match differs from the first word (i.e. returns false if the command has args)
|
||||
if (command[0] !== command[1]) {
|
||||
// The input looks like a command with arguments, perform exact match
|
||||
const name = command[1].substr(1); // strip leading `/`
|
||||
if (CommandMap[name]) {
|
||||
matches = [CommandMap[name]];
|
||||
}
|
||||
} else {
|
||||
if (query === '/') {
|
||||
// If they have just entered `/` show everything
|
||||
matches = COMMANDS;
|
||||
} else {
|
||||
// otherwise fuzzy match against all of the fields
|
||||
matches = this.matcher.match(command[1]);
|
||||
}
|
||||
completions = results.map((result) => {
|
||||
return {
|
||||
completion: result.command + ' ',
|
||||
component: (<TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={_t(result.description)}
|
||||
/>),
|
||||
range,
|
||||
};
|
||||
});
|
||||
}
|
||||
return completions;
|
||||
|
||||
return matches.map((result) => ({
|
||||
// If the command is the same as the one they entered, we don't want to discard their arguments
|
||||
completion: result.command === command[1] ? command[0] : (result.command + ' '),
|
||||
component: <TextualCompletion
|
||||
title={result.command}
|
||||
subtitle={result.args}
|
||||
description={_t(result.description)} />,
|
||||
range,
|
||||
}));
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
|
111
src/autocomplete/CommunityProvider.js
Normal file
111
src/autocomplete/CommunityProvider.js
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {makeGroupPermalink} from "../matrix-to";
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
import FlairStore from "../stores/FlairStore";
|
||||
|
||||
const COMMUNITY_REGEX = /\B\+\S*/g;
|
||||
|
||||
function score(query, space) {
|
||||
const index = space.indexOf(query);
|
||||
if (index === -1) {
|
||||
return Infinity;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
export default class CommunityProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(COMMUNITY_REGEX);
|
||||
this.matcher = new FuzzyMatcher([], {
|
||||
keys: ['groupId', 'name', 'shortDescription'],
|
||||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
|
||||
// Disable autocompletions when composing commands because of various issues
|
||||
// (see https://github.com/vector-im/riot-web/issues/4762)
|
||||
if (/^(\/join|\/leave)/.test(query)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cli = MatrixClientPeg.get();
|
||||
let completions = [];
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
|
||||
|
||||
const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => {
|
||||
try {
|
||||
return FlairStore.getGroupProfileCached(cli, groupId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
return Promise.resolve({
|
||||
name: '',
|
||||
groupId,
|
||||
avatarUrl: '',
|
||||
shortDescription: '',
|
||||
});
|
||||
}
|
||||
})));
|
||||
|
||||
this.matcher.setObjects(groups);
|
||||
|
||||
const matchedString = command[0];
|
||||
completions = this.matcher.match(matchedString);
|
||||
completions = _sortBy(completions, [
|
||||
(c) => score(matchedString, c.groupId),
|
||||
(c) => c.groupId.length,
|
||||
]).map(({avatarUrl, groupId, name}) => ({
|
||||
completion: groupId,
|
||||
suffix: ' ',
|
||||
href: makeGroupPermalink(groupId),
|
||||
component: (
|
||||
<PillCompletion initialComponent={
|
||||
<BaseAvatar name={name || groupId}
|
||||
width={24} height={24}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 24, 24) : null} />
|
||||
} title={name} description={groupId} />
|
||||
),
|
||||
range,
|
||||
}))
|
||||
.slice(0, 4);
|
||||
}
|
||||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
return '💬 ' + _t('Communities');
|
||||
}
|
||||
|
||||
renderCompletions(completions: [React.Component]): ?React.Component {
|
||||
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
|
||||
{ completions }
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,7 +22,7 @@ import AutocompleteProvider from './AutocompleteProvider';
|
|||
import 'whatwg-fetch';
|
||||
|
||||
import {TextualCompletion} from './Components';
|
||||
import type {SelectionRange} from './Autocompleter';
|
||||
import type {SelectionRange} from "./Autocompleter";
|
||||
|
||||
const DDG_REGEX = /\/ddg\s+(.+)$/g;
|
||||
const REFERRER = 'vector';
|
||||
|
@ -37,7 +37,7 @@ export default class DuckDuckGoProvider extends AutocompleteProvider {
|
|||
+ `&format=json&no_redirect=1&no_html=1&t=${encodeURIComponent(REFERRER)}`;
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false) {
|
||||
const {command, range} = this.getCurrentCommand(query, selection);
|
||||
if (!query || !command) {
|
||||
return [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,11 +19,11 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import { _t } from '../languageHandler';
|
||||
import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {emojioneList, shortnameToImage, shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||
import {shortnameToUnicode, asciiRegexp, unicodeRegexp} from 'emojione';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import sdk from '../index';
|
||||
import {PillCompletion} from './Components';
|
||||
import type {SelectionRange, Completion} from './Autocompleter';
|
||||
import type {Completion, SelectionRange} from './Autocompleter';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
@ -95,7 +95,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange) {
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean): Array<Completion> {
|
||||
if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
|
||||
return []; // don't give any suggestions if the user doesn't want them
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { _t } from '../languageHandler';
|
|||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
import {PillCompletion} from './Components';
|
||||
import sdk from '../index';
|
||||
import type {SelectionRange} from './Autocompleter';
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
|
||||
const AT_ROOM_REGEX = /@\S*/g;
|
||||
|
||||
|
@ -30,7 +30,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
|||
this.room = room;
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
||||
async getCompletions(query: string, selection: SelectionRange, force?:boolean = false): Array<Completion> {
|
||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//@flow
|
||||
/*
|
||||
Copyright 2017 Aviral Dasgupta
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -27,6 +28,10 @@ class KeyMap {
|
|||
priorityMap = new Map();
|
||||
}
|
||||
|
||||
function stripDiacritics(str: string): string {
|
||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
||||
}
|
||||
|
||||
export default class QueryMatcher {
|
||||
/**
|
||||
* @param {object[]} objects the objects to perform a match on
|
||||
|
@ -46,10 +51,11 @@ export default class QueryMatcher {
|
|||
objects.forEach((object, i) => {
|
||||
const keyValues = _at(object, keys);
|
||||
for (const keyValue of keyValues) {
|
||||
if (!map.hasOwnProperty(keyValue)) {
|
||||
map[keyValue] = [];
|
||||
const key = stripDiacritics(keyValue).toLowerCase();
|
||||
if (!map.hasOwnProperty(key)) {
|
||||
map[key] = [];
|
||||
}
|
||||
map[keyValue].push(object);
|
||||
map[key].push(object);
|
||||
}
|
||||
keyMap.priorityMap.set(object, i);
|
||||
});
|
||||
|
@ -82,7 +88,7 @@ export default class QueryMatcher {
|
|||
}
|
||||
|
||||
match(query: String): Array<Object> {
|
||||
query = query.toLowerCase();
|
||||
query = stripDiacritics(query).toLowerCase();
|
||||
if (this.options.shouldMatchWordsOnly) {
|
||||
query = query.replace(/[^\w]/g, '');
|
||||
}
|
||||
|
@ -91,7 +97,7 @@ export default class QueryMatcher {
|
|||
}
|
||||
const results = [];
|
||||
this.keyMap.keys.forEach((key) => {
|
||||
let resultKey = key.toLowerCase();
|
||||
let resultKey = key;
|
||||
if (this.options.shouldMatchWordsOnly) {
|
||||
resultKey = resultKey.replace(/[^\w]/g, '');
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,9 +27,9 @@ import {getDisplayAliasForRoom} from '../Rooms';
|
|||
import sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {makeRoomPermalink} from "../matrix-to";
|
||||
import type {SelectionRange} from './Autocompleter';
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
|
||||
const ROOM_REGEX = /(?=#)(\S*)/g;
|
||||
const ROOM_REGEX = /\B#\S*/g;
|
||||
|
||||
function score(query, space) {
|
||||
const index = space.indexOf(query);
|
||||
|
@ -47,7 +48,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
|
||||
|
||||
const client = MatrixClientPeg.get();
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
/*
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -23,15 +24,14 @@ import AutocompleteProvider from './AutocompleteProvider';
|
|||
import {PillCompletion} from './Components';
|
||||
import sdk from '../index';
|
||||
import FuzzyMatcher from './FuzzyMatcher';
|
||||
import _pull from 'lodash/pull';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import MatrixClientPeg from '../MatrixClientPeg';
|
||||
|
||||
import type {Room, RoomMember} from 'matrix-js-sdk';
|
||||
import type {SelectionRange} from './Autocompleter';
|
||||
import type {MatrixEvent, Room, RoomMember, RoomState} from 'matrix-js-sdk';
|
||||
import {makeUserPermalink} from "../matrix-to";
|
||||
import type {Completion, SelectionRange} from "./Autocompleter";
|
||||
|
||||
const USER_REGEX = /@\S*/g;
|
||||
const USER_REGEX = /\B@\S*/g;
|
||||
|
||||
// used when you hit 'tab' - we allow some separator chars at the beginning
|
||||
// to allow you to tab-complete /mat into /(matthew)
|
||||
|
@ -47,7 +47,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
this.matcher = new FuzzyMatcher([], {
|
||||
keys: ['name', 'userId'],
|
||||
shouldMatchPrefix: true,
|
||||
shouldMatchWordsOnly: false
|
||||
shouldMatchWordsOnly: false,
|
||||
});
|
||||
|
||||
this._onRoomTimelineBound = this._onRoomTimeline.bind(this);
|
||||
|
@ -64,7 +64,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
}
|
||||
|
||||
_onRoomTimeline(ev, room, toStartOfTimeline, removed, data) {
|
||||
_onRoomTimeline(ev: MatrixEvent, room: Room, toStartOfTimeline: boolean, removed: boolean, data: Object) {
|
||||
if (!room) return;
|
||||
if (removed) return;
|
||||
if (room.roomId !== this.room.roomId) return;
|
||||
|
@ -80,7 +80,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
this.onUserSpoke(ev.sender);
|
||||
}
|
||||
|
||||
_onRoomStateMember(ev, state, member) {
|
||||
_onRoomStateMember(ev: MatrixEvent, state: RoomState, member: RoomMember) {
|
||||
// ignore members in other rooms
|
||||
if (member.roomId !== this.room.roomId) {
|
||||
return;
|
||||
|
@ -90,7 +90,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
this.users = null;
|
||||
}
|
||||
|
||||
async getCompletions(query: string, selection: SelectionRange, force = false) {
|
||||
async getCompletions(query: string, selection: SelectionRange, force?: boolean = false): Array<Completion> {
|
||||
const MemberAvatar = sdk.getComponent('views.avatars.MemberAvatar');
|
||||
|
||||
// lazy-load user list into matcher
|
||||
|
@ -126,7 +126,7 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
return completions;
|
||||
}
|
||||
|
||||
getName() {
|
||||
getName(): string {
|
||||
return '👥 ' + _t('Users');
|
||||
}
|
||||
|
||||
|
@ -139,13 +139,9 @@ export default class UserProvider extends AutocompleteProvider {
|
|||
}
|
||||
|
||||
const currentUserId = MatrixClientPeg.get().credentials.userId;
|
||||
this.users = this.room.getJoinedMembers().filter((member) => {
|
||||
if (member.userId !== currentUserId) return true;
|
||||
});
|
||||
this.users = this.room.getJoinedMembers().filter(({userId}) => userId !== currentUserId);
|
||||
|
||||
this.users = _sortBy(this.users, (member) =>
|
||||
1E20 - lastSpoken[member.userId] || 1E20,
|
||||
);
|
||||
this.users = _sortBy(this.users, (member) => 1E20 - lastSpoken[member.userId] || 1E20);
|
||||
|
||||
this.matcher.setObjects(this.users);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,12 +16,10 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
const classNames = require('classnames');
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
|
@ -61,6 +60,54 @@ export default class ContextualMenu extends React.Component {
|
|||
// If true, insert an invisible screen-sized element behind the
|
||||
// menu that when clicked will close it.
|
||||
hasBackground: PropTypes.bool,
|
||||
|
||||
// The component to render as the context menu
|
||||
elementClass: PropTypes.element.isRequired,
|
||||
// on resize callback
|
||||
windowResize: PropTypes.func,
|
||||
// method to close menu
|
||||
closeMenu: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
contextMenuRect: null,
|
||||
};
|
||||
|
||||
this.onContextMenu = this.onContextMenu.bind(this);
|
||||
this.collectContextMenuRect = this.collectContextMenuRect.bind(this);
|
||||
}
|
||||
|
||||
collectContextMenuRect(element) {
|
||||
// We don't need to clean up when unmounting, so ignore
|
||||
if (!element) return;
|
||||
|
||||
this.setState({
|
||||
contextMenuRect: element.getBoundingClientRect(),
|
||||
});
|
||||
}
|
||||
|
||||
onContextMenu(e) {
|
||||
if (this.props.closeMenu) {
|
||||
this.props.closeMenu();
|
||||
|
||||
e.preventDefault();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
|
||||
// XXX: This isn't pretty but the only way to allow opening a different context menu on right click whilst
|
||||
// a context menu and its click-guard are up without completely rewriting how the context menus work.
|
||||
setImmediate(() => {
|
||||
const clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent(
|
||||
'contextmenu', true, true, window, 0,
|
||||
0, 0, x, y, false, false,
|
||||
false, false, 0, null,
|
||||
);
|
||||
document.elementFromPoint(x, y).dispatchEvent(clickEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -83,6 +130,9 @@ export default class ContextualMenu extends React.Component {
|
|||
chevronFace = 'right';
|
||||
}
|
||||
|
||||
const contextMenuRect = this.state.contextMenuRect || null;
|
||||
const padding = 10;
|
||||
|
||||
const chevronOffset = {};
|
||||
if (props.chevronFace) {
|
||||
chevronFace = props.chevronFace;
|
||||
|
@ -90,7 +140,19 @@ export default class ContextualMenu extends React.Component {
|
|||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
chevronOffset.top = props.chevronOffset;
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
let adjusted = target;
|
||||
|
||||
// If we know the dimensions of the context menu, adjust its position
|
||||
// such that it does not leave the (padded) window.
|
||||
if (contextMenuRect) {
|
||||
adjusted = Math.min(position.top, document.body.clientHeight - contextMenuRect.height - padding);
|
||||
}
|
||||
|
||||
position.top = adjusted;
|
||||
chevronOffset.top = Math.max(props.chevronOffset, props.chevronOffset + target - adjusted);
|
||||
}
|
||||
|
||||
// To override the default chevron colour, if it's been set
|
||||
|
@ -112,7 +174,7 @@ export default class ContextualMenu extends React.Component {
|
|||
`;
|
||||
}
|
||||
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
|
||||
const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace} />;
|
||||
const className = 'mx_ContextualMenu_wrapper';
|
||||
|
||||
const menuClasses = classNames({
|
||||
|
@ -154,17 +216,17 @@ export default class ContextualMenu extends React.Component {
|
|||
// FIXME: If a menu uses getDefaultProps it clobbers the onFinished
|
||||
// property set here so you can't close the menu from a button click!
|
||||
return <div className={className} style={position}>
|
||||
<div className={menuClasses} style={menuStyle}>
|
||||
<div className={menuClasses} style={menuStyle} ref={this.collectContextMenuRect}>
|
||||
{ chevron }
|
||||
<ElementClass {...props} onFinished={props.closeMenu} onResize={props.windowResize} />
|
||||
</div>
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu}></div> }
|
||||
{ props.hasBackground && <div className="mx_ContextualMenu_background" onClick={props.closeMenu} onContextMenu={this.onContextMenu} /> }
|
||||
<style>{ chevronCSS }</style>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export function createMenu(ElementClass, props) {
|
||||
export function createMenu(ElementClass, props, hasBackground=true) {
|
||||
const closeMenu = function(...args) {
|
||||
ReactDOM.unmountComponentAtNode(getOrCreateContainer());
|
||||
|
||||
|
@ -175,8 +237,8 @@ export function createMenu(ElementClass, props) {
|
|||
|
||||
// We only reference closeMenu once per call to createMenu
|
||||
const menu = <ContextualMenu
|
||||
hasBackground={hasBackground}
|
||||
{...props}
|
||||
hasBackground={true}
|
||||
elementClass={ElementClass}
|
||||
closeMenu={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
windowResize={closeMenu} // eslint-disable-line react/jsx-no-bind
|
||||
|
|
|
@ -68,8 +68,8 @@ const FilePanel = React.createClass({
|
|||
"room": {
|
||||
"timeline": {
|
||||
"contains_url": true,
|
||||
"not_types": [
|
||||
"m.sticker",
|
||||
"types": [
|
||||
"m.room.message",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -432,11 +432,14 @@ export default React.createClass({
|
|||
|
||||
this._changeAvatarComponent = null;
|
||||
this._initGroupStore(this.props.groupId, true);
|
||||
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
dis.unregister(this._dispatcherRef);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(newProps) {
|
||||
|
@ -559,16 +562,33 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onShareClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
|
||||
target: this._matrixClient.getGroup(this.props.groupId),
|
||||
});
|
||||
},
|
||||
|
||||
_onCancelClick: function() {
|
||||
this._closeSettings();
|
||||
},
|
||||
|
||||
_onAction(payload) {
|
||||
switch (payload.action) {
|
||||
// NOTE: close_settings is an app-wide dispatch; as it is dispatched from MatrixChat
|
||||
case 'close_settings':
|
||||
this.setState({
|
||||
editing: false,
|
||||
profileForm: null,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
_closeSettings() {
|
||||
this.setState({
|
||||
editing: false,
|
||||
profileForm: null,
|
||||
});
|
||||
dis.dispatch({action: 'panel_disable'});
|
||||
dis.dispatch({action: 'close_settings'});
|
||||
},
|
||||
|
||||
_onNameChange: function(value) {
|
||||
|
@ -1039,7 +1059,7 @@ export default React.createClass({
|
|||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_INVITE}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_INVITE}
|
||||
onClick={this._onJoinableChange}
|
||||
onChange={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Only people who have been invited') }
|
||||
|
@ -1051,7 +1071,7 @@ export default React.createClass({
|
|||
<input type="radio"
|
||||
value={GROUP_JOINPOLICY_OPEN}
|
||||
checked={this.state.joinableForm.policyType === GROUP_JOINPOLICY_OPEN}
|
||||
onClick={this._onJoinableChange}
|
||||
onChange={this._onJoinableChange}
|
||||
/>
|
||||
<div className="mx_GroupView_label_text">
|
||||
{ _t('Everyone') }
|
||||
|
@ -1114,10 +1134,6 @@ export default React.createClass({
|
|||
let avatarNode;
|
||||
let nameNode;
|
||||
let shortDescNode;
|
||||
const bodyNodes = [
|
||||
this._getMembershipSection(),
|
||||
this._getGroupSection(),
|
||||
];
|
||||
const rightButtons = [];
|
||||
if (this.state.editing && this.state.isUserPrivileged) {
|
||||
let avatarImage;
|
||||
|
@ -1194,6 +1210,7 @@ export default React.createClass({
|
|||
shortDescNode = <span onClick={onGroupHeaderItemClick}>{ summary.profile.short_description }</span>;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.editing) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
|
||||
|
@ -1218,6 +1235,11 @@ export default React.createClass({
|
|||
</AccessibleButton>,
|
||||
);
|
||||
}
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button" onClick={this._onShareClick} title={_t('Share Community')} key="_shareButton">
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>,
|
||||
);
|
||||
if (this.props.collapsedRhs) {
|
||||
rightButtons.push(
|
||||
<AccessibleButton className="mx_GroupHeader_button"
|
||||
|
@ -1256,7 +1278,8 @@ export default React.createClass({
|
|||
</div>
|
||||
</div>
|
||||
<GeminiScrollbarWrapper className="mx_GroupView_body">
|
||||
{ bodyNodes }
|
||||
{ this._getMembershipSection() }
|
||||
{ this._getGroupSection() }
|
||||
</GeminiScrollbarWrapper>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -82,17 +82,26 @@ var LeftPanel = React.createClass({
|
|||
|
||||
_onKeyDown: function(ev) {
|
||||
if (!this.focusedElement) return;
|
||||
let handled = false;
|
||||
let handled = true;
|
||||
|
||||
switch (ev.keyCode) {
|
||||
case KeyCode.TAB:
|
||||
this._onMoveFocus(ev.shiftKey);
|
||||
break;
|
||||
case KeyCode.UP:
|
||||
this._onMoveFocus(true);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyCode.DOWN:
|
||||
this._onMoveFocus(false);
|
||||
handled = true;
|
||||
break;
|
||||
case KeyCode.ENTER:
|
||||
this._onMoveFocus(false);
|
||||
if (this.focusedElement) {
|
||||
this.focusedElement.click();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
|
@ -102,37 +111,33 @@ var LeftPanel = React.createClass({
|
|||
},
|
||||
|
||||
_onMoveFocus: function(up) {
|
||||
var element = this.focusedElement;
|
||||
let element = this.focusedElement;
|
||||
|
||||
// unclear why this isn't needed
|
||||
// var descending = (up == this.focusDirection) ? this.focusDescending : !this.focusDescending;
|
||||
// this.focusDirection = up;
|
||||
|
||||
var descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
var classes;
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes;
|
||||
|
||||
do {
|
||||
var child = up ? element.lastElementChild : element.firstElementChild;
|
||||
var sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
}
|
||||
else if (sibling) {
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
@ -144,8 +149,7 @@ var LeftPanel = React.createClass({
|
|||
descending = true;
|
||||
}
|
||||
}
|
||||
|
||||
} while(element && !(
|
||||
} while (element && !(
|
||||
classes.contains("mx_RoomTile") ||
|
||||
classes.contains("mx_SearchBox_search") ||
|
||||
classes.contains("mx_RoomSubList_ellipsis")));
|
||||
|
|
|
@ -255,6 +255,22 @@ const LoggedInView = React.createClass({
|
|||
), true);
|
||||
},
|
||||
|
||||
_onClick: function(ev) {
|
||||
// When the panels are disabled, clicking on them results in a mouse event
|
||||
// which bubbles to certain elements in the tree. When this happens, close
|
||||
// any settings page that is currently open (user/room/group).
|
||||
if (this.props.leftDisabled &&
|
||||
this.props.rightDisabled &&
|
||||
(
|
||||
ev.target.className === 'mx_MatrixChat' ||
|
||||
ev.target.className === 'mx_MatrixChat_middlePanel' ||
|
||||
ev.target.className === 'mx_RoomView'
|
||||
)
|
||||
) {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const LeftPanel = sdk.getComponent('structures.LeftPanel');
|
||||
const RightPanel = sdk.getComponent('structures.RightPanel');
|
||||
|
@ -295,7 +311,7 @@ const LoggedInView = React.createClass({
|
|||
|
||||
case PageTypes.UserSettings:
|
||||
page_element = <UserSettings
|
||||
onClose={this.props.onUserSettingsClose}
|
||||
onClose={this.props.onCloseAllSettings}
|
||||
brand={this.props.config.brand}
|
||||
referralBaseUrl={this.props.config.referralBaseUrl}
|
||||
teamToken={this.props.teamToken}
|
||||
|
@ -380,7 +396,7 @@ const LoggedInView = React.createClass({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers}>
|
||||
<div className='mx_MatrixChat_wrapper' aria-hidden={this.props.hideToSRUsers} onClick={this._onClick}>
|
||||
{ topBar }
|
||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
||||
<div className={bodyClasses}>
|
||||
|
|
|
@ -23,6 +23,7 @@ import PropTypes from 'prop-types';
|
|||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import MatrixClientPeg from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
|
@ -398,6 +399,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
startPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
||||
// This shouldn't happen because componentWillUpdate and componentDidUpdate
|
||||
// are used.
|
||||
if (this._pageChanging) {
|
||||
|
@ -409,6 +413,9 @@ export default React.createClass({
|
|||
},
|
||||
|
||||
stopPageChangeTimer() {
|
||||
// Tor doesn't support performance
|
||||
if (!performance || !performance.mark) return null;
|
||||
|
||||
if (!this._pageChanging) {
|
||||
console.warn('MatrixChat.stopPageChangeTimer: timer not started');
|
||||
return;
|
||||
|
@ -560,6 +567,27 @@ export default React.createClass({
|
|||
this._setPage(PageTypes.UserSettings);
|
||||
this.notifyNewScreen('settings');
|
||||
break;
|
||||
case 'close_settings':
|
||||
this.setState({
|
||||
leftDisabled: false,
|
||||
rightDisabled: false,
|
||||
middleDisabled: false,
|
||||
});
|
||||
if (this.state.page_type === PageTypes.UserSettings) {
|
||||
// We do this to get setPage and notifyNewScreen
|
||||
if (this.state.currentRoomId) {
|
||||
this._viewRoom({
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
} else if (this.state.currentGroupId) {
|
||||
this._viewGroup({
|
||||
group_id: this.state.currentGroupId,
|
||||
});
|
||||
} else {
|
||||
this._viewHome();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'view_create_room':
|
||||
this._createRoom();
|
||||
break;
|
||||
|
@ -577,19 +605,10 @@ export default React.createClass({
|
|||
this.notifyNewScreen('groups');
|
||||
break;
|
||||
case 'view_group':
|
||||
{
|
||||
const groupId = payload.group_id;
|
||||
this.setState({
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
}
|
||||
this._viewGroup(payload);
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
this._viewHome();
|
||||
break;
|
||||
case 'view_set_mxid':
|
||||
this._setMxId(payload);
|
||||
|
@ -632,7 +651,8 @@ export default React.createClass({
|
|||
middleDisabled: payload.middleDisabled || false,
|
||||
rightDisabled: payload.rightDisabled || payload.sideDisabled || false,
|
||||
});
|
||||
break; }
|
||||
break;
|
||||
}
|
||||
case 'set_theme':
|
||||
this._onSetTheme(payload.value);
|
||||
break;
|
||||
|
@ -781,7 +801,6 @@ export default React.createClass({
|
|||
// @param {string=} roomInfo.room_id ID of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {string=} roomInfo.room_alias Alias of the room to join. One of room_id or room_alias must be given.
|
||||
// @param {boolean=} roomInfo.auto_join If true, automatically attempt to join the room if not already a member.
|
||||
// @param {boolean=} roomInfo.show_settings Makes RoomView show the room settings dialog.
|
||||
// @param {string=} roomInfo.event_id ID of the event in this room to show: this will cause a switch to the
|
||||
// context of that particular event.
|
||||
// @param {boolean=} roomInfo.highlighted If true, add event_id to the hash of the URL
|
||||
|
@ -848,6 +867,21 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_viewGroup: function(payload) {
|
||||
const groupId = payload.group_id;
|
||||
this.setState({
|
||||
currentGroupId: groupId,
|
||||
currentGroupIsNew: payload.group_is_new,
|
||||
});
|
||||
this._setPage(PageTypes.GroupView);
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
},
|
||||
|
||||
_viewHome: function() {
|
||||
this._setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
},
|
||||
|
||||
_setMxId: function(payload) {
|
||||
const SetMxIdDialog = sdk.getComponent('views.dialogs.SetMxIdDialog');
|
||||
const close = Modal.createTrackedDialog('Set MXID', '', SetMxIdDialog, {
|
||||
|
@ -957,6 +991,7 @@ export default React.createClass({
|
|||
if (rule !== "public") {
|
||||
warnings.push((
|
||||
<span className="warning" key="non_public_warning">
|
||||
{' '/* Whitespace, otherwise the sentences get smashed together */ }
|
||||
{ _t("This room is not public. You will not be able to rejoin without an invite.") }
|
||||
</span>
|
||||
));
|
||||
|
@ -995,10 +1030,20 @@ export default React.createClass({
|
|||
}, (err) => {
|
||||
modal.close();
|
||||
console.error("Failed to leave room " + roomId + " " + err);
|
||||
let title = _t("Failed to leave room");
|
||||
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
|
||||
if (err.errcode == 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
|
||||
title = _t("Can't leave Server Notices room");
|
||||
message = _t(
|
||||
"This room is used for important messages from the Homeserver, " +
|
||||
"so you cannot leave it.",
|
||||
);
|
||||
} else if (err && err.message) {
|
||||
message = err.message;
|
||||
}
|
||||
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
|
||||
title: _t("Failed to leave room"),
|
||||
description: (err && err.message ? err.message :
|
||||
_t("Server may be unavailable, overloaded, or you hit a bug.")),
|
||||
title: title,
|
||||
description: message,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1099,11 +1144,6 @@ export default React.createClass({
|
|||
} else if (this._is_registered) {
|
||||
this._is_registered = false;
|
||||
|
||||
// Set the display name = user ID localpart
|
||||
MatrixClientPeg.get().setDisplayName(
|
||||
MatrixClientPeg.get().getUserIdLocalpart(),
|
||||
);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
createRoom({
|
||||
dmUserId: this.props.config.welcomeUserId,
|
||||
|
@ -1231,6 +1271,28 @@ export default React.createClass({
|
|||
action: 'logout',
|
||||
});
|
||||
});
|
||||
cli.on('no_consent', function(message, consentUri) {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('No Consent Dialog', '', QuestionDialog, {
|
||||
title: _t('Terms and Conditions'),
|
||||
description: <div>
|
||||
<p> { _t(
|
||||
'To continue using the %(homeserverDomain)s homeserver ' +
|
||||
'you must review and agree to our terms and conditions.',
|
||||
{ homeserverDomain: cli.getDomain() },
|
||||
) }
|
||||
</p>
|
||||
</div>,
|
||||
button: _t('Review terms and conditions'),
|
||||
cancelButton: _t('Dismiss'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
window.open(consentUri, '_blank');
|
||||
}
|
||||
},
|
||||
}, null, true);
|
||||
});
|
||||
|
||||
cli.on("accountData", function(ev) {
|
||||
if (ev.getType() === 'im.vector.web.settings') {
|
||||
if (ev.getContent() && ev.getContent().theme) {
|
||||
|
@ -1242,6 +1304,32 @@ export default React.createClass({
|
|||
}
|
||||
});
|
||||
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
switch (errorCode) {
|
||||
case 'MEGOLM_UNKNOWN_INBOUND_SESSION_ID':
|
||||
return 'olm_keys_not_sent_error';
|
||||
case 'OLM_UNKNOWN_MESSAGE_INDEX':
|
||||
return 'olm_index_error';
|
||||
case undefined:
|
||||
return 'unexpected_error';
|
||||
default:
|
||||
return 'unspecified_error';
|
||||
}
|
||||
});
|
||||
|
||||
// Shelved for later date when we have time to think about persisting history of
|
||||
// tracked events across sessions.
|
||||
// dft.loadTrackedEventHashMap();
|
||||
|
||||
dft.start();
|
||||
|
||||
// When logging out, stop tracking failures and destroy state
|
||||
cli.on("Session.logged_out", () => dft.stop());
|
||||
cli.on("Event.decrypted", (e, err) => dft.eventDecrypted(e, err));
|
||||
|
||||
const krh = new KeyRequestHandler(cli);
|
||||
cli.on("crypto.roomKeyRequest", (req) => {
|
||||
krh.handleKeyRequest(req);
|
||||
|
@ -1573,19 +1661,8 @@ export default React.createClass({
|
|||
this._setPageSubtitle(subtitle);
|
||||
},
|
||||
|
||||
onUserSettingsClose: function() {
|
||||
// XXX: use browser history instead to find the previous room?
|
||||
// or maintain a this.state.pageHistory in _setPage()?
|
||||
if (this.state.currentRoomId) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.currentRoomId,
|
||||
});
|
||||
} else {
|
||||
dis.dispatch({
|
||||
action: 'view_home_page',
|
||||
});
|
||||
}
|
||||
onCloseAllSettings() {
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
},
|
||||
|
||||
onServerConfigChange(config) {
|
||||
|
@ -1644,7 +1721,7 @@ export default React.createClass({
|
|||
return (
|
||||
<LoggedInView ref={this._collectLoggedInView} matrixClient={MatrixClientPeg.get()}
|
||||
onRoomCreated={this.onRoomCreated}
|
||||
onUserSettingsClose={this.onUserSettingsClose}
|
||||
onCloseAllSettings={this.onCloseAllSettings}
|
||||
onRegistered={this.onRegistered}
|
||||
currentRoomId={this.state.currentRoomId}
|
||||
teamToken={this._teamToken}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -25,6 +26,9 @@ import sdk from '../../index';
|
|||
|
||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||
*/
|
||||
module.exports = React.createClass({
|
||||
|
@ -189,7 +193,7 @@ module.exports = React.createClass({
|
|||
/**
|
||||
* Page up/down.
|
||||
*
|
||||
* mult: -1 to page up, +1 to page down
|
||||
* @param {number} mult: -1 to page up, +1 to page down
|
||||
*/
|
||||
scrollRelative: function(mult) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -199,6 +203,8 @@ module.exports = React.createClass({
|
|||
|
||||
/**
|
||||
* Scroll up/down in response to a scroll key
|
||||
*
|
||||
* @param {KeyboardEvent} ev: the keyboard event to handle
|
||||
*/
|
||||
handleScrollKey: function(ev) {
|
||||
if (this.refs.scrollPanel) {
|
||||
|
@ -257,6 +263,7 @@ module.exports = React.createClass({
|
|||
|
||||
this.eventNodes = {};
|
||||
|
||||
let visible = false;
|
||||
let i;
|
||||
|
||||
// first figure out which is the last event in the list which we're
|
||||
|
@ -297,7 +304,7 @@ module.exports = React.createClass({
|
|||
// if the readmarker has moved, cancel any active ghost.
|
||||
if (this.currentReadMarkerEventId && this.props.readMarkerEventId &&
|
||||
this.props.readMarkerVisible &&
|
||||
this.currentReadMarkerEventId != this.props.readMarkerEventId) {
|
||||
this.currentReadMarkerEventId !== this.props.readMarkerEventId) {
|
||||
this.currentGhostEventId = null;
|
||||
}
|
||||
|
||||
|
@ -404,8 +411,8 @@ module.exports = React.createClass({
|
|||
|
||||
let isVisibleReadMarker = false;
|
||||
|
||||
if (eventId == this.props.readMarkerEventId) {
|
||||
var visible = this.props.readMarkerVisible;
|
||||
if (eventId === this.props.readMarkerEventId) {
|
||||
visible = this.props.readMarkerVisible;
|
||||
|
||||
// if the read marker comes at the end of the timeline (except
|
||||
// for local echoes, which are excluded from RMs, because they
|
||||
|
@ -423,11 +430,11 @@ module.exports = React.createClass({
|
|||
|
||||
// XXX: there should be no need for a ghost tile - we should just use a
|
||||
// a dispatch (user_activity_end) to start the RM animation.
|
||||
if (eventId == this.currentGhostEventId) {
|
||||
if (eventId === this.currentGhostEventId) {
|
||||
// if we're showing an animation, continue to show it.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
} else if (!isVisibleReadMarker &&
|
||||
eventId == this.currentReadMarkerEventId) {
|
||||
eventId === this.currentReadMarkerEventId) {
|
||||
// there is currently a read-up-to marker at this point, but no
|
||||
// more. Show an animation of it disappearing.
|
||||
ret.push(this._getReadMarkerGhostTile());
|
||||
|
@ -449,16 +456,17 @@ module.exports = React.createClass({
|
|||
|
||||
// Some events should appear as continuations from previous events of
|
||||
// different types.
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
||||
const eventTypeContinues =
|
||||
prevEvent !== null &&
|
||||
continuedTypes.includes(mxEv.getType()) &&
|
||||
continuedTypes.includes(prevEvent.getType());
|
||||
|
||||
if (prevEvent !== null
|
||||
&& prevEvent.sender && mxEv.sender
|
||||
&& mxEv.sender.userId === prevEvent.sender.userId
|
||||
&& (mxEv.getType() == prevEvent.getType() || eventTypeContinues)) {
|
||||
// if there is a previous event and it has the same sender as this event
|
||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||
if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId &&
|
||||
(mxEv.getType() === prevEvent.getType() || eventTypeContinues) &&
|
||||
(mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) {
|
||||
continuation = true;
|
||||
}
|
||||
|
||||
|
@ -493,7 +501,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
const eventId = mxEv.getId();
|
||||
const highlight = (eventId == this.props.highlightedEventId);
|
||||
const highlight = (eventId === this.props.highlightedEventId);
|
||||
|
||||
// we can't use local echoes as scroll tokens, because their event IDs change.
|
||||
// Local echos have a send "status".
|
||||
|
@ -632,7 +640,8 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
let topSpinner, bottomSpinner;
|
||||
let topSpinner;
|
||||
let bottomSpinner;
|
||||
if (this.props.backPaginating) {
|
||||
topSpinner = <li key="_topSpinner"><Spinner /></li>;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ export default withMatrixClient(React.createClass({
|
|||
if (this.state.groups) {
|
||||
const groupNodes = [];
|
||||
this.state.groups.forEach((g) => {
|
||||
groupNodes.push(<GroupTile groupId={g} />);
|
||||
groupNodes.push(<GroupTile key={g} groupId={g} />);
|
||||
});
|
||||
contentHeader = groupNodes.length > 0 ? <h3>{ _t('Your Communities') }</h3> : <div />;
|
||||
content = groupNodes.length > 0 ?
|
||||
|
@ -124,7 +124,7 @@ export default withMatrixClient(React.createClass({
|
|||
) }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
|
||||
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
|
||||
<TintableSvg src="img/icons-create-room.svg" width="50" height="50" />
|
||||
</AccessibleButton>
|
||||
|
@ -140,7 +140,7 @@ export default withMatrixClient(React.createClass({
|
|||
{ 'i': (sub) => <i>{ sub }</i> })
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>*/}
|
||||
</div>
|
||||
<div className="mx_MyGroups_content">
|
||||
{ contentHeader }
|
||||
|
|
|
@ -25,6 +25,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
|
|||
import MemberAvatar from '../views/avatars/MemberAvatar';
|
||||
import Resend from '../../Resend';
|
||||
import * as cryptodevices from '../../cryptodevices';
|
||||
import dis from '../../dispatcher';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -157,10 +158,12 @@ module.exports = React.createClass({
|
|||
|
||||
_onResendAllClick: function() {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onCancelAllClick: function() {
|
||||
Resend.cancelUnsentEvents(this.props.room);
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
},
|
||||
|
||||
_onShowDevicesClick: function() {
|
||||
|
@ -305,7 +308,26 @@ module.exports = React.createClass({
|
|||
},
|
||||
);
|
||||
} else {
|
||||
if (
|
||||
let consentError = null;
|
||||
for (const m of unsentMessages) {
|
||||
if (m.error && m.error.errcode === 'M_CONSENT_NOT_GIVEN') {
|
||||
consentError = m.error;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (consentError) {
|
||||
title = _t(
|
||||
"You can't send any messages until you review and agree to " +
|
||||
"<consentLink>our terms and conditions</consentLink>.",
|
||||
{},
|
||||
{
|
||||
'consentLink': (sub) =>
|
||||
<a href={consentError.data && consentError.data.consent_uri} target="_blank">
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else if (
|
||||
unsentMessages.length === 1 &&
|
||||
unsentMessages[0].error &&
|
||||
unsentMessages[0].error.data &&
|
||||
|
@ -329,11 +351,13 @@ module.exports = React.createClass({
|
|||
|
||||
return <div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title={_t("Warning")} alt={_t("Warning")} />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ title }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ content }
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -350,11 +374,13 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_RoomStatusBar_connectionLostBar">
|
||||
<img src="img/warning.svg" width="24" height="23" title="/!\ " alt="/!\ " />
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
<div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_title">
|
||||
{ _t('Connectivity to the server has been lost.') }
|
||||
</div>
|
||||
<div className="mx_RoomStatusBar_connectionLostBar_desc">
|
||||
{ _t('Sent messages will be stored until your connection has returned.') }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,56 +16,53 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var classNames = require('classnames');
|
||||
var sdk = require('../../index');
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import sdk from '../../index';
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import { _t } from '../../languageHandler';
|
||||
var dis = require('../../dispatcher');
|
||||
var Unread = require('../../Unread');
|
||||
var MatrixClientPeg = require('../../MatrixClientPeg');
|
||||
var RoomNotifs = require('../../RoomNotifs');
|
||||
var FormattingUtils = require('../../utils/FormattingUtils');
|
||||
var AccessibleButton = require('../../components/views/elements/AccessibleButton');
|
||||
import Modal from '../../Modal';
|
||||
import dis from '../../dispatcher';
|
||||
import Unread from '../../Unread';
|
||||
import * as RoomNotifs from '../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../utils/FormattingUtils';
|
||||
import { KeyCode } from '../../Keyboard';
|
||||
import { Group } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
|
||||
// turn this on for drop & drag console debugging galore
|
||||
var debug = false;
|
||||
const debug = false;
|
||||
|
||||
const TRUNCATE_AT = 10;
|
||||
|
||||
var RoomSubList = React.createClass({
|
||||
const RoomSubList = React.createClass({
|
||||
displayName: 'RoomSubList',
|
||||
|
||||
debug: debug,
|
||||
|
||||
propTypes: {
|
||||
list: React.PropTypes.arrayOf(React.PropTypes.object).isRequired,
|
||||
label: React.PropTypes.string.isRequired,
|
||||
tagName: React.PropTypes.string,
|
||||
editable: React.PropTypes.bool,
|
||||
list: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
tagName: PropTypes.string,
|
||||
editable: PropTypes.bool,
|
||||
|
||||
order: React.PropTypes.string.isRequired,
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: React.PropTypes.bool,
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
startAsHidden: React.PropTypes.bool,
|
||||
showSpinner: React.PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: React.PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: React.PropTypes.func,
|
||||
alwaysShowHeader: React.PropTypes.bool,
|
||||
incomingCall: React.PropTypes.object,
|
||||
onShowMoreRooms: React.PropTypes.func,
|
||||
searchFilter: React.PropTypes.string,
|
||||
emptyContent: React.PropTypes.node, // content shown if the list is empty
|
||||
headerItems: React.PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: React.PropTypes.arrayOf(React.PropTypes.node), // extra elements added beneath tiles
|
||||
startAsHidden: PropTypes.bool,
|
||||
showSpinner: PropTypes.bool, // true to show a spinner if 0 elements when expanded
|
||||
collapsed: PropTypes.bool.isRequired, // is LeftPanel collapsed?
|
||||
onHeaderClick: PropTypes.func,
|
||||
alwaysShowHeader: PropTypes.bool,
|
||||
incomingCall: PropTypes.object,
|
||||
onShowMoreRooms: PropTypes.func,
|
||||
searchFilter: PropTypes.string,
|
||||
emptyContent: PropTypes.node, // content shown if the list is empty
|
||||
headerItems: PropTypes.node, // content shown in the sublist header
|
||||
extraTiles: PropTypes.arrayOf(PropTypes.node), // extra elements added beneath tiles
|
||||
showEmpty: PropTypes.bool,
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -77,10 +75,13 @@ var RoomSubList = React.createClass({
|
|||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
onHeaderClick: function() {}, // NOP
|
||||
onShowMoreRooms: function() {}, // NOP
|
||||
onHeaderClick: function() {
|
||||
}, // NOP
|
||||
onShowMoreRooms: function() {
|
||||
}, // NOP
|
||||
extraTiles: [],
|
||||
isInvite: false,
|
||||
showEmpty: true,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -105,15 +106,17 @@ var RoomSubList = React.createClass({
|
|||
|
||||
applySearchFilter: function(list, filter) {
|
||||
if (filter === "") return list;
|
||||
return list.filter((room) => {
|
||||
return room.name && room.name.toLowerCase().indexOf(filter.toLowerCase()) >= 0
|
||||
});
|
||||
const lcFilter = filter.toLowerCase();
|
||||
// case insensitive if room name includes filter,
|
||||
// or if starts with `#` and one of room's aliases starts with filter
|
||||
return list.filter((room) => (room.name && room.name.toLowerCase().includes(lcFilter)) ||
|
||||
(filter[0] === '#' && room.getAliases().some((alias) => alias.toLowerCase().startsWith(lcFilter))));
|
||||
},
|
||||
|
||||
// The header is collapsable if it is hidden or not stuck
|
||||
// The dataset elements are added in the RoomList _initAndPositionStickyHeaders method
|
||||
isCollapsableOnClick: function() {
|
||||
var stuck = this.refs.header.dataset.stuck;
|
||||
const stuck = this.refs.header.dataset.stuck;
|
||||
if (this.state.hidden || stuck === undefined || stuck === "none") {
|
||||
return true;
|
||||
} else {
|
||||
|
@ -139,12 +142,12 @@ var RoomSubList = React.createClass({
|
|||
onClick: function(ev) {
|
||||
if (this.isCollapsableOnClick()) {
|
||||
// The header isCollapsable, so the click is to be interpreted as collapse and truncation logic
|
||||
var isHidden = !this.state.hidden;
|
||||
this.setState({ hidden : isHidden });
|
||||
const isHidden = !this.state.hidden;
|
||||
this.setState({hidden: isHidden});
|
||||
|
||||
if (isHidden) {
|
||||
// as good a way as any to reset the truncate state
|
||||
this.setState({ truncateAt : TRUNCATE_AT });
|
||||
this.setState({truncateAt: TRUNCATE_AT});
|
||||
}
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -159,7 +162,7 @@ var RoomSubList = React.createClass({
|
|||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
clear_search: (ev && (ev.keyCode == KeyCode.ENTER || ev.keyCode == KeyCode.SPACE)),
|
||||
clear_search: (ev && (ev.keyCode === KeyCode.ENTER || ev.keyCode === KeyCode.SPACE)),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -169,17 +172,17 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function(roomNotifState) {
|
||||
return roomNotifState != RoomNotifs.MUTE;
|
||||
return roomNotifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Total up all the notification counts from the rooms
|
||||
*
|
||||
* @param {Number} If supplied will only total notifications for rooms outside the truncation number
|
||||
* @param {Number} truncateAt If supplied will only total notifications for rooms outside the truncation number
|
||||
* @returns {Array} The array takes the form [total, highlight] where highlight is a bool
|
||||
*/
|
||||
roomNotificationCount: function(truncateAt) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
|
||||
if (this.props.isInvite) {
|
||||
return [0, true];
|
||||
|
@ -187,9 +190,9 @@ var RoomSubList = React.createClass({
|
|||
|
||||
return this.props.list.reduce(function(result, room, index) {
|
||||
if (truncateAt === undefined || index >= truncateAt) {
|
||||
var roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
var highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
var notificationCount = room.getUnreadNotificationCount();
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && self._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && self._shouldShowMentionBadge(roomNotifState);
|
||||
|
@ -238,38 +241,83 @@ var RoomSubList = React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
_onNotifBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// find first room which has notifications and switch to it
|
||||
for (const room of this.state.sortedList) {
|
||||
const roomNotifState = RoomNotifs.getRoomNotifsState(room.roomId);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
||||
const notificationCount = room.getUnreadNotificationCount();
|
||||
|
||||
const notifBadges = notificationCount > 0 && this._shouldShowNotifBadge(roomNotifState);
|
||||
const mentionBadges = highlight && this._shouldShowMentionBadge(roomNotifState);
|
||||
|
||||
if (notifBadges || mentionBadges) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: room.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_onInviteBadgeClick: function(e) {
|
||||
// prevent the roomsublist collapsing
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
// switch to first room in sortedList as that'll be the top of the list for the user
|
||||
if (this.state.sortedList && this.state.sortedList.length > 0) {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: this.state.sortedList[0].roomId,
|
||||
});
|
||||
} else if (this.props.extraTiles && this.props.extraTiles.length > 0) {
|
||||
// Group Invites are different in that they are all extra tiles and not rooms
|
||||
// XXX: this is a horrible special case because Group Invite sublist is a hack
|
||||
if (this.props.extraTiles[0].props && this.props.extraTiles[0].props.group instanceof Group) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
group_id: this.props.extraTiles[0].props.group.groupId,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_getHeaderJsx: function() {
|
||||
var TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
const subListNotifications = this.roomNotificationCount();
|
||||
const subListNotifCount = subListNotifications[0];
|
||||
const subListNotifHighlight = subListNotifications[1];
|
||||
|
||||
var subListNotifications = this.roomNotificationCount();
|
||||
var subListNotifCount = subListNotifications[0];
|
||||
var subListNotifHighlight = subListNotifications[1];
|
||||
const totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
const roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var totalTiles = this.props.list.length + (this.props.extraTiles || []).length;
|
||||
var roomCount = totalTiles > 0 ? totalTiles : '';
|
||||
|
||||
var chevronClasses = classNames({
|
||||
const chevronClasses = classNames({
|
||||
'mx_RoomSubList_chevron': true,
|
||||
'mx_RoomSubList_chevronRight': this.state.hidden,
|
||||
'mx_RoomSubList_chevronDown': !this.state.hidden,
|
||||
});
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_badge': true,
|
||||
'mx_RoomSubList_badgeHighlight': subListNotifHighlight,
|
||||
});
|
||||
|
||||
var badge;
|
||||
let badge;
|
||||
if (subListNotifCount > 0) {
|
||||
badge = <div className={badgeClasses}>{ FormattingUtils.formatCount(subListNotifCount) }</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onNotifBadgeClick}>
|
||||
{ FormattingUtils.formatCount(subListNotifCount) }
|
||||
</div>;
|
||||
} else if (this.props.isInvite) {
|
||||
// no notifications but highlight anyway because this is an invite badge
|
||||
badge = <div className={badgeClasses}>!</div>;
|
||||
badge = <div className={badgeClasses} onClick={this._onInviteBadgeClick}>!</div>;
|
||||
}
|
||||
|
||||
// When collapsed, allow a long hover on the header to show user
|
||||
// the full tag name and room count
|
||||
var title;
|
||||
let title;
|
||||
if (this.props.collapsed) {
|
||||
title = this.props.label;
|
||||
if (roomCount !== '') {
|
||||
|
@ -277,63 +325,66 @@ var RoomSubList = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
var incomingCall;
|
||||
let incomingCall;
|
||||
if (this.props.incomingCall) {
|
||||
var self = this;
|
||||
const self = this;
|
||||
// Check if the incoming call is for this section
|
||||
var incomingCallRoom = this.props.list.filter(function(room) {
|
||||
const incomingCallRoom = this.props.list.filter(function(room) {
|
||||
return self.props.incomingCall.roomId === room.roomId;
|
||||
});
|
||||
|
||||
if (incomingCallRoom.length === 1) {
|
||||
var IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall = <IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={ this.props.incomingCall }/>;
|
||||
const IncomingCallBox = sdk.getComponent("voip.IncomingCallBox");
|
||||
incomingCall =
|
||||
<IncomingCallBox className="mx_RoomSubList_incomingCall" incomingCall={this.props.incomingCall} />;
|
||||
}
|
||||
}
|
||||
|
||||
var tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
const tabindex = this.props.searchFilter === "" ? "0" : "-1";
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<div className="mx_RoomSubList_labelContainer" title={ title } ref="header">
|
||||
<AccessibleButton onClick={ this.onClick } className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{ this.props.collapsed ? '' : this.props.label }
|
||||
<div className="mx_RoomSubList_roomCount">{ roomCount }</div>
|
||||
<div className={chevronClasses}></div>
|
||||
{ badge }
|
||||
{ incomingCall }
|
||||
<div className="mx_RoomSubList_labelContainer" title={title} ref="header">
|
||||
<AccessibleButton onClick={this.onClick} className="mx_RoomSubList_label" tabIndex={tabindex}>
|
||||
{this.props.collapsed ? '' : this.props.label}
|
||||
<div className="mx_RoomSubList_roomCount">{roomCount}</div>
|
||||
<div className={chevronClasses} />
|
||||
{badge}
|
||||
{incomingCall}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_createOverflowTile: function(overflowCount, totalCount) {
|
||||
var content = <div className="mx_RoomSubList_chevronDown"></div>;
|
||||
let content = <div className="mx_RoomSubList_chevronDown" />;
|
||||
|
||||
var overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
var overflowNotifCount = overflowNotifications[0];
|
||||
var overflowNotifHighlight = overflowNotifications[1];
|
||||
const overflowNotifications = this.roomNotificationCount(TRUNCATE_AT);
|
||||
const overflowNotifCount = overflowNotifications[0];
|
||||
const overflowNotifHighlight = overflowNotifications[1];
|
||||
if (overflowNotifCount && !this.props.collapsed) {
|
||||
content = FormattingUtils.formatCount(overflowNotifCount);
|
||||
}
|
||||
|
||||
var badgeClasses = classNames({
|
||||
const badgeClasses = classNames({
|
||||
'mx_RoomSubList_moreBadge': true,
|
||||
'mx_RoomSubList_moreBadgeNotify': overflowNotifCount && !this.props.collapsed,
|
||||
'mx_RoomSubList_moreBadgeHighlight': overflowNotifHighlight && !this.props.collapsed,
|
||||
});
|
||||
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomSubList_ellipsis" onClick={this._showFullMemberList}>
|
||||
<div className="mx_RoomSubList_line"></div>
|
||||
<div className="mx_RoomSubList_more">{ _t("more") }</div>
|
||||
<div className={ badgeClasses }>{ content }</div>
|
||||
<div className="mx_RoomSubList_line" />
|
||||
<div className="mx_RoomSubList_more">{_t("more")}</div>
|
||||
<div className={badgeClasses}>{content}</div>
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
||||
_showFullMemberList: function() {
|
||||
this.setState({
|
||||
truncateAt: -1
|
||||
truncateAt: -1,
|
||||
});
|
||||
|
||||
this.props.onShowMoreRooms();
|
||||
|
@ -341,37 +392,51 @@ var RoomSubList = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
var connectDropTarget = this.props.connectDropTarget;
|
||||
var TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
var label = this.props.collapsed ? null : this.props.label;
|
||||
const TruncatedList = sdk.getComponent('elements.TruncatedList');
|
||||
|
||||
let content;
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
|
||||
if (this.props.showEmpty) {
|
||||
// this is new behaviour with still controversial UX in that in hiding RoomSubLists the drop zones for DnD
|
||||
// are also gone so when filtering users can't DnD rooms to some tags but is a lot cleaner otherwise.
|
||||
if (this.state.sortedList.length === 0 && !this.props.searchFilter && this.props.extraTiles.length === 0) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
if (this.state.sortedList.length === 0 && this.props.extraTiles.length === 0) {
|
||||
// if no search filter is applied and there is a placeholder defined then show it, otherwise show nothing
|
||||
if (!this.props.searchFilter && this.props.emptyContent) {
|
||||
content = this.props.emptyContent;
|
||||
} else {
|
||||
// don't show an empty sublist
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
content = this.makeRoomTiles();
|
||||
content.push(...this.props.extraTiles);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.sortedList.length > 0 || this.props.extraTiles.length > 0 || this.props.editable) {
|
||||
var subList;
|
||||
var classes = "mx_RoomSubList";
|
||||
let subList;
|
||||
const classes = "mx_RoomSubList";
|
||||
|
||||
if (!this.state.hidden) {
|
||||
subList = <TruncatedList className={ classes } truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile} >
|
||||
{ content }
|
||||
</TruncatedList>;
|
||||
}
|
||||
else {
|
||||
subList = <TruncatedList className={ classes }>
|
||||
</TruncatedList>;
|
||||
subList = <TruncatedList className={classes} truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this._createOverflowTile}>
|
||||
{content}
|
||||
</TruncatedList>;
|
||||
} else {
|
||||
subList = <TruncatedList className={classes}>
|
||||
</TruncatedList>;
|
||||
}
|
||||
|
||||
const subListContent = <div>
|
||||
{ this._getHeaderJsx() }
|
||||
{ subList }
|
||||
{this._getHeaderJsx()}
|
||||
{subList}
|
||||
</div>;
|
||||
|
||||
return this.props.editable ?
|
||||
|
@ -379,23 +444,26 @@ var RoomSubList = React.createClass({
|
|||
droppableId={"room-sub-list-droppable_" + this.props.tagName}
|
||||
type="draggable-RoomTile"
|
||||
>
|
||||
{ (provided, snapshot) => (
|
||||
{(provided, snapshot) => (
|
||||
<div ref={provided.innerRef}>
|
||||
{ subListContent }
|
||||
{subListContent}
|
||||
</div>
|
||||
) }
|
||||
)}
|
||||
</Droppable> : subListContent;
|
||||
}
|
||||
else {
|
||||
var Loader = sdk.getComponent("elements.Spinner");
|
||||
} else {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
if (this.props.showSpinner) {
|
||||
content = <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx_RoomSubList">
|
||||
{ this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined }
|
||||
{ (this.props.showSpinner && !this.state.hidden) ? <Loader /> : undefined }
|
||||
{this.props.alwaysShowHeader ? this._getHeaderJsx() : undefined}
|
||||
{ this.state.hidden ? undefined : content }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
module.exports = RoomSubList;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -44,7 +45,8 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
|||
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||
import WidgetUtils from '../../utils/WidgetUtils';
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function() {};
|
||||
|
@ -115,6 +117,7 @@ module.exports = React.createClass({
|
|||
showApps: false,
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
|
||||
// error object, as from the matrix client/server API
|
||||
// If we failed to load information about the room,
|
||||
|
@ -182,6 +185,8 @@ module.exports = React.createClass({
|
|||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
shouldPeek: RoomViewStore.shouldPeek(),
|
||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
||||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
||||
};
|
||||
|
||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||
|
@ -314,14 +319,7 @@ module.exports = React.createClass({
|
|||
return false;
|
||||
}
|
||||
|
||||
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
// any valid widget = show apps
|
||||
for (let i = 0; i < appsStateEvents.length; i++) {
|
||||
if (appsStateEvents[i].getContent().type && appsStateEvents[i].getContent().url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return WidgetUtils.getRoomWidgets(room).length > 0;
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
|
@ -615,9 +613,11 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_updatePreviewUrlVisibility: function(room) {
|
||||
_updatePreviewUrlVisibility: function({roomId}) {
|
||||
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit
|
||||
const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled';
|
||||
this.setState({
|
||||
showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
|
||||
showUrlPreview: SettingsStore.getValue(key, roomId),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -642,19 +642,23 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(event) {
|
||||
if (event.getType() === "org.matrix.preview_urls" && this.state.room) {
|
||||
const type = event.getType();
|
||||
if ((type === "org.matrix.preview_urls" || type === "im.vector.web.settings") && this.state.room) {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(this.state.room);
|
||||
}
|
||||
},
|
||||
|
||||
onRoomAccountData: function(event, room) {
|
||||
if (room.roomId == this.state.roomId) {
|
||||
if (event.getType() === "org.matrix.room.color_scheme") {
|
||||
const type = event.getType();
|
||||
if (type === "org.matrix.room.color_scheme") {
|
||||
const color_scheme = event.getContent();
|
||||
// XXX: we should validate the event
|
||||
console.log("Tinter.tint from onRoomAccountData");
|
||||
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
|
||||
} else if (event.getType() === "org.matrix.room.preview_urls") {
|
||||
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
|
||||
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
|
||||
this._updatePreviewUrlVisibility(room);
|
||||
}
|
||||
}
|
||||
|
@ -672,6 +676,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
this._updateRoomMembers();
|
||||
this._checkIfAlone(this.state.room);
|
||||
},
|
||||
|
||||
onRoomMemberMembership: function(ev, member, oldMembership) {
|
||||
|
@ -909,6 +914,8 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
uploadFile: async function(file) {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
dis.dispatch({action: 'view_set_mxid'});
|
||||
return;
|
||||
|
@ -1135,11 +1142,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onPinnedClick: function() {
|
||||
this.setState({showingPinned: !this.state.showingPinned, searching: false});
|
||||
const nowShowingPinned = !this.state.showingPinned;
|
||||
const roomId = this.state.room.roomId;
|
||||
this.setState({showingPinned: nowShowingPinned, searching: false});
|
||||
SettingsStore.setValue("PinnedEvents.isOpen", roomId, SettingLevel.ROOM_DEVICE, nowShowingPinned);
|
||||
},
|
||||
|
||||
onSettingsClick: function() {
|
||||
this.showSettings(true);
|
||||
dis.dispatch({ action: 'open_room_settings' });
|
||||
},
|
||||
|
||||
onSettingsSaveClick: function() {
|
||||
|
@ -1172,24 +1182,20 @@ module.exports = React.createClass({
|
|||
});
|
||||
// still editing room settings
|
||||
} else {
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}
|
||||
}).finally(() => {
|
||||
this.setState({
|
||||
uploadingRoomSettings: false,
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
}).done();
|
||||
},
|
||||
|
||||
onCancelClick: function() {
|
||||
console.log("updateTint from onCancelClick");
|
||||
this.updateTint();
|
||||
this.setState({
|
||||
editingRoomSettings: false,
|
||||
});
|
||||
dis.dispatch({ action: 'close_settings' });
|
||||
if (this.state.forwardingEvent) {
|
||||
dis.dispatch({
|
||||
action: 'forward_event',
|
||||
|
@ -1406,13 +1412,6 @@ module.exports = React.createClass({
|
|||
});*/
|
||||
},
|
||||
|
||||
showSettings: function(show) {
|
||||
// XXX: this is a bit naughty; we should be doing this via props
|
||||
if (show) {
|
||||
this.setState({editingRoomSettings: true});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* called by the parent component when PageUp/Down/etc is pressed.
|
||||
*
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2017, 2018 New Vector Ltd.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -26,6 +26,7 @@ import dis from '../../dispatcher';
|
|||
import { _t } from '../../languageHandler';
|
||||
|
||||
import { Droppable } from 'react-beautiful-dnd';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const TagPanel = React.createClass({
|
||||
displayName: 'TagPanel',
|
||||
|
@ -84,7 +85,10 @@ const TagPanel = React.createClass({
|
|||
},
|
||||
|
||||
onMouseDown(e) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
// only dispatch if its not a no-op
|
||||
if (this.state.selectedTags.length > 0) {
|
||||
dis.dispatch({action: 'deselect_tags'});
|
||||
}
|
||||
},
|
||||
|
||||
onCreateGroupClick(ev) {
|
||||
|
@ -113,17 +117,26 @@ const TagPanel = React.createClass({
|
|||
/>;
|
||||
});
|
||||
|
||||
const clearButton = this.state.selectedTags.length > 0 ?
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/> :
|
||||
<div />;
|
||||
const itemsSelected = this.state.selectedTags.length > 0;
|
||||
|
||||
return <div className="mx_TagPanel">
|
||||
<AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
let clearButton;
|
||||
if (itemsSelected) {
|
||||
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
|
||||
<TintableSvg src="img/icons-close.svg" width="24" height="24"
|
||||
alt={_t("Clear filter")}
|
||||
title={_t("Clear filter")}
|
||||
/>
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_TagPanel', {
|
||||
mx_TagPanel_items_selected: itemsSelected,
|
||||
});
|
||||
|
||||
return <div className={classes}>
|
||||
<div className="mx_TagPanel_clearButton_container">
|
||||
{ clearButton }
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
<div className="mx_TagPanel_divider" />
|
||||
<GeminiScrollbarWrapper
|
||||
className="mx_TagPanel_scroller"
|
||||
|
|
|
@ -81,6 +81,7 @@ const SIMPLE_SETTINGS = [
|
|||
{ id: "VideoView.flipVideoHorizontally" },
|
||||
{ id: "TagPanel.disableTagPanel" },
|
||||
{ id: "enableWidgetScreenshots" },
|
||||
{ id: "RoomSubList.showEmpty" },
|
||||
];
|
||||
|
||||
// These settings must be defined in SettingsStore
|
||||
|
@ -284,7 +285,13 @@ module.exports = React.createClass({
|
|||
this.setState({ electron_settings: settings });
|
||||
},
|
||||
|
||||
_refreshMediaDevices: function() {
|
||||
_refreshMediaDevices: function(stream) {
|
||||
if (stream) {
|
||||
// kill stream so that we don't leave it lingering around with webcam enabled etc
|
||||
// as here we called gUM to ask user for permission to their device names only
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
return CallMediaHandler.getDevices();
|
||||
}).then((mediaDevices) => {
|
||||
|
@ -292,6 +299,7 @@ module.exports = React.createClass({
|
|||
if (this._unmounted) return;
|
||||
this.setState({
|
||||
mediaDevices,
|
||||
activeAudioOutput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audiooutput'),
|
||||
activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
|
||||
activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
|
||||
});
|
||||
|
@ -422,7 +430,6 @@ module.exports = React.createClass({
|
|||
"push notifications on other devices until you log back in to them",
|
||||
) + ".",
|
||||
});
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
},
|
||||
|
||||
_onAddEmailEditFinished: function(value, shouldSubmit) {
|
||||
|
@ -970,6 +977,11 @@ module.exports = React.createClass({
|
|||
return devices.map((device) => <span key={device.deviceId}>{ device.label }</span>);
|
||||
},
|
||||
|
||||
_setAudioOutput: function(deviceId) {
|
||||
this.setState({activeAudioOutput: deviceId});
|
||||
CallMediaHandler.setAudioOutput(deviceId);
|
||||
},
|
||||
|
||||
_setAudioInput: function(deviceId) {
|
||||
this.setState({activeAudioInput: deviceId});
|
||||
CallMediaHandler.setAudioInput(deviceId);
|
||||
|
@ -1010,6 +1022,7 @@ module.exports = React.createClass({
|
|||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
||||
let speakerDropdown = <p>{ _t('No Audio Outputs detected') }</p>;
|
||||
let microphoneDropdown = <p>{ _t('No Microphones detected') }</p>;
|
||||
let webcamDropdown = <p>{ _t('No Webcams detected') }</p>;
|
||||
|
||||
|
@ -1018,6 +1031,26 @@ module.exports = React.createClass({
|
|||
label: _t('Default Device'),
|
||||
};
|
||||
|
||||
const audioOutputs = this.state.mediaDevices.audiooutput.slice(0);
|
||||
if (audioOutputs.length > 0) {
|
||||
let defaultOutput = '';
|
||||
if (!audioOutputs.some((input) => input.deviceId === 'default')) {
|
||||
audioOutputs.unshift(defaultOption);
|
||||
} else {
|
||||
defaultOutput = 'default';
|
||||
}
|
||||
|
||||
speakerDropdown = <div>
|
||||
<h4>{ _t('Audio Output') }</h4>
|
||||
<Dropdown
|
||||
className="mx_UserSettings_webRtcDevices_dropdown"
|
||||
value={this.state.activeAudioOutput || defaultOutput}
|
||||
onOptionChange={this._setAudioOutput}>
|
||||
{ this._mapWebRtcDevicesToSpans(audioOutputs) }
|
||||
</Dropdown>
|
||||
</div>;
|
||||
}
|
||||
|
||||
const audioInputs = this.state.mediaDevices.audioinput.slice(0);
|
||||
if (audioInputs.length > 0) {
|
||||
let defaultInput = '';
|
||||
|
@ -1059,8 +1092,9 @@ module.exports = React.createClass({
|
|||
}
|
||||
|
||||
return <div>
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
{ speakerDropdown }
|
||||
{ microphoneDropdown }
|
||||
{ webcamDropdown }
|
||||
</div>;
|
||||
},
|
||||
|
||||
|
@ -1074,6 +1108,14 @@ module.exports = React.createClass({
|
|||
</div>;
|
||||
},
|
||||
|
||||
onSelfShareClick: function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share self dialog', '', ShareDialog, {
|
||||
target: cli.getUser(this._me),
|
||||
});
|
||||
},
|
||||
|
||||
_showSpoiler: function(event) {
|
||||
const target = event.target;
|
||||
target.innerHTML = target.getAttribute('data-spoiler');
|
||||
|
@ -1295,10 +1337,13 @@ module.exports = React.createClass({
|
|||
|
||||
<div className="mx_UserSettings_section">
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t("Logged in as:") } { this._me }
|
||||
{ _t("Logged in as:") + ' ' }
|
||||
<a onClick={this.onSelfShareClick} className="mx_UserSettings_link">
|
||||
{ this._me }
|
||||
</a>
|
||||
</div>
|
||||
<div className="mx_UserSettings_advanced">
|
||||
{ _t('Access Token:') }
|
||||
{ _t('Access Token:') + ' ' }
|
||||
<span className="mx_UserSettings_advanced_spoiler"
|
||||
onClick={this._showSpoiler}
|
||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,8 +15,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -45,6 +43,8 @@ module.exports = React.createClass({
|
|||
enteredHomeserverUrl: this.props.customHsUrl || this.props.defaultHsUrl,
|
||||
enteredIdentityServerUrl: this.props.customIsUrl || this.props.defaultIsUrl,
|
||||
progress: null,
|
||||
password: null,
|
||||
password2: null,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -103,7 +103,7 @@ module.exports = React.createClass({
|
|||
</div>,
|
||||
button: _t('Continue'),
|
||||
extraButtons: [
|
||||
<button className="mx_Dialog_primary"
|
||||
<button key="export_keys" className="mx_Dialog_primary"
|
||||
onClick={this._onExportE2eKeysClicked}>
|
||||
{ _t('Export E2E room keys') }
|
||||
</button>,
|
||||
|
@ -169,7 +169,8 @@ module.exports = React.createClass({
|
|||
} else if (this.state.progress === "sent_email") {
|
||||
resetPasswordJsx = (
|
||||
<div className="mx_Login_prompt">
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
|
||||
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, " +
|
||||
"click below.", { emailAddress: this.state.email }) }
|
||||
<br />
|
||||
<input className="mx_Login_submit" type="button" onClick={this.onVerify}
|
||||
value={_t('I have verified my email address')} />
|
||||
|
@ -179,14 +180,15 @@ module.exports = React.createClass({
|
|||
resetPasswordJsx = (
|
||||
<div className="mx_Login_prompt">
|
||||
<p>{ _t('Your password has been reset') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
|
||||
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. ' +
|
||||
'To re-enable notifications, sign in again on each device') }.</p>
|
||||
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
|
||||
value={_t('Return to login screen')} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
|
@ -199,6 +201,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
resetPasswordJsx = (
|
||||
<div>
|
||||
<div className="mx_Login_prompt">
|
||||
|
@ -233,6 +237,7 @@ module.exports = React.createClass({
|
|||
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
|
||||
{ _t('Create an account') }
|
||||
</a>
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
|
38
src/components/structures/login/LanguageSelector.js
Normal file
38
src/components/structures/login/LanguageSelector.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import {getCurrentLanguage} from "../../../languageHandler";
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import sdk from '../../../index';
|
||||
import React from 'react';
|
||||
|
||||
function onChange(newLang) {
|
||||
if (getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
}
|
||||
|
||||
export default function LanguageSelector() {
|
||||
if (SdkConfig.get()['disable_login_language_selector']) return <div />;
|
||||
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={onChange} className="mx_Login_language" value={getCurrentLanguage()} />
|
||||
</div>;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,15 +21,13 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as languageHandler from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
/**
|
||||
* A wire component which glues together login UI components and Login logic
|
||||
|
@ -94,6 +93,13 @@ module.exports = React.createClass({
|
|||
this._unmounted = true;
|
||||
},
|
||||
|
||||
onPasswordLoginError: function(errorText) {
|
||||
this.setState({
|
||||
errorText,
|
||||
loginIncorrect: Boolean(errorText),
|
||||
});
|
||||
},
|
||||
|
||||
onPasswordLogin: function(username, phoneCountry, phoneNumber, password) {
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -113,10 +119,10 @@ module.exports = React.createClass({
|
|||
|
||||
// Some error strings only apply for logging in
|
||||
const usingEmail = username.indexOf("@") > 0;
|
||||
if (error.httpStatus == 400 && usingEmail) {
|
||||
if (error.httpStatus === 400 && usingEmail) {
|
||||
errorText = _t('This Home Server does not support login using email address.');
|
||||
} else if (error.httpStatus === 401 || error.httpStatus === 403) {
|
||||
if (SdkConfig.get().disable_custom_urls) {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{ _t('Incorrect username and/or password.') }</div>
|
||||
|
@ -143,7 +149,7 @@ module.exports = React.createClass({
|
|||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
// mentions this (although the bug is for UI auth which is not this)
|
||||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus == 403,
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) {
|
||||
|
@ -231,7 +237,7 @@ module.exports = React.createClass({
|
|||
hsUrl = hsUrl || this.state.enteredHomeserverUrl;
|
||||
isUrl = isUrl || this.state.enteredIdentityServerUrl;
|
||||
|
||||
const fallbackHsUrl = hsUrl == this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null;
|
||||
|
||||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
|
@ -310,19 +316,27 @@ module.exports = React.createClass({
|
|||
!this.state.enteredHomeserverUrl.startsWith("http"))
|
||||
) {
|
||||
errorText = <span>
|
||||
{
|
||||
_t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">{ sub }</a>; } },
|
||||
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href="https://www.google.com/search?&q=enable%20unsafe%20scripts">
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{
|
||||
_t("Can't connect to homeserver - please check your connectivity, ensure your <a>homeserver's SSL certificate</a> is trusted, and that a browser extension is not blocking requests.",
|
||||
{},
|
||||
{ 'a': (sub) => { return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>; } },
|
||||
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " +
|
||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a href={this.state.enteredHomeserverUrl}>{ sub }</a>;
|
||||
},
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
}
|
||||
|
@ -350,6 +364,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
|
@ -370,23 +385,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
_onLanguageChange: function(newLang) {
|
||||
if (languageHandler.getCurrentLanguage() !== newLang) {
|
||||
SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
|
||||
PlatformPeg.get().reload();
|
||||
}
|
||||
},
|
||||
|
||||
_renderLanguageSetting: function() {
|
||||
const LanguageDropdown = sdk.getComponent('views.elements.LanguageDropdown');
|
||||
return <div className="mx_Login_language_div">
|
||||
<LanguageDropdown onOptionChange={this._onLanguageChange}
|
||||
className="mx_Login_language"
|
||||
value={languageHandler.getCurrentLanguage()}
|
||||
/>
|
||||
</div>;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const LoginPage = sdk.getComponent("login.LoginPage");
|
||||
|
@ -399,25 +397,14 @@ module.exports = React.createClass({
|
|||
if (this.props.enableGuest) {
|
||||
loginAsGuestJsx =
|
||||
<a className="mx_Login_create" onClick={this._onLoginAsGuestClick} href="#">
|
||||
{ _t('Login as guest') }
|
||||
{ _t('Try the app first') }
|
||||
</a>;
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx =
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app') }
|
||||
</a>;
|
||||
}
|
||||
*/
|
||||
|
||||
let serverConfig;
|
||||
let header;
|
||||
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfig = <ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
customHsUrl={this.props.customHsUrl}
|
||||
|
@ -447,6 +434,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
|
@ -460,8 +449,7 @@ module.exports = React.createClass({
|
|||
{ _t('Create an account') }
|
||||
</a>
|
||||
{ loginAsGuestJsx }
|
||||
{ returnToAppJsx }
|
||||
{ !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,7 +23,6 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
import sdk from '../../../index';
|
||||
import ServerConfig from '../../views/login/ServerConfig';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import RegistrationForm from '../../views/login/RegistrationForm';
|
||||
import RtsClient from '../../../RtsClient';
|
||||
|
@ -62,6 +62,12 @@ module.exports = React.createClass({
|
|||
onLoginClick: PropTypes.func.isRequired,
|
||||
onCancelClick: PropTypes.func,
|
||||
onServerConfigChange: PropTypes.func.isRequired,
|
||||
|
||||
rtsClient: PropTypes.shape({
|
||||
getTeamsConfig: PropTypes.func.isRequired,
|
||||
trackReferral: PropTypes.func.isRequired,
|
||||
getTeam: PropTypes.func.isRequired,
|
||||
}),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -133,7 +139,7 @@ module.exports = React.createClass({
|
|||
newState.isUrl = config.isUrl;
|
||||
}
|
||||
this.props.onServerConfigChange(config);
|
||||
this.setState(newState, function() {
|
||||
this.setState(newState, () => {
|
||||
this._replaceClient();
|
||||
});
|
||||
},
|
||||
|
@ -159,11 +165,11 @@ module.exports = React.createClass({
|
|||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
|
||||
let msisdn_available = false;
|
||||
let msisdnAvailable = false;
|
||||
for (const flow of response.available_flows) {
|
||||
msisdn_available |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1;
|
||||
}
|
||||
if (!msisdn_available) {
|
||||
if (!msisdnAvailable) {
|
||||
msg = _t('This server does not support authentication with a phone number.');
|
||||
}
|
||||
}
|
||||
|
@ -242,7 +248,7 @@ module.exports = React.createClass({
|
|||
return matrixClient.getPushers().then((resp)=>{
|
||||
const pushers = resp.pushers;
|
||||
for (let i = 0; i < pushers.length; ++i) {
|
||||
if (pushers[i].kind == 'email') {
|
||||
if (pushers[i].kind === 'email') {
|
||||
const emailPusher = pushers[i];
|
||||
emailPusher.data = { brand: this.props.brand };
|
||||
matrixClient.setPusher(emailPusher).done(() => {
|
||||
|
@ -267,7 +273,7 @@ module.exports = React.createClass({
|
|||
errMsg = _t('Passwords don\'t match.');
|
||||
break;
|
||||
case "RegistrationForm.ERR_PASSWORD_LENGTH":
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH: MIN_PASSWORD_LENGTH});
|
||||
errMsg = _t('Password too short (min %(MIN_PASSWORD_LENGTH)s).', {MIN_PASSWORD_LENGTH});
|
||||
break;
|
||||
case "RegistrationForm.ERR_EMAIL_INVALID":
|
||||
errMsg = _t('This doesn\'t look like a valid email address.');
|
||||
|
@ -353,7 +359,7 @@ module.exports = React.createClass({
|
|||
registerBody = <Spinner />;
|
||||
} else {
|
||||
let serverConfigSection;
|
||||
if (!SdkConfig.get().disable_custom_urls) {
|
||||
if (!SdkConfig.get()['disable_custom_urls']) {
|
||||
serverConfigSection = (
|
||||
<ServerConfig ref="serverConfig"
|
||||
withToggleButton={true}
|
||||
|
@ -385,18 +391,6 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
let returnToAppJsx;
|
||||
/*
|
||||
// with the advent of ILAG I don't think we need this any more
|
||||
if (this.props.onCancelClick) {
|
||||
returnToAppJsx = (
|
||||
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
|
||||
{ _t('Return to app') }
|
||||
</a>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
let header;
|
||||
let errorText;
|
||||
// FIXME: remove hardcoded Status team tweaks at some point
|
||||
|
@ -418,6 +412,8 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector');
|
||||
|
||||
return (
|
||||
<LoginPage>
|
||||
<div className="mx_Login_box">
|
||||
|
@ -431,7 +427,7 @@ module.exports = React.createClass({
|
|||
{ registerBody }
|
||||
{ signIn }
|
||||
{ errorText }
|
||||
{ returnToAppJsx }
|
||||
<LanguageSelector />
|
||||
<LoginFooter />
|
||||
</div>
|
||||
</LoginPage>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
Copyright 2018 Vector Creations Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Modal from '../../../Modal';
|
||||
import {Group} from 'matrix-js-sdk';
|
||||
import GroupStore from "../../../stores/GroupStore";
|
||||
|
||||
export default class GroupInviteTileContextMenu extends React.Component {
|
||||
static propTypes = {
|
||||
group: PropTypes.instanceOf(Group).isRequired,
|
||||
/* callback called when the menu is dismissed */
|
||||
onFinished: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._onClickReject = this._onClickReject.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._unmounted = false;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
}
|
||||
|
||||
_onClickReject() {
|
||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
|
||||
title: _t('Reject invitation'),
|
||||
description: _t('Are you sure you want to reject the invitation?'),
|
||||
onFinished: async (shouldLeave) => {
|
||||
if (!shouldLeave) return;
|
||||
|
||||
// FIXME: controller shouldn't be loading a view :(
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
|
||||
|
||||
try {
|
||||
await GroupStore.leaveGroup(this.props.group.groupId);
|
||||
} catch (e) {
|
||||
console.error("Error rejecting community invite: ", e);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Error rejecting invite', '', ErrorDialog, {
|
||||
title: _t("Error"),
|
||||
description: _t("Unable to reject invite"),
|
||||
});
|
||||
} finally {
|
||||
modal.close();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Close the context menu
|
||||
if (this.props.onFinished) {
|
||||
this.props.onFinished();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div>
|
||||
<div className="mx_RoomTileContextMenu_leave" onClick={this._onClickReject} >
|
||||
<img className="mx_RoomTileContextMenu_tag_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||
{ _t('Reject') }
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
|
@ -15,10 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import dis from '../../../dispatcher';
|
||||
|
@ -184,6 +183,15 @@ module.exports = React.createClass({
|
|||
this.closeMenu();
|
||||
},
|
||||
|
||||
onPermalinkClick: function(e: Event) {
|
||||
e.preventDefault();
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {
|
||||
target: this.props.mxEvent,
|
||||
});
|
||||
this.closeMenu();
|
||||
},
|
||||
|
||||
onReplyClick: function() {
|
||||
dis.dispatch({
|
||||
action: 'reply_to_event',
|
||||
|
@ -211,7 +219,10 @@ module.exports = React.createClass({
|
|||
let replyButton;
|
||||
let collapseReplyThread;
|
||||
|
||||
if (eventStatus === 'not_sent') {
|
||||
// status is SENT before remote-echo, null after
|
||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||
|
||||
if (eventStatus === EventStatus.NOT_SENT) {
|
||||
resendButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onResendClick}>
|
||||
{ _t('Resend') }
|
||||
|
@ -219,7 +230,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.state.canRedact) {
|
||||
if (isSent && this.state.canRedact) {
|
||||
redactButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
||||
{ _t('Remove') }
|
||||
|
@ -227,7 +238,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (eventStatus === "queued" || eventStatus === "not_sent") {
|
||||
if (eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT) {
|
||||
cancelButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onCancelSendClick}>
|
||||
{ _t('Cancel Sending') }
|
||||
|
@ -235,7 +246,7 @@ module.exports = React.createClass({
|
|||
);
|
||||
}
|
||||
|
||||
if (!eventStatus && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
if (isSent && this.props.mxEvent.getType() === 'm.room.message') {
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
|
||||
forwardButton = (
|
||||
|
@ -244,13 +255,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
);
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_rich_quoting")) {
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
replyButton = (
|
||||
<div className="mx_MessageContextMenu_field" onClick={this.onReplyClick}>
|
||||
{ _t('Reply') }
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.canPin) {
|
||||
pinButton = (
|
||||
|
@ -290,7 +299,7 @@ module.exports = React.createClass({
|
|||
const permalinkButton = (
|
||||
<div className="mx_MessageContextMenu_field">
|
||||
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())}
|
||||
target="_blank" rel="noopener" onClick={this.closeMenu}>{ _t('Permalink') }</a>
|
||||
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Promise from 'bluebird';
|
||||
|
@ -27,6 +27,13 @@ import GroupStore from '../../../stores/GroupStore';
|
|||
const TRUNCATE_QUERY_LIST = 40;
|
||||
const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200;
|
||||
|
||||
const addressTypeName = {
|
||||
'mx-user-id': _td("Matrix ID"),
|
||||
'mx-room-id': _td("Matrix Room ID"),
|
||||
'email': _td("email address"),
|
||||
};
|
||||
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: "AddressPickerDialog",
|
||||
|
||||
|
@ -66,7 +73,7 @@ module.exports = React.createClass({
|
|||
|
||||
// List of UserAddressType objects representing
|
||||
// the list of addresses we're going to invite
|
||||
userList: [],
|
||||
selectedList: [],
|
||||
|
||||
// Whether a search is ongoing
|
||||
busy: false,
|
||||
|
@ -76,10 +83,9 @@ module.exports = React.createClass({
|
|||
serverSupportsUserDirectory: true,
|
||||
// The query being searched for
|
||||
query: "",
|
||||
// List of UserAddressType objects representing
|
||||
// the set of auto-completion results for the current search
|
||||
// query.
|
||||
queryList: [],
|
||||
// List of UserAddressType objects representing the set of
|
||||
// auto-completion results for the current search query.
|
||||
suggestedList: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -91,14 +97,14 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onButtonClick: function() {
|
||||
let userList = this.state.userList.slice();
|
||||
let selectedList = this.state.selectedList.slice();
|
||||
// Check the text input field to see if user has an unconverted address
|
||||
// If there is and it's valid add it to the local userList
|
||||
// If there is and it's valid add it to the local selectedList
|
||||
if (this.refs.textinput.value !== '') {
|
||||
userList = this._addInputToList();
|
||||
if (userList === null) return;
|
||||
selectedList = this._addInputToList();
|
||||
if (selectedList === null) return;
|
||||
}
|
||||
this.props.onFinished(true, userList);
|
||||
this.props.onFinished(true, selectedList);
|
||||
},
|
||||
|
||||
onCancel: function() {
|
||||
|
@ -118,18 +124,18 @@ module.exports = React.createClass({
|
|||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionDown();
|
||||
} else if (this.state.queryList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
} else if (this.state.suggestedList.length > 0 && (e.keyCode === 188 || e.keyCode === 13 || e.keyCode === 9)) { // comma or enter or tab
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.addressSelector) this.addressSelector.chooseSelection();
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.userList.length && e.keyCode === 8) { // backspace
|
||||
} else if (this.refs.textinput.value.length === 0 && this.state.selectedList.length && e.keyCode === 8) { // backspace
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
this.onDismissed(this.state.userList.length - 1)();
|
||||
this.onDismissed(this.state.selectedList.length - 1)();
|
||||
} else if (e.keyCode === 13) { // enter
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (this.refs.textinput.value == '') {
|
||||
if (this.refs.textinput.value === '') {
|
||||
// if there's nothing in the input box, submit the form
|
||||
this.onButtonClick();
|
||||
} else {
|
||||
|
@ -148,7 +154,7 @@ module.exports = React.createClass({
|
|||
clearTimeout(this.queryChangedDebouncer);
|
||||
}
|
||||
// Only do search if there is something to search
|
||||
if (query.length > 0 && query != '@' && query.length >= 2) {
|
||||
if (query.length > 0 && query !== '@' && query.length >= 2) {
|
||||
this.queryChangedDebouncer = setTimeout(() => {
|
||||
if (this.props.pickerType === 'user') {
|
||||
if (this.props.groupId) {
|
||||
|
@ -170,7 +176,7 @@ module.exports = React.createClass({
|
|||
}, QUERY_USER_DIRECTORY_DEBOUNCE_MS);
|
||||
} else {
|
||||
this.setState({
|
||||
queryList: [],
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
searchError: null,
|
||||
});
|
||||
|
@ -179,11 +185,11 @@ module.exports = React.createClass({
|
|||
|
||||
onDismissed: function(index) {
|
||||
return () => {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.splice(index, 1);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.splice(index, 1);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -197,11 +203,11 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onSelected: function(index) {
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(this.state.queryList[index]);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(this.state.suggestedList[index]);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
|
@ -379,10 +385,10 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_processResults: function(results, query) {
|
||||
const queryList = [];
|
||||
const suggestedList = [];
|
||||
results.forEach((result) => {
|
||||
if (result.room_id) {
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-room-id',
|
||||
address: result.room_id,
|
||||
displayName: result.name,
|
||||
|
@ -399,7 +405,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Return objects, structure of which is defined
|
||||
// by UserAddressType
|
||||
queryList.push({
|
||||
suggestedList.push({
|
||||
addressType: 'mx-user-id',
|
||||
address: result.user_id,
|
||||
displayName: result.display_name,
|
||||
|
@ -413,18 +419,18 @@ module.exports = React.createClass({
|
|||
// a perfectly valid address if there are close matches.
|
||||
const addrType = getAddressType(query);
|
||||
if (this.props.validAddressTypes.includes(addrType)) {
|
||||
queryList.unshift({
|
||||
suggestedList.unshift({
|
||||
addressType: addrType,
|
||||
address: query,
|
||||
isKnown: false,
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
if (addrType == 'email') {
|
||||
if (addrType === 'email') {
|
||||
this._lookupThreepid(addrType, query).done();
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
queryList,
|
||||
suggestedList,
|
||||
error: false,
|
||||
}, () => {
|
||||
if (this.addressSelector) this.addressSelector.moveSelectionTop();
|
||||
|
@ -442,14 +448,14 @@ module.exports = React.createClass({
|
|||
if (!this.props.validAddressTypes.includes(addrType)) {
|
||||
this.setState({ error: true });
|
||||
return null;
|
||||
} else if (addrType == 'mx-user-id') {
|
||||
} else if (addrType === 'mx-user-id') {
|
||||
const user = MatrixClientPeg.get().getUser(addrObj.address);
|
||||
if (user) {
|
||||
addrObj.displayName = user.displayName;
|
||||
addrObj.avatarMxc = user.avatarUrl;
|
||||
addrObj.isKnown = true;
|
||||
}
|
||||
} else if (addrType == 'mx-room-id') {
|
||||
} else if (addrType === 'mx-room-id') {
|
||||
const room = MatrixClientPeg.get().getRoom(addrObj.address);
|
||||
if (room) {
|
||||
addrObj.displayName = room.name;
|
||||
|
@ -458,15 +464,15 @@ module.exports = React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
const userList = this.state.userList.slice();
|
||||
userList.push(addrObj);
|
||||
const selectedList = this.state.selectedList.slice();
|
||||
selectedList.push(addrObj);
|
||||
this.setState({
|
||||
userList: userList,
|
||||
queryList: [],
|
||||
selectedList,
|
||||
suggestedList: [],
|
||||
query: "",
|
||||
});
|
||||
if (this._cancelThreepidLookup) this._cancelThreepidLookup();
|
||||
return userList;
|
||||
return selectedList;
|
||||
},
|
||||
|
||||
_lookupThreepid: function(medium, address) {
|
||||
|
@ -492,7 +498,7 @@ module.exports = React.createClass({
|
|||
if (res === null) return null;
|
||||
if (cancelled) return null;
|
||||
this.setState({
|
||||
queryList: [{
|
||||
suggestedList: [{
|
||||
// a UserAddressType
|
||||
addressType: medium,
|
||||
address: address,
|
||||
|
@ -510,15 +516,27 @@ module.exports = React.createClass({
|
|||
const AddressSelector = sdk.getComponent("elements.AddressSelector");
|
||||
this.scrollElement = null;
|
||||
|
||||
// map addressType => set of addresses to avoid O(n*m) operation
|
||||
const selectedAddresses = {};
|
||||
this.state.selectedList.forEach(({address, addressType}) => {
|
||||
if (!selectedAddresses[addressType]) selectedAddresses[addressType] = new Set();
|
||||
selectedAddresses[addressType].add(address);
|
||||
});
|
||||
|
||||
// Filter out any addresses in the above already selected addresses (matching both type and address)
|
||||
const filteredSuggestedList = this.state.suggestedList.filter(({address, addressType}) => {
|
||||
return !(selectedAddresses[addressType] && selectedAddresses[addressType].has(address));
|
||||
});
|
||||
|
||||
const query = [];
|
||||
// create the invite list
|
||||
if (this.state.userList.length > 0) {
|
||||
if (this.state.selectedList.length > 0) {
|
||||
const AddressTile = sdk.getComponent("elements.AddressTile");
|
||||
for (let i = 0; i < this.state.userList.length; i++) {
|
||||
for (let i = 0; i < this.state.selectedList.length; i++) {
|
||||
query.push(
|
||||
<AddressTile
|
||||
key={i}
|
||||
address={this.state.userList[i]}
|
||||
address={this.state.selectedList[i]}
|
||||
canDismiss={true}
|
||||
onDismissed={this.onDismissed(i)}
|
||||
showAddress={this.props.pickerType === 'user'} />,
|
||||
|
@ -528,7 +546,7 @@ module.exports = React.createClass({
|
|||
|
||||
// Add the query at the end
|
||||
query.push(
|
||||
<textarea key={this.state.userList.length}
|
||||
<textarea key={this.state.selectedList.length}
|
||||
rows="1"
|
||||
id="textinput"
|
||||
ref="textinput"
|
||||
|
@ -543,34 +561,22 @@ module.exports = React.createClass({
|
|||
let error;
|
||||
let addressSelector;
|
||||
if (this.state.error) {
|
||||
let tryUsing = '';
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => {
|
||||
return {
|
||||
'mx-user-id': _t("Matrix ID"),
|
||||
'mx-room-id': _t("Matrix Room ID"),
|
||||
'email': _t("email address"),
|
||||
}[t];
|
||||
});
|
||||
tryUsing = _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
});
|
||||
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
|
||||
error = <div className="mx_ChatInviteDialog_error">
|
||||
{ _t("You have entered an invalid address.") }
|
||||
<br />
|
||||
{ tryUsing }
|
||||
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
|
||||
validTypesList: validTypeDescriptions.join(", "),
|
||||
}) }
|
||||
</div>;
|
||||
} else if (this.state.searchError) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
|
||||
} else if (
|
||||
this.state.query.length > 0 &&
|
||||
this.state.queryList.length === 0 &&
|
||||
!this.state.busy
|
||||
) {
|
||||
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
|
||||
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
|
||||
} else {
|
||||
addressSelector = (
|
||||
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
|
||||
addressList={this.state.queryList}
|
||||
addressList={filteredSuggestedList}
|
||||
showAddress={this.props.pickerType === 'user'}
|
||||
onSelected={this.onSelected}
|
||||
truncateAt={TRUNCATE_QUERY_LIST}
|
||||
|
|
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
|
@ -64,7 +65,10 @@ export default React.createClass({
|
|||
|
||||
// Id of content element
|
||||
// If provided, this is used to add a aria-describedby attribute
|
||||
contentId: React.PropTypes.string,
|
||||
contentId: PropTypes.string,
|
||||
|
||||
// optional additional class for the title element
|
||||
titleClass: PropTypes.string,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
|
@ -105,25 +109,28 @@ export default React.createClass({
|
|||
render: function() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
cancelButton = <AccessibleButton onClick={this._onCancelClick} className="mx_Dialog_cancelButton">
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FocusTrap onKeyDown={this._onKeyDown}
|
||||
className={this.props.className}
|
||||
role="dialog"
|
||||
aria-labelledby='mx_BaseDialog_title'
|
||||
// This should point to a node describing the dialog.
|
||||
// If we were about to completelly follow this recommendation we'd need to
|
||||
// If we were about to completely follow this recommendation we'd need to
|
||||
// make all the components relying on BaseDialog to be aware of it.
|
||||
// So instead we will use the whole content as the description.
|
||||
// Description comes first and if the content contains more text,
|
||||
// AT users can skip its presentation.
|
||||
aria-describedby={this.props.contentId}
|
||||
>
|
||||
{ this.props.hasCancel ? <AccessibleButton onClick={this._onCancelClick}
|
||||
className="mx_Dialog_cancelButton"
|
||||
>
|
||||
<TintableSvg src="img/icons-close-button.svg" width="35" height="35" />
|
||||
</AccessibleButton> : null }
|
||||
<div className={'mx_Dialog_title ' + this.props.titleClass} id='mx_BaseDialog_title'>
|
||||
{ cancelButton }
|
||||
<div className={classNames('mx_Dialog_title', this.props.titleClass)} id='mx_BaseDialog_title'>
|
||||
{ this.props.title }
|
||||
</div>
|
||||
{ this.props.children }
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -28,6 +29,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onFinished = this.onFinished.bind(this);
|
||||
this.onRoomTileClick = this.onRoomTileClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -53,10 +55,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
const room = client.getRoom(roomId);
|
||||
if (room) {
|
||||
const me = room.getMember(client.credentials.userId);
|
||||
const highlight = (
|
||||
room.getUnreadNotificationCount('highlight') > 0 ||
|
||||
me.membership == "invite"
|
||||
);
|
||||
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === "invite";
|
||||
tiles.push(
|
||||
<RoomTile key={room.roomId} room={room}
|
||||
transparent={true}
|
||||
|
@ -64,7 +63,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
selected={false}
|
||||
unread={Unread.doesRoomHaveUnreadMessages(room)}
|
||||
highlight={highlight}
|
||||
isInvite={me.membership == "invite"}
|
||||
isInvite={me.membership === "invite"}
|
||||
onClick={this.onRoomTileClick}
|
||||
/>,
|
||||
);
|
||||
|
@ -110,6 +109,10 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
this.props.onExistingRoomSelected(roomId);
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
render() {
|
||||
let title = '';
|
||||
let content = null;
|
||||
|
@ -170,14 +173,14 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
{ profile }
|
||||
</div>
|
||||
<DialogButtons primaryButton={_t('Start Chatting')}
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus="true" />
|
||||
onPrimaryButtonClick={this.props.onNewDMClick} focus={true} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return (
|
||||
<BaseDialog className='mx_ChatCreateOrReuseDialog'
|
||||
onFinished={this.props.onFinished.bind(false)}
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
contentId='mx_Dialog_content'
|
||||
>
|
||||
|
@ -187,7 +190,7 @@ export default class ChatCreateOrReuseDialog extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
ChatCreateOrReuseDialog.propTyps = {
|
||||
ChatCreateOrReuseDialog.propTypes = {
|
||||
userId: PropTypes.string.isRequired,
|
||||
// Called when clicking outside of the dialog
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
|
|
|
@ -52,8 +52,8 @@ export default React.createClass({
|
|||
<div className="mx_CreateRoomDialog_label">
|
||||
<label htmlFor="textinput"> { _t('Room name (optional)') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} size="64" />
|
||||
<div className="mx_CreateRoomDialog_input_container">
|
||||
<input id="textinput" ref="textinput" className="mx_CreateRoomDialog_input" autoFocus={true} />
|
||||
</div>
|
||||
<br />
|
||||
|
||||
|
|
|
@ -33,10 +33,21 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
this._onOk = this._onOk.bind(this);
|
||||
this._onCancel = this._onCancel.bind(this);
|
||||
this._onPasswordFieldChange = this._onPasswordFieldChange.bind(this);
|
||||
this._onEraseFieldChange = this._onEraseFieldChange.bind(this);
|
||||
|
||||
const deactivationPreferences =
|
||||
MatrixClientPeg.get().getAccountData('im.riot.account_deactivation_preferences');
|
||||
|
||||
const shouldErase = (
|
||||
deactivationPreferences &&
|
||||
deactivationPreferences.getContent() &&
|
||||
deactivationPreferences.getContent().shouldErase
|
||||
) || false;
|
||||
|
||||
this.state = {
|
||||
confirmButtonEnabled: false,
|
||||
busy: false,
|
||||
shouldErase,
|
||||
errStr: null,
|
||||
};
|
||||
}
|
||||
|
@ -47,19 +58,55 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
_onOk() {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
_onEraseFieldChange(ev) {
|
||||
this.setState({
|
||||
shouldErase: ev.target.checked,
|
||||
});
|
||||
}
|
||||
|
||||
async _onOk() {
|
||||
this.setState({busy: true});
|
||||
MatrixClientPeg.get().deactivateAccount({
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
}).done(() => {
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}, (err) => {
|
||||
|
||||
// Before we deactivate the account insert an event into
|
||||
// the user's account data indicating that they wish to be
|
||||
// erased from the homeserver.
|
||||
//
|
||||
// We do this because the API for erasing after deactivation
|
||||
// might not be supported by the connected homeserver. Leaving
|
||||
// an indication in account data is only best-effort, and
|
||||
// in the worse case, the HS maintainer would have to run a
|
||||
// script to erase deactivated accounts that have shouldErase
|
||||
// set to true in im.riot.account_deactivation_preferences.
|
||||
//
|
||||
// Note: The preferences are scoped to Riot, hence the
|
||||
// "im.riot..." event type.
|
||||
//
|
||||
// Note: This may have already been set on previous attempts
|
||||
// where, for example, the user entered the wrong password.
|
||||
// This is fine because the UI always indicates the preference
|
||||
// prior to us calling `deactivateAccount`.
|
||||
try {
|
||||
await MatrixClientPeg.get().setAccountData('im.riot.account_deactivation_preferences', {
|
||||
shouldErase: this.state.shouldErase,
|
||||
});
|
||||
} catch (err) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
errStr: _t('Failed to indicate account erasure'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// This assumes that the HS requires password UI auth
|
||||
// for this endpoint. In reality it could be any UI auth.
|
||||
const auth = {
|
||||
type: 'm.login.password',
|
||||
user: MatrixClientPeg.get().credentials.userId,
|
||||
password: this._passwordField.value,
|
||||
};
|
||||
await MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase);
|
||||
} catch (err) {
|
||||
let errStr = _t('Unknown error');
|
||||
// https://matrix.org/jira/browse/SYN-744
|
||||
if (err.httpStatus == 401 || err.httpStatus == 403) {
|
||||
|
@ -70,7 +117,12 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
busy: false,
|
||||
errStr: errStr,
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||
Lifecycle.onLoggedOut();
|
||||
this.props.onFinished(false);
|
||||
}
|
||||
|
||||
_onCancel() {
|
||||
|
@ -105,21 +157,64 @@ export default class DeactivateAccountDialog extends React.Component {
|
|||
onFinished={this.props.onFinished}
|
||||
onEnterPressed={this.onOk}
|
||||
titleClass="danger"
|
||||
title={_t("Deactivate Account")}>
|
||||
title={_t("Deactivate Account")}
|
||||
>
|
||||
<div className="mx_Dialog_content">
|
||||
<p>{ _t("This will make your account permanently unusable. You will not be able to re-register the same user ID.") }</p>
|
||||
<p>{ _t(
|
||||
"This will make your account permanently unusable. " +
|
||||
"You will not be able to log in, and no one will be able to re-register the same " +
|
||||
"user ID. " +
|
||||
"This will cause your account to leave all rooms it is participating in, and it " +
|
||||
"will remove your account details from your identity server. " +
|
||||
"<b>This action is irreversible.</b>",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{ _t("This action is irreversible.") }</p>
|
||||
<p>{ _t(
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you " +
|
||||
"have sent.</b> " +
|
||||
"If you would like us to forget your messages, please tick the box below.",
|
||||
{},
|
||||
{ b: (sub) => <b> { sub } </b> },
|
||||
) }</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password.") }</p>
|
||||
<p>{ _t(
|
||||
"Message visibility in Matrix is similar to email. " +
|
||||
"Our forgetting your messages means that messages you have sent will not be shared " +
|
||||
"with any new or unregistered users, but registered users who already have access " +
|
||||
"to these messages will still have access to their copy.",
|
||||
) }</p>
|
||||
|
||||
<div className="mx_DeactivateAccountDialog_input_section">
|
||||
<p>
|
||||
<label htmlFor="mx_DeactivateAccountDialog_erase_account_input">
|
||||
<input
|
||||
id="mx_DeactivateAccountDialog_erase_account_input"
|
||||
type="checkbox"
|
||||
checked={this.state.shouldErase}
|
||||
onChange={this._onEraseFieldChange}
|
||||
/>
|
||||
{ _t(
|
||||
"Please forget all messages I have sent when my account is deactivated " +
|
||||
"(<b>Warning:</b> this will cause future users to see an incomplete view " +
|
||||
"of conversations)",
|
||||
{},
|
||||
{ b: (sub) => <b>{ sub }</b> },
|
||||
) }
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<p>{ _t("To continue, please enter your password:") }</p>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={_t("password")}
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p>{ _t("Password") }:</p>
|
||||
<input
|
||||
type="password"
|
||||
onChange={this._onPasswordFieldChange}
|
||||
ref={(e) => {this._passwordField = e;}}
|
||||
className={passwordBoxClass}
|
||||
/>
|
||||
{ error }
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
|
|
@ -132,17 +132,17 @@ class SendCustomEvent extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
{ this.state.isStateEvent && this.textInput('stateKey', _t('State Key')) }
|
||||
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -219,15 +219,15 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
|
||||
return <div>
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
{ this.textInput('eventType', _t('Event Type')) }
|
||||
<br />
|
||||
|
||||
<div className="mx_UserSettings_profileLabelCell">
|
||||
<div className="mx_DevTools_inputLabelCell">
|
||||
<label htmlFor="evContent"> { _t('Event Content') } </label>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_TextInputDialog_input" cols="63" rows="5" />
|
||||
<textarea id="evContent" onChange={this._onChange} value={this.state.evContent} className="mx_DevTools_textarea" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_Dialog_buttons">
|
||||
|
@ -242,6 +242,9 @@ class SendAccountData extends GenericEditor {
|
|||
}
|
||||
}
|
||||
|
||||
const INITIAL_LOAD_TILES = 20;
|
||||
const LOAD_TILES_STEP_SIZE = 50;
|
||||
|
||||
class FilteredList extends React.Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
|
@ -249,31 +252,65 @@ class FilteredList extends React.Component {
|
|||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static filterChildren(children, query) {
|
||||
if (!query) return children;
|
||||
const lcQuery = query.toLowerCase();
|
||||
return children.filter((child) => child.key.toLowerCase().includes(lcQuery));
|
||||
}
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.onQuery = this.onQuery.bind(this);
|
||||
|
||||
this.state = {
|
||||
filteredChildren: FilteredList.filterChildren(this.props.children, this.props.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
};
|
||||
}
|
||||
|
||||
onQuery(ev) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.children === nextProps.children && this.props.query === nextProps.query) return;
|
||||
this.setState({
|
||||
filteredChildren: FilteredList.filterChildren(nextProps.children, nextProps.query),
|
||||
truncateAt: INITIAL_LOAD_TILES,
|
||||
});
|
||||
}
|
||||
|
||||
showAll = () => {
|
||||
this.setState({
|
||||
truncateAt: this.state.truncateAt + LOAD_TILES_STEP_SIZE,
|
||||
});
|
||||
};
|
||||
|
||||
createOverflowElement = (overflowCount: number, totalCount: number) => {
|
||||
return <button className="mx_DevTools_RoomStateExplorer_button" onClick={this.showAll}>
|
||||
{ _t("and %(count)s others...", { count: overflowCount }) }
|
||||
</button>;
|
||||
};
|
||||
|
||||
onQuery = (ev) => {
|
||||
if (this.props.onChange) this.props.onChange(ev.target.value);
|
||||
}
|
||||
};
|
||||
|
||||
filterChildren() {
|
||||
if (this.props.query) {
|
||||
const lowerQuery = this.props.query.toLowerCase();
|
||||
return this.props.children.filter((child) => child.key.toLowerCase().includes(lowerQuery));
|
||||
}
|
||||
return this.props.children;
|
||||
}
|
||||
getChildren = (start: number, end: number) => {
|
||||
return this.state.filteredChildren.slice(start, end);
|
||||
};
|
||||
|
||||
getChildCount = (): number => {
|
||||
return this.state.filteredChildren.length;
|
||||
};
|
||||
|
||||
render() {
|
||||
const TruncatedList = sdk.getComponent("elements.TruncatedList");
|
||||
return <div>
|
||||
<input size="64"
|
||||
onChange={this.onQuery}
|
||||
value={this.props.query}
|
||||
placeholder={_t('Filter results')}
|
||||
className="mx_TextInputDialog_input mx_DevTools_RoomStateExplorer_query" />
|
||||
{ this.filterChildren() }
|
||||
<TruncatedList getChildren={this.getChildren}
|
||||
getChildCount={this.getChildCount}
|
||||
truncateAt={this.state.truncateAt}
|
||||
createOverflowElement={this.createOverflowElement} />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -485,7 +522,7 @@ class AccountDataExplorer extends DevtoolsComponent {
|
|||
}
|
||||
|
||||
return <div className="mx_ViewSource">
|
||||
<div className="mx_Dialog_content">
|
||||
<div className="mx_DevTools_content">
|
||||
<SyntaxHighlight className="json">
|
||||
{ JSON.stringify(this.state.event.event, null, 2) }
|
||||
</SyntaxHighlight>
|
||||
|
|
|
@ -67,8 +67,10 @@ export default React.createClass({
|
|||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -36,7 +37,7 @@ export default React.createClass({
|
|||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
emailAddress: null,
|
||||
emailAddress: '',
|
||||
emailBusy: false,
|
||||
};
|
||||
},
|
||||
|
@ -127,6 +128,7 @@ export default React.createClass({
|
|||
const EditableText = sdk.getComponent('elements.EditableText');
|
||||
|
||||
const emailInput = this.state.emailBusy ? <Spinner /> : <EditableText
|
||||
initialValue={this.state.emailAddress}
|
||||
className="mx_SetEmailDialog_email_input"
|
||||
autoFocus="true"
|
||||
placeholder={_t("Email address")}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -79,15 +80,11 @@ export default React.createClass({
|
|||
Modal.createDialog(WarmFuzzy, {
|
||||
didSetEmail: res.didSetEmail,
|
||||
onFinished: () => {
|
||||
this._onContinueClicked();
|
||||
this.props.onFinished();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
_onContinueClicked: function() {
|
||||
this.props.onFinished(true);
|
||||
},
|
||||
|
||||
_onPasswordChangeError: function(err) {
|
||||
let errMsg = err.error || "";
|
||||
if (err.httpStatus === 403) {
|
||||
|
|
224
src/components/views/dialogs/ShareDialog.js
Normal file
224
src/components/views/dialogs/ShareDialog.js
Normal file
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room, User, Group, RoomMember, MatrixEvent} from 'matrix-js-sdk';
|
||||
import sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import QRCode from 'qrcode-react';
|
||||
import {makeEventPermalink, makeGroupPermalink, makeRoomPermalink, makeUserPermalink} from "../../../matrix-to";
|
||||
import * as ContextualMenu from "../../structures/ContextualMenu";
|
||||
|
||||
const socials = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
img: 'img/social/facebook.png',
|
||||
url: (url) => `https://www.facebook.com/sharer/sharer.php?u=${url}`,
|
||||
}, {
|
||||
name: 'Twitter',
|
||||
img: 'img/social/twitter-2.png',
|
||||
url: (url) => `https://twitter.com/home?status=${url}`,
|
||||
}, /* // icon missing
|
||||
name: 'Google Plus',
|
||||
img: 'img/social/',
|
||||
url: (url) => `https://plus.google.com/share?url=${url}`,
|
||||
},*/ {
|
||||
name: 'LinkedIn',
|
||||
img: 'img/social/linkedin.png',
|
||||
url: (url) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
|
||||
}, {
|
||||
name: 'Reddit',
|
||||
img: 'img/social/reddit.png',
|
||||
url: (url) => `http://www.reddit.com/submit?url=${url}`,
|
||||
}, {
|
||||
name: 'email',
|
||||
img: 'img/social/email-1.png',
|
||||
url: (url) => `mailto:?body=${url}`,
|
||||
},
|
||||
];
|
||||
|
||||
export default class ShareDialog extends React.Component {
|
||||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
target: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Room),
|
||||
PropTypes.instanceOf(User),
|
||||
PropTypes.instanceOf(Group),
|
||||
PropTypes.instanceOf(RoomMember),
|
||||
PropTypes.instanceOf(MatrixEvent),
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onCopyClick = this.onCopyClick.bind(this);
|
||||
this.onLinkSpecificEventCheckboxClick = this.onLinkSpecificEventCheckboxClick.bind(this);
|
||||
|
||||
this.state = {
|
||||
// MatrixEvent defaults to share linkSpecificEvent
|
||||
linkSpecificEvent: this.props.target instanceof MatrixEvent,
|
||||
};
|
||||
}
|
||||
|
||||
static _selectText(target) {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(target);
|
||||
|
||||
const selection = window.getSelection();
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
static onLinkClick(e) {
|
||||
e.preventDefault();
|
||||
const {target} = e;
|
||||
ShareDialog._selectText(target);
|
||||
}
|
||||
|
||||
onCopyClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
ShareDialog._selectText(this.refs.link);
|
||||
|
||||
let successful;
|
||||
try {
|
||||
successful = document.execCommand('copy');
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
}
|
||||
|
||||
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||
const buttonRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = buttonRect.right + window.pageXOffset;
|
||||
const y = (buttonRect.top + (buttonRect.height / 2) + window.pageYOffset) - 19;
|
||||
const {close} = ContextualMenu.createMenu(GenericTextContextMenu, {
|
||||
chevronOffset: 10,
|
||||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
}
|
||||
|
||||
onLinkSpecificEventCheckboxClick() {
|
||||
this.setState({
|
||||
linkSpecificEvent: !this.state.linkSpecificEvent,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let title;
|
||||
let matrixToUrl;
|
||||
|
||||
let checkbox;
|
||||
|
||||
if (this.props.target instanceof Room) {
|
||||
title = _t('Share Room');
|
||||
|
||||
const events = this.props.target.getLiveTimeline().getEvents();
|
||||
if (events.length > 0) {
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to most recent message') }
|
||||
</label>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.roomId, events[events.length - 1].getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.roomId);
|
||||
}
|
||||
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
|
||||
title = _t('Share User');
|
||||
matrixToUrl = makeUserPermalink(this.props.target.userId);
|
||||
} else if (this.props.target instanceof Group) {
|
||||
title = _t('Share Community');
|
||||
matrixToUrl = makeGroupPermalink(this.props.target.groupId);
|
||||
} else if (this.props.target instanceof MatrixEvent) {
|
||||
title = _t('Share Room Message');
|
||||
checkbox = <div>
|
||||
<input type="checkbox"
|
||||
id="mx_ShareDialog_checkbox"
|
||||
checked={this.state.linkSpecificEvent}
|
||||
onClick={this.onLinkSpecificEventCheckboxClick} />
|
||||
<label htmlFor="mx_ShareDialog_checkbox">
|
||||
{ _t('Link to selected message') }
|
||||
</label>
|
||||
</div>;
|
||||
|
||||
if (this.state.linkSpecificEvent) {
|
||||
matrixToUrl = makeEventPermalink(this.props.target.getRoomId(), this.props.target.getId());
|
||||
} else {
|
||||
matrixToUrl = makeRoomPermalink(this.props.target.getRoomId());
|
||||
}
|
||||
}
|
||||
|
||||
const encodedUrl = encodeURIComponent(matrixToUrl);
|
||||
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
return <BaseDialog title={title}
|
||||
className='mx_ShareDialog'
|
||||
contentId='mx_Dialog_content'
|
||||
onFinished={this.props.onFinished}
|
||||
>
|
||||
<div className="mx_ShareDialog_content">
|
||||
<div className="mx_ShareDialog_matrixto">
|
||||
<a ref="link"
|
||||
href={matrixToUrl}
|
||||
onClick={ShareDialog.onLinkClick}
|
||||
className="mx_ShareDialog_matrixto_link"
|
||||
>
|
||||
{ matrixToUrl }
|
||||
</a>
|
||||
<a href={matrixToUrl} className="mx_ShareDialog_matrixto_copy" onClick={this.onCopyClick}>
|
||||
{ _t('COPY') }
|
||||
<div> </div>
|
||||
</a>
|
||||
</div>
|
||||
{ checkbox }
|
||||
<hr />
|
||||
|
||||
<div className="mx_ShareDialog_split">
|
||||
<div className="mx_ShareDialog_qrcode_container">
|
||||
<QRCode value={matrixToUrl} size={256} logoWidth={48} logo="img/matrix-m.svg" />
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
href={social.url(encodedUrl)}
|
||||
className="mx_ShareDialog_social_icon"
|
||||
>
|
||||
<img src={social.img} alt={social.name} height={64} width={64} />
|
||||
</a>)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import url from 'url';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
|
||||
export default class AppPermission extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -19,7 +20,7 @@ export default class AppPermission extends React.Component {
|
|||
|
||||
const searchParams = new URLSearchParams(wurl.search);
|
||||
|
||||
if (this.isScalarWurl(wurl) && searchParams && searchParams.get('url')) {
|
||||
if (WidgetUtils.isScalarUrl(wurl) && searchParams && searchParams.get('url')) {
|
||||
curl = url.parse(searchParams.get('url'));
|
||||
if (curl) {
|
||||
curl.search = curl.query = "";
|
||||
|
@ -33,25 +34,16 @@ export default class AppPermission extends React.Component {
|
|||
return curlString;
|
||||
}
|
||||
|
||||
isScalarWurl(wurl) {
|
||||
if (wurl && wurl.hostname && (
|
||||
wurl.hostname === 'scalar.vector.im' ||
|
||||
wurl.hostname === 'scalar-staging.riot.im' ||
|
||||
wurl.hostname === 'scalar-develop.riot.im' ||
|
||||
wurl.hostname === 'demo.riot.im' ||
|
||||
wurl.hostname === 'localhost'
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
let e2eWarningText;
|
||||
if (this.props.isRoomEncrypted) {
|
||||
e2eWarningText =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('NOTE: Apps are not end-to-end encrypted') }</span>;
|
||||
}
|
||||
const cookieWarning =
|
||||
<span className='mx_AppPermissionWarningTextLabel'>
|
||||
{ _t('Warning: This widget might use cookies.') }
|
||||
</span>;
|
||||
return (
|
||||
<div className='mx_AppPermissionWarning'>
|
||||
<div className='mx_AppPermissionWarningImage'>
|
||||
|
@ -60,6 +52,7 @@ export default class AppPermission extends React.Component {
|
|||
<div className='mx_AppPermissionWarningText'>
|
||||
<span className='mx_AppPermissionWarningTextLabel'>{ _t('Do you want to load widget from URL:') }</span> <span className='mx_AppPermissionWarningTextURL'>{ this.state.curlBase }</span>
|
||||
{ e2eWarningText }
|
||||
{ cookieWarning }
|
||||
</div>
|
||||
<input
|
||||
className='mx_AppPermissionButton'
|
||||
|
|
|
@ -25,14 +25,13 @@ import PlatformPeg from '../../../PlatformPeg';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import WidgetMessaging from '../../../WidgetMessaging';
|
||||
import TintableSvgButton from './TintableSvgButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import Modal from '../../../Modal';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import AppPermission from './AppPermission';
|
||||
import AppWarning from './AppWarning';
|
||||
import MessageSpinner from './MessageSpinner';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import dis from '../../../dispatcher';
|
||||
|
||||
const ALLOWED_APP_URL_SCHEMES = ['https:', 'http:'];
|
||||
|
@ -55,6 +54,7 @@ export default class AppTile extends React.Component {
|
|||
this._grantWidgetPermission = this._grantWidgetPermission.bind(this);
|
||||
this._revokeWidgetPermission = this._revokeWidgetPermission.bind(this);
|
||||
this._onPopoutWidgetClick = this._onPopoutWidgetClick.bind(this);
|
||||
this._onReloadWidgetClick = this._onReloadWidgetClick.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -120,30 +120,6 @@ export default class AppTile extends React.Component {
|
|||
return u.format();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if specified url is a scalar URL, typically https://scalar.vector.im/api
|
||||
* @param {[type]} url URL to check
|
||||
* @return {Boolean} True if specified URL is a scalar URL
|
||||
*/
|
||||
isScalarUrl(url) {
|
||||
if (!url) {
|
||||
console.error('Scalar URL check failed. No URL specified');
|
||||
return false;
|
||||
}
|
||||
|
||||
let scalarUrls = SdkConfig.get().integrations_widgets_urls;
|
||||
if (!scalarUrls || scalarUrls.length == 0) {
|
||||
scalarUrls = [SdkConfig.get().integrations_rest_url];
|
||||
}
|
||||
|
||||
for (let i = 0; i < scalarUrls.length; i++) {
|
||||
if (url.startsWith(scalarUrls[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
isMixedContent() {
|
||||
const parentContentProtocol = window.location.protocol;
|
||||
const u = url.parse(this.props.url);
|
||||
|
@ -199,7 +175,7 @@ export default class AppTile extends React.Component {
|
|||
setScalarToken() {
|
||||
this.setState({initialising: true});
|
||||
|
||||
if (!this.isScalarUrl(this.props.url)) {
|
||||
if (!WidgetUtils.isScalarUrl(this.props.url)) {
|
||||
console.warn('Non-scalar widget, not setting scalar token!', url);
|
||||
this.setState({
|
||||
error: null,
|
||||
|
@ -269,7 +245,12 @@ export default class AppTile extends React.Component {
|
|||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
if (!this.state.widgetUrl.startsWith(event.origin)) {
|
||||
const widgetUrlObj = url.parse(this.state.widgetUrl);
|
||||
const eventOrigin = url.parse(event.origin);
|
||||
if (
|
||||
eventOrigin.protocol !== widgetUrlObj.protocol ||
|
||||
eventOrigin.host !== widgetUrlObj.host
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -338,10 +319,9 @@ export default class AppTile extends React.Component {
|
|||
return;
|
||||
}
|
||||
this.setState({deleting: true});
|
||||
MatrixClientPeg.get().sendStateEvent(
|
||||
|
||||
WidgetUtils.setRoomWidget(
|
||||
this.props.room.roomId,
|
||||
'im.vector.modular.widgets',
|
||||
{}, // empty content
|
||||
this.props.id,
|
||||
).catch((e) => {
|
||||
console.error('Failed to delete widget', e);
|
||||
|
@ -519,6 +499,11 @@ export default class AppTile extends React.Component {
|
|||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener noreferrer'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick(e) {
|
||||
// Reload iframe in this way to avoid cross-origin restrictions
|
||||
this.refs.appFrame.src = this.refs.appFrame.src;
|
||||
}
|
||||
|
||||
render() {
|
||||
let appTileBody;
|
||||
|
||||
|
@ -606,6 +591,7 @@ export default class AppTile extends React.Component {
|
|||
const showPictureSnapshotButton = this._hasCapability('m.capability.screenshot') && this.props.show;
|
||||
const showPictureSnapshotIcon = 'img/camera_green.svg';
|
||||
const popoutWidgetIcon = 'img/button-new-window.svg';
|
||||
const reloadWidgetIcon = 'img/button-refresh.svg';
|
||||
const windowStateIcon = (this.props.show ? 'img/minimize.svg' : 'img/maximize.svg');
|
||||
|
||||
return (
|
||||
|
@ -624,6 +610,16 @@ export default class AppTile extends React.Component {
|
|||
{ this.props.showTitle && this._getTileTitle() }
|
||||
</span>
|
||||
<span className="mx_AppTileMenuBarWidgets">
|
||||
{ /* Reload widget */ }
|
||||
{ this.props.showReload && <TintableSvgButton
|
||||
src={reloadWidgetIcon}
|
||||
className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
|
||||
title={_t('Reload widget')}
|
||||
onClick={this._onReloadWidgetClick}
|
||||
width="10"
|
||||
height="10"
|
||||
/> }
|
||||
|
||||
{ /* Popout widget */ }
|
||||
{ this.props.showPopout && <TintableSvgButton
|
||||
src={popoutWidgetIcon}
|
||||
|
@ -707,6 +703,11 @@ AppTile.propTypes = {
|
|||
showDelete: PropTypes.bool,
|
||||
// Optionally hide the popout widget icon
|
||||
showPopout: PropTypes.bool,
|
||||
// Optionally show the reload widget icon
|
||||
// This is not currently intended for use with production widgets. However
|
||||
// it can be useful when developing persistent widgets in order to avoid
|
||||
// having to reload all of riot to get new widget content.
|
||||
showReload: PropTypes.bool,
|
||||
// Widget capabilities to allow by default (without user confirmation)
|
||||
// NOTE -- Use with caution. This is intended to aid better integration / UX
|
||||
// basic widget capabilities, e.g. injecting sticker message events.
|
||||
|
@ -726,6 +727,7 @@ AppTile.defaultProps = {
|
|||
showMinimise: true,
|
||||
showDelete: true,
|
||||
showPopout: true,
|
||||
showReload: false,
|
||||
handleMinimisePointerEvents: false,
|
||||
whitelistCapabilities: [],
|
||||
userWidget: false,
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
|
||||
import TagTile from './TagTile';
|
||||
|
||||
import React from 'react';
|
||||
import { Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
export default function DNDTagTile(props) {
|
||||
|
|
|
@ -29,6 +29,9 @@ module.exports = React.createClass({
|
|||
// The primary button which is styled differently and has default focus.
|
||||
primaryButton: PropTypes.node.isRequired,
|
||||
|
||||
// A node to insert into the cancel button instead of default "Cancel"
|
||||
cancelButton: PropTypes.node,
|
||||
|
||||
// onClick handler for the primary button.
|
||||
onPrimaryButtonClick: PropTypes.func.isRequired,
|
||||
|
||||
|
@ -60,9 +63,9 @@ module.exports = React.createClass({
|
|||
primaryButtonClassName += " " + this.props.primaryButtonClass;
|
||||
}
|
||||
let cancelButton;
|
||||
if (this.props.hasCancel) {
|
||||
if (this.props.cancelButton || this.props.hasCancel) {
|
||||
cancelButton = <button onClick={this._onCancelClick} disabled={this.props.disabled}>
|
||||
{ _t("Cancel") }
|
||||
{ this.props.cancelButton || _t("Cancel") }
|
||||
</button>;
|
||||
}
|
||||
return (
|
||||
|
|
|
@ -139,8 +139,11 @@ module.exports = React.createClass({
|
|||
</div>
|
||||
{ editableItems }
|
||||
{ this.props.canEdit ?
|
||||
// This is slightly evil; we want a new instance of
|
||||
// EditableItem when the list grows. To make sure it's
|
||||
// reset to its initial state.
|
||||
<EditableItem
|
||||
key={-1}
|
||||
key={editableItems.length}
|
||||
initialValue={this.props.newItem}
|
||||
onAdd={this.onItemAdded}
|
||||
onChange={this.onNewItemChanged}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,15 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const KEY_TAB = 9;
|
||||
const KEY_SHIFT = 16;
|
||||
const KEY_WINDOWS = 91;
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'EditableText',
|
||||
|
||||
|
@ -66,9 +61,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
if (nextProps.initialValue !== this.props.initialValue ||
|
||||
nextProps.initialValue !== this.value
|
||||
) {
|
||||
if (nextProps.initialValue !== this.props.initialValue) {
|
||||
this.value = nextProps.initialValue;
|
||||
if (this.refs.editable_div) {
|
||||
this.showPlaceholder(!this.value);
|
||||
|
@ -139,7 +132,7 @@ module.exports = React.createClass({
|
|||
this.showPlaceholder(false);
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
@ -156,9 +149,9 @@ module.exports = React.createClass({
|
|||
this.value = ev.target.textContent;
|
||||
}
|
||||
|
||||
if (ev.key == "Enter") {
|
||||
if (ev.key === "Enter") {
|
||||
this.onFinish(ev);
|
||||
} else if (ev.key == "Escape") {
|
||||
} else if (ev.key === "Escape") {
|
||||
this.cancelEdit();
|
||||
}
|
||||
|
||||
|
@ -193,7 +186,7 @@ module.exports = React.createClass({
|
|||
const submit = (ev.key === "Enter") || shouldSubmit;
|
||||
this.setState({
|
||||
phase: this.Phases.Display,
|
||||
}, function() {
|
||||
}, () => {
|
||||
if (this.value !== this.props.initialValue) {
|
||||
self.onValueChanged(submit);
|
||||
}
|
||||
|
@ -204,23 +197,35 @@ module.exports = React.createClass({
|
|||
const sel = window.getSelection();
|
||||
sel.removeAllRanges();
|
||||
|
||||
if (this.props.blurToCancel) {this.cancelEdit();} else {this.onFinish(ev, this.props.blurToSubmit);}
|
||||
if (this.props.blurToCancel) {
|
||||
this.cancelEdit();
|
||||
} else {
|
||||
this.onFinish(ev, this.props.blurToSubmit);
|
||||
}
|
||||
|
||||
this.showPlaceholder(!this.value);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
let editable_el;
|
||||
const {className, editable, initialValue, label, labelClassName} = this.props;
|
||||
let editableEl;
|
||||
|
||||
if (!this.props.editable || (this.state.phase == this.Phases.Display && (this.props.label || this.props.labelClassName) && !this.value)) {
|
||||
if (!editable || (this.state.phase === this.Phases.Display && (label || labelClassName) && !this.value)) {
|
||||
// show the label
|
||||
editable_el = <div className={this.props.className + " " + this.props.labelClassName} onClick={this.onClickDiv}>{ this.props.label || this.props.initialValue }</div>;
|
||||
editableEl = <div className={className + " " + labelClassName} onClick={this.onClickDiv}>
|
||||
{ label || initialValue }
|
||||
</div>;
|
||||
} else {
|
||||
// show the content editable div, but manually manage its contents as react and contentEditable don't play nice together
|
||||
editable_el = <div ref="editable_div" contentEditable="true" className={this.props.className}
|
||||
onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur}></div>;
|
||||
editableEl = <div ref="editable_div"
|
||||
contentEditable={true}
|
||||
className={className}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur} />;
|
||||
}
|
||||
|
||||
return editable_el;
|
||||
return editableEl;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -16,28 +16,24 @@ limitations under the License.
|
|||
|
||||
const React = require('react');
|
||||
const ReactDOM = require('react-dom');
|
||||
const PropTypes = require('prop-types');
|
||||
|
||||
// Shamelessly ripped off Modal.js. There's probably a better way
|
||||
// of doing reusable widgets like dialog boxes & menus where we go and
|
||||
// pass in a custom control as the actual body.
|
||||
|
||||
const ContainerId = "mx_PersistedElement";
|
||||
|
||||
function getOrCreateContainer() {
|
||||
let container = document.getElementById(ContainerId);
|
||||
function getOrCreateContainer(containerId) {
|
||||
let container = document.getElementById(containerId);
|
||||
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.id = ContainerId;
|
||||
container.id = containerId;
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// Greater than that of the ContextualMenu
|
||||
const PE_Z_INDEX = 3000;
|
||||
|
||||
/*
|
||||
* Class of component that renders its children in a separate ReactDOM virtual tree
|
||||
* in a container element appended to document.body.
|
||||
|
@ -50,6 +46,14 @@ const PE_Z_INDEX = 3000;
|
|||
* bounding rect as the parent of PE.
|
||||
*/
|
||||
export default class PersistedElement extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
// Unique identifier for this PersistedElement instance
|
||||
// Any PersistedElements with the same persistKey will use
|
||||
// the same DOM container.
|
||||
persistKey: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.collectChildContainer = this.collectChildContainer.bind(this);
|
||||
|
@ -97,18 +101,16 @@ export default class PersistedElement extends React.Component {
|
|||
left: parentRect.left + 'px',
|
||||
width: parentRect.width + 'px',
|
||||
height: parentRect.height + 'px',
|
||||
zIndex: PE_Z_INDEX,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = <div ref={this.collectChild}>
|
||||
const content = <div ref={this.collectChild} style={this.props.style}>
|
||||
{this.props.children}
|
||||
</div>;
|
||||
|
||||
ReactDOM.render(content, getOrCreateContainer());
|
||||
ReactDOM.render(content, getOrCreateContainer('mx_persistedElement_'+this.props.persistKey));
|
||||
|
||||
return <div ref={this.collectChildContainer}></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -22,12 +23,13 @@ import PropTypes from 'prop-types';
|
|||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { MATRIXTO_URL_PATTERN } from '../../../linkify-matrix';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
|
||||
// For URLs of matrix.to links in the timeline which have been reformatted by
|
||||
// HttpUtils transformTags to relative links. This excludes event URLs (with `[^\/]*`)
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room)\/(([\#\!\@\+])[^\/]*)$/;
|
||||
const REGEX_LOCAL_MATRIXTO = /^#\/(?:user|room|group)\/(([#!@+])[^\/]*)$/;
|
||||
|
||||
const Pill = React.createClass({
|
||||
statics: {
|
||||
|
@ -45,6 +47,7 @@ const Pill = React.createClass({
|
|||
},
|
||||
TYPE_USER_MENTION: 'TYPE_USER_MENTION',
|
||||
TYPE_ROOM_MENTION: 'TYPE_ROOM_MENTION',
|
||||
TYPE_GROUP_MENTION: 'TYPE_GROUP_MENTION',
|
||||
TYPE_AT_ROOM_MENTION: 'TYPE_AT_ROOM_MENTION', // '@room' mention
|
||||
},
|
||||
|
||||
|
@ -83,12 +86,14 @@ const Pill = React.createClass({
|
|||
|
||||
// The member related to the user pill
|
||||
member: null,
|
||||
// The group related to the group pill
|
||||
group: null,
|
||||
// The room related to the room pill
|
||||
room: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
let regex = REGEX_MATRIXTO;
|
||||
if (nextProps.inMessage) {
|
||||
regex = REGEX_LOCAL_MATRIXTO;
|
||||
|
@ -111,9 +116,11 @@ const Pill = React.createClass({
|
|||
'@': Pill.TYPE_USER_MENTION,
|
||||
'#': Pill.TYPE_ROOM_MENTION,
|
||||
'!': Pill.TYPE_ROOM_MENTION,
|
||||
'+': Pill.TYPE_GROUP_MENTION,
|
||||
}[prefix];
|
||||
|
||||
let member;
|
||||
let group;
|
||||
let room;
|
||||
switch (pillType) {
|
||||
case Pill.TYPE_AT_ROOM_MENTION: {
|
||||
|
@ -142,8 +149,21 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
try {
|
||||
group = await FlairStore.getGroupProfileCached(cli, resourceId);
|
||||
} catch (e) { // if FlairStore failed, fall back to just groupId
|
||||
group = {
|
||||
groupId: resourceId,
|
||||
avatarUrl: null,
|
||||
name: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
this.setState({resourceId, pillType, member, room});
|
||||
this.setState({resourceId, pillType, member, group, room});
|
||||
},
|
||||
|
||||
componentWillMount() {
|
||||
|
@ -181,6 +201,7 @@ const Pill = React.createClass({
|
|||
});
|
||||
},
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('views.avatars.BaseAvatar');
|
||||
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
|
@ -231,6 +252,20 @@ const Pill = React.createClass({
|
|||
}
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
if (this.state.group) {
|
||||
const {avatarUrl, groupId, name} = this.state.group;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
linkText = groupId;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||
}
|
||||
pillClass = 'mx_GroupPill';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const classes = classNames(pillClass, {
|
||||
|
|
|
@ -160,7 +160,7 @@ export default class ReplyThread extends React.Component {
|
|||
}
|
||||
|
||||
static makeThread(parentEv, onWidgetLoad, ref) {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_rich_quoting") || !ReplyThread.getParentEventId(parentEv)) {
|
||||
if (!ReplyThread.getParentEventId(parentEv)) {
|
||||
return <div />;
|
||||
}
|
||||
return <ReplyThread parentEv={parentEv} onWidgetLoad={onWidgetLoad} ref={ref} />;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd.
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -103,14 +104,27 @@ export default React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
_openContextMenu: function(x, y, chevronOffset) {
|
||||
// Hide the (...) immediately
|
||||
this.setState({ hover: false });
|
||||
|
||||
const TagTileContextMenu = sdk.getComponent('context_menus.TagTileContextMenu');
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextButtonClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
|
@ -119,17 +133,14 @@ export default React.createClass({
|
|||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(TagTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
tag: this.props.tag,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
this._openContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._openContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onMouseOver: function() {
|
||||
|
@ -164,7 +175,7 @@ export default React.createClass({
|
|||
<div className="mx_TagTile_context_button" onClick={this.onContextButtonClick}>
|
||||
{ "\u00B7\u00B7\u00B7" }
|
||||
</div> : <div />;
|
||||
return <AccessibleButton className={className} onClick={this.onClick}>
|
||||
return <AccessibleButton className={className} onClick={this.onClick} onContextMenu={this.onContextMenu}>
|
||||
<div className="mx_TagTile_avatar" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut}>
|
||||
<BaseAvatar
|
||||
name={name}
|
||||
|
|
|
@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
|
|||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import sdk from '../../../index';
|
||||
import Analytics from '../../../Analytics';
|
||||
|
||||
export default class CookieBar extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -29,6 +30,10 @@ export default class CookieBar extends React.Component {
|
|||
super();
|
||||
}
|
||||
|
||||
onUsageDataClicked() {
|
||||
Analytics.showDetailsModal();
|
||||
}
|
||||
|
||||
onAccept() {
|
||||
dis.dispatch({
|
||||
action: 'accept_cookies',
|
||||
|
@ -49,11 +54,18 @@ export default class CookieBar extends React.Component {
|
|||
<img className="mx_MatrixToolbar_warning" src="img/warning.svg" width="24" height="23" alt="Warning" />
|
||||
<div className="mx_MatrixToolbar_content">
|
||||
{ this.props.policyUrl ? _t(
|
||||
"Help improve Riot by sending usage data? " +
|
||||
"This will use a cookie. " +
|
||||
"(See our <PolicyLink>cookie and privacy policies</PolicyLink>).",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie " +
|
||||
"(please see our <PolicyLink>Cookie Policy</PolicyLink>).",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
href="javascript:;"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
// XXX: We need to link to the page that explains our cookies
|
||||
'PolicyLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
|
@ -64,10 +76,23 @@ export default class CookieBar extends React.Component {
|
|||
</a>
|
||||
,
|
||||
},
|
||||
) : _t("Help improve Riot by sending usage data? This will use a cookie.") }
|
||||
) : _t(
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. " +
|
||||
"This will use a cookie.",
|
||||
{},
|
||||
{
|
||||
'UsageDataLink': (sub) => <a
|
||||
className="mx_MatrixToolbar_link"
|
||||
href="javascript:;"
|
||||
onClick={this.onUsageDataClicked}
|
||||
>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</div>
|
||||
<AccessibleButton element='button' className="mx_MatrixToolbar_action" onClick={this.onAccept}>
|
||||
{ _t("Yes please") }
|
||||
{ _t("Yes, I want to help!") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton className="mx_MatrixToolbar_close" onClick={this.onReject}>
|
||||
<img src="img/cancel.svg" width="18" height="18" />
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,28 +15,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import Modal from '../../../Modal';
|
||||
import dis from '../../../dispatcher';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
export default React.createClass({
|
||||
onUpdateClicked: function() {
|
||||
const SetPasswordDialog = sdk.getComponent('dialogs.SetPasswordDialog');
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog, {
|
||||
onFinished: (passwordChanged) => {
|
||||
if (!passwordChanged) {
|
||||
return;
|
||||
}
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({
|
||||
action: 'password_changed',
|
||||
});
|
||||
},
|
||||
});
|
||||
Modal.createTrackedDialog('Set Password Dialog', 'Password Nag Bar', SetPasswordDialog);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,6 +21,9 @@ import { MatrixClient } from 'matrix-js-sdk';
|
|||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import classNames from 'classnames';
|
||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||
import {createMenu} from "../../structures/ContextualMenu";
|
||||
|
||||
export default React.createClass({
|
||||
displayName: 'GroupInviteTile',
|
||||
|
@ -32,6 +36,15 @@ export default React.createClass({
|
|||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return ({
|
||||
hover: false,
|
||||
badgeHover: false,
|
||||
menuDisplayed: false,
|
||||
selected: this.props.group.groupId === null, // XXX: this needs linking to LoggedInView/GroupView state
|
||||
});
|
||||
},
|
||||
|
||||
onClick: function(e) {
|
||||
dis.dispatch({
|
||||
action: 'view_group',
|
||||
|
@ -39,6 +52,69 @@ export default React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMouseEnter: function() {
|
||||
const state = {hover: true};
|
||||
// Only allow non-guests to access the context menu
|
||||
if (!this.context.matrixClient.isGuest()) {
|
||||
state.badgeHover = true;
|
||||
}
|
||||
this.setState(state);
|
||||
},
|
||||
|
||||
onMouseLeave: function() {
|
||||
this.setState({
|
||||
badgeHover: false,
|
||||
hover: false,
|
||||
});
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const GroupInviteTileContextMenu = sdk.getComponent('context_menus.GroupInviteTileContextMenu');
|
||||
|
||||
createMenu(GroupInviteTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
group: this.props.group,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
|
@ -49,19 +125,40 @@ export default React.createClass({
|
|||
|
||||
const av = <BaseAvatar name={groupName} width={24} height={24} url={httpAvatarUrl} />;
|
||||
|
||||
const label = <EmojiText
|
||||
element="div"
|
||||
title={this.props.group.groupId}
|
||||
className="mx_RoomTile_name mx_RoomTile_badgeShown"
|
||||
dir="auto"
|
||||
>
|
||||
const nameClasses = classNames('mx_RoomTile_name mx_RoomTile_invite mx_RoomTile_badgeShown', {
|
||||
'mx_RoomTile_badgeShown': this.state.badgeHover || this.state.menuDisplayed,
|
||||
});
|
||||
|
||||
const label = <EmojiText element="div" title={this.props.group.groupId} className={nameClasses} dir="auto">
|
||||
{ groupName }
|
||||
</EmojiText>;
|
||||
|
||||
const badge = <div className="mx_RoomSubList_badge mx_RoomSubList_badgeHighlight">!</div>;
|
||||
const badgeEllipsis = this.state.badgeHover || this.state.menuDisplayed;
|
||||
const badgeClasses = classNames('mx_RoomTile_badge mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_badgeButton': badgeEllipsis,
|
||||
});
|
||||
|
||||
const badgeContent = badgeEllipsis ? '\u00B7\u00B7\u00B7' : '!';
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
let tooltip;
|
||||
if (this.props.collapsed && this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={groupName} dir="auto" />;
|
||||
}
|
||||
|
||||
const classes = classNames('mx_RoomTile mx_RoomTile_highlight', {
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_selected': this.state.selected,
|
||||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton className="mx_RoomTile mx_RoomTile_highlight" onClick={this.onClick}>
|
||||
<AccessibleButton className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className="mx_RoomTile_avatar">
|
||||
{ av }
|
||||
</div>
|
||||
|
@ -69,6 +166,7 @@ export default React.createClass({
|
|||
{ label }
|
||||
{ badge }
|
||||
</div>
|
||||
{ tooltip }
|
||||
</AccessibleButton>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -187,7 +187,7 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this._onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
|
|
|
@ -69,7 +69,7 @@ export default React.createClass({
|
|||
render() {
|
||||
const GroupTile = sdk.getComponent('groups.GroupTile');
|
||||
const input = <input type="checkbox"
|
||||
onClick={this._onPublicityToggle}
|
||||
onChange={this._onPublicityToggle}
|
||||
checked={this.state.isGroupPublicised}
|
||||
/>;
|
||||
const labelText = !this.state.ready ? _t("Loading...") :
|
||||
|
|
|
@ -22,6 +22,7 @@ import sdk from '../../../index';
|
|||
import dis from '../../../dispatcher';
|
||||
import FlairStore from '../../../stores/FlairStore';
|
||||
|
||||
function nop() {}
|
||||
|
||||
const GroupTile = React.createClass({
|
||||
displayName: 'GroupTile',
|
||||
|
@ -81,7 +82,7 @@ const GroupTile = React.createClass({
|
|||
) : null;
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown}>
|
||||
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
|
||||
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
|
||||
{ (droppableProvided, droppableSnapshot) => (
|
||||
<div ref={droppableProvided.innerRef}>
|
||||
|
|
|
@ -28,6 +28,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
*/
|
||||
class PasswordLogin extends React.Component {
|
||||
static defaultProps = {
|
||||
onError: function() {},
|
||||
onUsernameChanged: function() {},
|
||||
onPasswordChanged: function() {},
|
||||
onPhoneCountryChanged: function() {},
|
||||
|
@ -56,33 +57,64 @@ class PasswordLogin extends React.Component {
|
|||
this.onPhoneCountryChanged = this.onPhoneCountryChanged.bind(this);
|
||||
this.onPhoneNumberChanged = this.onPhoneNumberChanged.bind(this);
|
||||
this.onPasswordChanged = this.onPasswordChanged.bind(this);
|
||||
this.isLoginEmpty = this.isLoginEmpty.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this._passwordField = null;
|
||||
this._loginField = null;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.loginIncorrect && nextProps.loginIncorrect) {
|
||||
field_input_incorrect(this._passwordField);
|
||||
field_input_incorrect(this.isLoginEmpty() ? this._loginField : this._passwordField);
|
||||
}
|
||||
}
|
||||
|
||||
onSubmitForm(ev) {
|
||||
ev.preventDefault();
|
||||
if (this.state.loginType === PasswordLogin.LOGIN_FIELD_PHONE) {
|
||||
this.props.onSubmit(
|
||||
'', // XXX: Synapse breaks if you send null here:
|
||||
this.state.phoneCountry,
|
||||
this.state.phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
|
||||
let username = ''; // XXX: Synapse breaks if you send null here:
|
||||
let phoneCountry = null;
|
||||
let phoneNumber = null;
|
||||
let error;
|
||||
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The email field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
username = this.state.username;
|
||||
if (!username) {
|
||||
error = _t('The user name field must not be blank.');
|
||||
}
|
||||
break;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
phoneCountry = this.state.phoneCountry;
|
||||
phoneNumber = this.state.phoneNumber;
|
||||
if (!phoneNumber) {
|
||||
error = _t('The phone number field must not be blank.');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
this.props.onError(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.password) {
|
||||
this.props.onError(_t('The password field must not be blank.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmit(
|
||||
this.state.username,
|
||||
null,
|
||||
null,
|
||||
username,
|
||||
phoneCountry,
|
||||
phoneNumber,
|
||||
this.state.password,
|
||||
);
|
||||
}
|
||||
|
@ -93,6 +125,7 @@ class PasswordLogin extends React.Component {
|
|||
}
|
||||
|
||||
onLoginTypeChange(loginType) {
|
||||
this.props.onError(null); // send a null error to clear any error messages
|
||||
this.setState({
|
||||
loginType: loginType,
|
||||
username: "", // Reset because email and username use the same state
|
||||
|
@ -126,8 +159,10 @@ class PasswordLogin extends React.Component {
|
|||
switch (loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
classes.mx_Login_email = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="email_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -139,8 +174,10 @@ class PasswordLogin extends React.Component {
|
|||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
classes.mx_Login_username = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.username;
|
||||
return <input
|
||||
className={classNames(classes)}
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="username_input"
|
||||
type="text"
|
||||
name="username" // make it a little easier for browser's remember-password
|
||||
|
@ -153,14 +190,14 @@ class PasswordLogin extends React.Component {
|
|||
autoFocus
|
||||
disabled={disabled}
|
||||
/>;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE: {
|
||||
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
|
||||
classes.mx_Login_phoneNumberField = true;
|
||||
classes.mx_Login_field_has_prefix = true;
|
||||
classes.error = this.props.loginIncorrect && !this.state.phoneNumber;
|
||||
return <div className="mx_Login_phoneSection">
|
||||
<CountryDropdown
|
||||
className="mx_Login_phoneCountry mx_Login_field_prefix"
|
||||
ref="phone_country"
|
||||
onOptionChange={this.onPhoneCountryChanged}
|
||||
value={this.state.phoneCountry}
|
||||
isSmall={true}
|
||||
|
@ -169,7 +206,7 @@ class PasswordLogin extends React.Component {
|
|||
/>
|
||||
<input
|
||||
className={classNames(classes)}
|
||||
ref="phoneNumber"
|
||||
ref={(e) => {this._loginField = e;}}
|
||||
key="phone_input"
|
||||
type="text"
|
||||
name="phoneNumber"
|
||||
|
@ -180,6 +217,17 @@ class PasswordLogin extends React.Component {
|
|||
disabled={disabled}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isLoginEmpty() {
|
||||
switch (this.state.loginType) {
|
||||
case PasswordLogin.LOGIN_FIELD_EMAIL:
|
||||
case PasswordLogin.LOGIN_FIELD_MXID:
|
||||
return !this.state.username;
|
||||
case PasswordLogin.LOGIN_FIELD_PHONE:
|
||||
return !this.state.phoneCountry || !this.state.phoneNumber;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +255,7 @@ class PasswordLogin extends React.Component {
|
|||
const pwFieldClass = classNames({
|
||||
mx_Login_field: true,
|
||||
mx_Login_field_disabled: matrixIdText === '',
|
||||
error: this.props.loginIncorrect,
|
||||
error: this.props.loginIncorrect && !this.isLoginEmpty(), // only error password if error isn't top field
|
||||
});
|
||||
|
||||
const Dropdown = sdk.getComponent('elements.Dropdown');
|
||||
|
@ -258,6 +306,7 @@ PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone";
|
|||
|
||||
PasswordLogin.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired, // fn(username, password)
|
||||
onError: PropTypes.func,
|
||||
onForgotPasswordClick: PropTypes.func, // fn()
|
||||
initialUsername: PropTypes.string,
|
||||
initialPhoneCountry: PropTypes.string,
|
||||
|
|
|
@ -327,6 +327,7 @@ module.exports = React.createClass({
|
|||
// will have the correct name when the user tries to download it.
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
rel: "noopener",
|
||||
target: "_blank",
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
}, "*");
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,48 +16,43 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
|
||||
import MFileBody from './MFileBody';
|
||||
import ImageUtils from '../../../ImageUtils';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
import dis from '../../../dispatcher';
|
||||
import { decryptFile } from '../../../utils/DecryptFile';
|
||||
import Promise from 'bluebird';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
export default class extends React.Component {
|
||||
displayName: 'MImageBody'
|
||||
|
||||
export default class MImageBody extends React.Component {
|
||||
static propTypes = {
|
||||
/* the MatrixEvent to show */
|
||||
mxEvent: PropTypes.object.isRequired,
|
||||
|
||||
/* called when the image has loaded */
|
||||
onWidgetLoad: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
/* the maximum image height to use */
|
||||
maxImageHeight: PropTypes.number,
|
||||
};
|
||||
|
||||
static contextTypes = {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAction = this.onAction.bind(this);
|
||||
this.onImageError = this.onImageError.bind(this);
|
||||
this.onImageLoad = this.onImageLoad.bind(this);
|
||||
this.onImageEnter = this.onImageEnter.bind(this);
|
||||
this.onImageLeave = this.onImageLeave.bind(this);
|
||||
this.onClientSync = this.onClientSync.bind(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
this.fixupHeight = this.fixupHeight.bind(this);
|
||||
this._isGif = this._isGif.bind(this);
|
||||
|
||||
this.state = {
|
||||
|
@ -65,6 +61,9 @@ export default class extends React.Component {
|
|||
decryptedBlob: null,
|
||||
error: null,
|
||||
imgError: false,
|
||||
imgLoaded: false,
|
||||
loadedImageDimensions: null,
|
||||
hover: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -88,7 +87,7 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onClick(ev) {
|
||||
if (ev.button == 0 && !ev.metaKey) {
|
||||
if (ev.button === 0 && !ev.metaKey) {
|
||||
ev.preventDefault();
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const httpUrl = this._getContentUrl();
|
||||
|
@ -119,6 +118,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageEnter(e) {
|
||||
this.setState({ hover: true });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -127,6 +128,8 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
onImageLeave(e) {
|
||||
this.setState({ hover: false });
|
||||
|
||||
if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
return;
|
||||
}
|
||||
|
@ -142,6 +145,16 @@ export default class extends React.Component {
|
|||
|
||||
onImageLoad() {
|
||||
this.props.onWidgetLoad();
|
||||
|
||||
let loadedImageDimensions;
|
||||
|
||||
if (this.refs.image) {
|
||||
const { naturalWidth, naturalHeight } = this.refs.image;
|
||||
|
||||
loadedImageDimensions = { naturalWidth, naturalHeight };
|
||||
}
|
||||
|
||||
this.setState({ imgLoaded: true, loadedImageDimensions });
|
||||
}
|
||||
|
||||
_getContentUrl() {
|
||||
|
@ -161,9 +174,7 @@ export default class extends React.Component {
|
|||
return this.state.decryptedThumbnailUrl;
|
||||
}
|
||||
return this.state.decryptedUrl;
|
||||
} else if (content.info &&
|
||||
content.info.mimetype == "image/svg+xml" &&
|
||||
content.info.thumbnail_url) {
|
||||
} else if (content.info && content.info.mimetype === "image/svg+xml" && content.info.thumbnail_url) {
|
||||
// special case to return client-generated thumbnails for SVGs, if any,
|
||||
// given we deliberately don't thumbnail them serverside to prevent
|
||||
// billion lol attacks and similar
|
||||
|
@ -176,11 +187,10 @@ export default class extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
const content = this.props.mxEvent.getContent();
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
let thumbnailPromise = Promise.resolve(null);
|
||||
if (content.info.thumbnail_file) {
|
||||
if (content.info && content.info.thumbnail_file) {
|
||||
thumbnailPromise = decryptFile(
|
||||
content.info.thumbnail_file,
|
||||
).then(function(blob) {
|
||||
|
@ -207,7 +217,6 @@ export default class extends React.Component {
|
|||
});
|
||||
}).done();
|
||||
}
|
||||
this.fixupHeight();
|
||||
this._afterComponentDidMount();
|
||||
}
|
||||
|
||||
|
@ -218,7 +227,6 @@ export default class extends React.Component {
|
|||
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.context.matrixClient.removeListener('sync', this.onClientSync);
|
||||
this._afterComponentWillUnmount();
|
||||
|
||||
|
@ -235,59 +243,113 @@ export default class extends React.Component {
|
|||
_afterComponentWillUnmount() {
|
||||
}
|
||||
|
||||
onAction(payload) {
|
||||
if (payload.action === "timeline_resize") {
|
||||
this.fixupHeight();
|
||||
}
|
||||
}
|
||||
|
||||
fixupHeight() {
|
||||
if (!this.refs.image) {
|
||||
console.warn(`Refusing to fix up height on ${this.displayName} with no image element`);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const timelineWidth = this.refs.body.offsetWidth;
|
||||
const maxHeight = 600; // let images take up as much width as they can so long as the height doesn't exceed 600px.
|
||||
// the alternative here would be 600*timelineWidth/800; to scale them down to fit inside a 4:3 bounding box
|
||||
|
||||
// FIXME: this will break on clientside generated thumbnails (as per e2e rooms)
|
||||
// which may well be much smaller than the 800x600 bounding box.
|
||||
|
||||
// FIXME: It will also break really badly for images with broken or missing thumbnails
|
||||
|
||||
// FIXME: Because we don't know what size of thumbnail the server's actually going to send
|
||||
// us, we can't even really layout the page nicely for it. Instead we have to assume
|
||||
// it'll target 800x600 and we'll downsize if needed to make things fit.
|
||||
|
||||
// console.log("trying to fit image into timelineWidth of " + this.refs.body.offsetWidth + " or " + this.refs.body.clientWidth);
|
||||
let thumbHeight = null;
|
||||
if (content.info) {
|
||||
thumbHeight = ImageUtils.thumbHeight(content.info.w, content.info.h, timelineWidth, maxHeight);
|
||||
}
|
||||
this.refs.image.style.height = thumbHeight + "px";
|
||||
// console.log("Image height now", thumbHeight);
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let infoWidth;
|
||||
let infoHeight;
|
||||
|
||||
if (content && content.info && content.info.w && content.info.h) {
|
||||
infoWidth = content.info.w;
|
||||
infoHeight = content.info.h;
|
||||
} else {
|
||||
// Whilst the image loads, display nothing.
|
||||
//
|
||||
// Once loaded, use the loaded image dimensions stored in `loadedImageDimensions`.
|
||||
//
|
||||
// By doing this, the image "pops" into the timeline, but is still restricted
|
||||
// by the same width and height logic below.
|
||||
if (!this.state.loadedImageDimensions) {
|
||||
return this.wrapImage(contentUrl,
|
||||
<img style={{display: 'none'}} src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
infoWidth = this.state.loadedImageDimensions.naturalWidth;
|
||||
infoHeight = this.state.loadedImageDimensions.naturalHeight;
|
||||
}
|
||||
|
||||
// The maximum height of the thumbnail as it is rendered as an <img>
|
||||
const maxHeight = Math.min(this.props.maxImageHeight || 600, infoHeight);
|
||||
// The maximum width of the thumbnail, as dictated by its natural
|
||||
// maximum height.
|
||||
const maxWidth = infoWidth * maxHeight / infoHeight;
|
||||
|
||||
let img = null;
|
||||
let placeholder = null;
|
||||
|
||||
// e2e image hasn't been decrypted yet
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
placeholder = <img src="img/spinner.gif" alt={content.body} width="32" height="32" />;
|
||||
} else if (!this.state.imgLoaded) {
|
||||
// Deliberately, getSpinner is left unimplemented here, MStickerBody overides
|
||||
placeholder = this.getPlaceholder();
|
||||
}
|
||||
|
||||
const showPlaceholder = Boolean(placeholder);
|
||||
|
||||
if (thumbUrl && !this.state.imgError) {
|
||||
// Restrict the width of the thumbnail here, otherwise it will fill the container
|
||||
// which has the same width as the timeline
|
||||
// mx_MImageBody_thumbnail resizes img to exactly container size
|
||||
img = <img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
style={{ maxWidth: maxWidth + "px" }}
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />;
|
||||
}
|
||||
|
||||
const thumbnail = (
|
||||
<a href={contentUrl} onClick={this.onClick}>
|
||||
<img className="mx_MImageBody_thumbnail" src={thumbUrl} ref="image"
|
||||
alt={content.body}
|
||||
onError={this.onImageError}
|
||||
onLoad={this.onImageLoad}
|
||||
onMouseEnter={this.onImageEnter}
|
||||
onMouseLeave={this.onImageLeave} />
|
||||
</a>
|
||||
<div className="mx_MImageBody_thumbnail_container" style={{ maxHeight: maxHeight + "px" }} >
|
||||
{ /* Calculate aspect ratio, using %padding will size _container correctly */ }
|
||||
<div style={{ paddingBottom: (100 * infoHeight / infoWidth) + '%' }} />
|
||||
|
||||
{ showPlaceholder &&
|
||||
<div className="mx_MImageBody_thumbnail" style={{
|
||||
// Constrain width here so that spinner appears central to the loaded thumbnail
|
||||
maxWidth: infoWidth + "px",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail_spinner">
|
||||
{ placeholder }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div style={{display: !showPlaceholder ? undefined : 'none'}}>
|
||||
{ img }
|
||||
</div>
|
||||
|
||||
{ this.state.hover && this.getTooltip() }
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
{ thumbUrl && !this.state.imgError ? thumbnail : '' }
|
||||
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />
|
||||
</span>
|
||||
);
|
||||
return this.wrapImage(contentUrl, thumbnail);
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
wrapImage(contentUrl, children) {
|
||||
return <a href={contentUrl} onClick={this.onClick}>
|
||||
{children}
|
||||
</a>;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getPlaceholder() {
|
||||
// MImageBody doesn't show a placeholder whilst the image loads, (but it could do)
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getTooltip() {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Overidden by MStickerBody
|
||||
getFileBody() {
|
||||
return <MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} />;
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -302,25 +364,6 @@ export default class extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (content.file !== undefined && this.state.decryptedUrl === null) {
|
||||
// Need to decrypt the attachment
|
||||
// The attachment is decrypted in componentDidMount.
|
||||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MImageBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"alignItems": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<img src="img/spinner.gif" alt={content.body} width="32" height="32" style={{
|
||||
"margin": "auto",
|
||||
}} />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const contentUrl = this._getContentUrl();
|
||||
let thumbUrl;
|
||||
if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
|
||||
|
@ -329,6 +372,12 @@ export default class extends React.Component {
|
|||
thumbUrl = this._getThumbUrl();
|
||||
}
|
||||
|
||||
return this._messageContent(contentUrl, thumbUrl, content);
|
||||
const thumbnail = this._messageContent(contentUrl, thumbUrl, content);
|
||||
const fileBody = this.getFileBody();
|
||||
|
||||
return <span className="mx_MImageBody" ref="body">
|
||||
{ thumbnail }
|
||||
{ fileBody }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,145 +16,42 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import MImageBody from './MImageBody';
|
||||
import sdk from '../../../index';
|
||||
import TintableSVG from '../elements/TintableSvg';
|
||||
|
||||
export default class MStickerBody extends MImageBody {
|
||||
displayName: 'MStickerBody'
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._onMouseEnter = this._onMouseEnter.bind(this);
|
||||
this._onMouseLeave = this._onMouseLeave.bind(this);
|
||||
this._onImageLoad = this._onImageLoad.bind(this);
|
||||
}
|
||||
|
||||
_onMouseEnter() {
|
||||
this.setState({showTooltip: true});
|
||||
}
|
||||
|
||||
_onMouseLeave() {
|
||||
this.setState({showTooltip: false});
|
||||
}
|
||||
|
||||
_onImageLoad() {
|
||||
this.setState({
|
||||
placeholderClasses: 'mx_MStickerBody_placeholder_invisible',
|
||||
});
|
||||
const hidePlaceholderTimer = setTimeout(() => {
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
}, 500);
|
||||
this.setState({hidePlaceholderTimer});
|
||||
if (this.props.onWidgetLoad) {
|
||||
this.props.onWidgetLoad();
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentDidMount() {
|
||||
if (this.refs.image.complete) {
|
||||
// Image already loaded
|
||||
this.setState({
|
||||
placeholderVisible: false,
|
||||
placeholderClasses: '.mx_MStickerBody_placeholder_invisible',
|
||||
imageClasses: 'mx_MStickerBody_image_visible',
|
||||
});
|
||||
} else {
|
||||
// Image not already loaded
|
||||
this.setState({
|
||||
placeholderVisible: true,
|
||||
placeholderClasses: '',
|
||||
imageClasses: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_afterComponentWillUnmount() {
|
||||
if (this.state.hidePlaceholderTimer) {
|
||||
clearTimeout(this.state.hidePlaceholderTimer);
|
||||
this.setState({hidePlaceholderTimer: null});
|
||||
}
|
||||
}
|
||||
|
||||
_messageContent(contentUrl, thumbUrl, content) {
|
||||
let tooltip;
|
||||
const tooltipBody = (
|
||||
this.props.mxEvent &&
|
||||
this.props.mxEvent.getContent() &&
|
||||
this.props.mxEvent.getContent().body) ?
|
||||
this.props.mxEvent.getContent().body : null;
|
||||
if (this.state.showTooltip && tooltipBody) {
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
tooltip = <RoomTooltip
|
||||
className='mx_RoleButton_tooltip'
|
||||
label={tooltipBody} />;
|
||||
}
|
||||
|
||||
const gutterSize = 0;
|
||||
let placeholderSize = 75;
|
||||
let placeholderFixupHeight = '100px';
|
||||
let placeholderTop = 0;
|
||||
let placeholderLeft = 0;
|
||||
|
||||
if (content.info) {
|
||||
placeholderTop = Math.floor((content.info.h/2) - (placeholderSize/2)) + 'px';
|
||||
placeholderLeft = Math.floor((content.info.w/2) - (placeholderSize/2) + gutterSize) + 'px';
|
||||
placeholderFixupHeight = content.info.h + 'px';
|
||||
}
|
||||
|
||||
// The pixel size of sticker images is generally larger than their intended display
|
||||
// size so they render at native reolution on HiDPI displays. We therefore need to
|
||||
// explicity set the size so they render at the intended size.
|
||||
// XXX: This will be clobberred when we run fixupHeight(), but we need to do it
|
||||
// here otherwise the stickers are momentarily displayed at the pixel size.
|
||||
const imageStyle = {
|
||||
height: content.info.h,
|
||||
// leave the browser the calculate the width automatically
|
||||
};
|
||||
|
||||
placeholderSize = placeholderSize + 'px';
|
||||
|
||||
// Body 'ref' required by MImageBody
|
||||
return (
|
||||
<span className='mx_MStickerBody' ref='body'
|
||||
style={{
|
||||
height: placeholderFixupHeight,
|
||||
}}>
|
||||
<div className={'mx_MStickerBody_image_container'}>
|
||||
{ this.state.placeholderVisible &&
|
||||
<div
|
||||
className={'mx_MStickerBody_placeholder ' + this.state.placeholderClasses}
|
||||
style={{
|
||||
top: placeholderTop,
|
||||
left: placeholderLeft,
|
||||
}}
|
||||
>
|
||||
<TintableSVG
|
||||
src={'img/icons-show-stickers.svg'}
|
||||
width={placeholderSize}
|
||||
height={placeholderSize} />
|
||||
</div> }
|
||||
<img
|
||||
className={'mx_MStickerBody_image ' + this.state.imageClasses}
|
||||
src={contentUrl}
|
||||
style={imageStyle}
|
||||
ref='image'
|
||||
alt={content.body}
|
||||
onLoad={this._onImageLoad}
|
||||
onMouseEnter={this._onMouseEnter}
|
||||
onMouseLeave={this._onMouseLeave}
|
||||
/>
|
||||
{ tooltip }
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// Empty to prevent default behaviour of MImageBody
|
||||
onClick() {
|
||||
}
|
||||
|
||||
// MStickerBody doesn't need a wrapping `<a href=...>`, but it does need extra padding
|
||||
// which is added by mx_MStickerBody_wrapper
|
||||
wrapImage(contentUrl, children) {
|
||||
return <div className="mx_MStickerBody_wrapper"> { children } </div>;
|
||||
}
|
||||
|
||||
// Placeholder to show in place of the sticker image if
|
||||
// img onLoad hasn't fired yet.
|
||||
getPlaceholder() {
|
||||
const TintableSVG = sdk.getComponent('elements.TintableSvg');
|
||||
return <TintableSVG src="img/icons-show-stickers.svg" width="75" height="75" />;
|
||||
}
|
||||
|
||||
// Tooltip to show on mouse over
|
||||
getTooltip() {
|
||||
const content = this.props.mxEvent && this.props.mxEvent.getContent();
|
||||
|
||||
if (!content || !content.body || !content.info || !content.info.w) return null;
|
||||
|
||||
const RoomTooltip = sdk.getComponent('rooms.RoomTooltip');
|
||||
return <div style={{left: content.info.w + 'px'}} className="mx_MStickerBody_tooltip">
|
||||
<RoomTooltip label={content.body} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// Don't show "Download this_file.png ..."
|
||||
getFileBody() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,12 +147,7 @@ module.exports = React.createClass({
|
|||
// For now add an img tag with a spinner.
|
||||
return (
|
||||
<span className="mx_MVideoBody" ref="body">
|
||||
<div className="mx_MImageBody_thumbnail" ref="image" style={{
|
||||
"display": "flex",
|
||||
"align-items": "center",
|
||||
"justify-items": "center",
|
||||
"width": "100%",
|
||||
}}>
|
||||
<div className="mx_MImageBody_thumbnail mx_MImageBody_thumbnail_spinner" ref="image">
|
||||
<img src="img/spinner.gif" alt={content.body} width="16" height="16" />
|
||||
</div>
|
||||
</span>
|
||||
|
|
|
@ -39,8 +39,11 @@ module.exports = React.createClass({
|
|||
/* callback called when dynamic content in events are loaded */
|
||||
onWidgetLoad: PropTypes.func,
|
||||
|
||||
/* the shsape of the tile, used */
|
||||
/* the shape of the tile, used */
|
||||
tileShape: PropTypes.string,
|
||||
|
||||
/* the maximum image height to use, if the event is an image */
|
||||
maxImageHeight: PropTypes.number,
|
||||
},
|
||||
|
||||
getEventTileOps: function() {
|
||||
|
@ -59,17 +62,24 @@ module.exports = React.createClass({
|
|||
'm.audio': sdk.getComponent('messages.MAudioBody'),
|
||||
'm.video': sdk.getComponent('messages.MVideoBody'),
|
||||
};
|
||||
const evTypes = {
|
||||
'm.sticker': sdk.getComponent('messages.MStickerBody'),
|
||||
};
|
||||
|
||||
const content = this.props.mxEvent.getContent();
|
||||
const type = this.props.mxEvent.getType();
|
||||
const msgtype = content.msgtype;
|
||||
let BodyType = UnknownBody;
|
||||
if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (this.props.mxEvent.getType() === 'm.sticker') {
|
||||
BodyType = sdk.getComponent('messages.MStickerBody');
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
if (!this.props.mxEvent.isRedacted()) {
|
||||
// only resolve BodyType if event is not redacted
|
||||
if (type && evTypes[type]) {
|
||||
BodyType = evTypes[type];
|
||||
} else if (msgtype && bodyTypes[msgtype]) {
|
||||
BodyType = bodyTypes[msgtype];
|
||||
} else if (content.url) {
|
||||
// Fallback to MFileBody if there's a content URL
|
||||
BodyType = bodyTypes['m.file'];
|
||||
}
|
||||
}
|
||||
|
||||
return <BodyType
|
||||
|
@ -78,6 +88,7 @@ module.exports = React.createClass({
|
|||
highlightLink={this.props.highlightLink}
|
||||
showUrlPreview={this.props.showUrlPreview}
|
||||
tileShape={this.props.tileShape}
|
||||
maxImageHeight={this.props.maxImageHeight}
|
||||
onWidgetLoad={this.props.onWidgetLoad} />;
|
||||
},
|
||||
});
|
||||
|
|
|
@ -72,14 +72,12 @@ export default React.createClass({
|
|||
|
||||
_updateRelatedGroups() {
|
||||
if (this.unmounted) return;
|
||||
const relatedGroupsEvent = this.context.matrixClient
|
||||
.getRoom(this.props.mxEvent.getRoomId())
|
||||
.currentState
|
||||
.getStateEvents('m.room.related_groups', '');
|
||||
const room = this.context.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
const relatedGroupsEvent = room.currentState.getStateEvents('m.room.related_groups', '');
|
||||
this.setState({
|
||||
relatedGroups: relatedGroupsEvent ?
|
||||
relatedGroupsEvent.getContent().groups || []
|
||||
: [],
|
||||
relatedGroups: relatedGroupsEvent ? relatedGroupsEvent.getContent().groups || [] : [],
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -36,6 +36,7 @@ import * as ContextualMenu from '../../structures/ContextualMenu';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {host as matrixtoHost} from '../../../matrix-to';
|
||||
|
||||
linkifyMatrix(linkify);
|
||||
|
||||
|
@ -304,7 +305,7 @@ module.exports = React.createClass({
|
|||
// never preview matrix.to links (if anything we should give a smart
|
||||
// preview of the room/user they point to: nobody needs to be reminded
|
||||
// what the matrix.to site looks like).
|
||||
if (host == 'matrix.to') return false;
|
||||
if (host === matrixtoHost) return false;
|
||||
|
||||
if (node.textContent.toLowerCase().trim().startsWith(host.toLowerCase())) {
|
||||
// it's a "foo.pl" style link
|
||||
|
@ -336,10 +337,21 @@ module.exports = React.createClass({
|
|||
left: x,
|
||||
top: y,
|
||||
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||
});
|
||||
e.target.onmouseout = close;
|
||||
}, false);
|
||||
e.target.onmouseleave = close;
|
||||
};
|
||||
p.appendChild(button);
|
||||
|
||||
// Wrap a div around <pre> so that the copy button can be correctly positioned
|
||||
// when the <pre> overflows and is scrolled horizontally.
|
||||
const div = document.createElement("div");
|
||||
div.className = "mx_EventTile_pre_container";
|
||||
|
||||
// Insert containing div in place of <pre> block
|
||||
p.parentNode.replaceChild(div, p);
|
||||
|
||||
// Append <pre> block and copy button to container
|
||||
div.appendChild(p);
|
||||
div.appendChild(button);
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -422,8 +434,7 @@ module.exports = React.createClass({
|
|||
const mxEvent = this.props.mxEvent;
|
||||
const content = mxEvent.getContent();
|
||||
|
||||
const stripReply = SettingsStore.isFeatureEnabled("feature_rich_quoting") &&
|
||||
ReplyThread.getParentEventId(mxEvent);
|
||||
const stripReply = ReplyThread.getParentEventId(mxEvent);
|
||||
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
|
||||
disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
|
||||
// Part of Replies fallback support
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,6 +16,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const sdk = require("../../../index");
|
||||
|
@ -29,6 +31,10 @@ module.exports = React.createClass({
|
|||
room: PropTypes.object,
|
||||
},
|
||||
|
||||
contextTypes: {
|
||||
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
|
||||
},
|
||||
|
||||
saveSettings: function() {
|
||||
const promises = [];
|
||||
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
|
||||
|
@ -39,42 +45,58 @@ module.exports = React.createClass({
|
|||
render: function() {
|
||||
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
|
||||
const roomId = this.props.room.roomId;
|
||||
const isEncrypted = this.context.matrixClient.isRoomEncrypted(roomId);
|
||||
|
||||
let previewsForAccount = null;
|
||||
if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, { 'a': (sub)=><a href="#/settings">{ sub }</a> })
|
||||
);
|
||||
}
|
||||
|
||||
let previewsForRoom = null;
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={this.props.room.roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
|
||||
if (!isEncrypted) {
|
||||
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
|
||||
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
|
||||
if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>enabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
} else if (accountEnabled) {
|
||||
previewsForAccount = (
|
||||
_t("You have <a>disabled</a> URL previews by default.", {}, {
|
||||
'a': (sub)=><a href="#/settings">{ sub }</a>,
|
||||
})
|
||||
);
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
|
||||
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
|
||||
previewsForRoom = (
|
||||
<label>
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
level={SettingLevel.ROOM}
|
||||
roomId={roomId}
|
||||
isExplicit={true}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsRoom" />
|
||||
</label>
|
||||
);
|
||||
} else {
|
||||
let str = _td("URL previews are enabled by default for participants in this room.");
|
||||
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/true)) {
|
||||
str = _td("URL previews are disabled by default for participants in this room.");
|
||||
}
|
||||
previewsForRoom = (<label>{ _t(str) }</label>);
|
||||
}
|
||||
} else {
|
||||
previewsForAccount = (
|
||||
_t("In encrypted rooms, like this one, URL previews are disabled by default to ensure that your " +
|
||||
"homeserver (where the previews are generated) cannot gather information about links you see in " +
|
||||
"this room.")
|
||||
);
|
||||
}
|
||||
|
||||
const previewsForRoomAccount = (
|
||||
<SettingsFlag name="urlPreviewsEnabled"
|
||||
const previewsForRoomAccount = ( // in an e2ee room we use a special key to enforce per-room opt-in
|
||||
<SettingsFlag name={isEncrypted ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'}
|
||||
level={SettingLevel.ROOM_ACCOUNT}
|
||||
roomId={this.props.room.roomId}
|
||||
roomId={roomId}
|
||||
manualSave={true}
|
||||
ref="urlPreviewsSelf"
|
||||
/>
|
||||
|
@ -83,8 +105,13 @@ module.exports = React.createClass({
|
|||
return (
|
||||
<div className="mx_RoomSettings_toggles">
|
||||
<h3>{ _t("URL Previews") }</h3>
|
||||
|
||||
<label>{ previewsForAccount }</label>
|
||||
<div>
|
||||
{ _t('When someone puts a URL in their message, a URL preview can be shown to give more ' +
|
||||
'information about that link such as the title, description, and an image from the website.') }
|
||||
</div>
|
||||
<div>
|
||||
{ previewsForAccount }
|
||||
</div>
|
||||
{ previewsForRoom }
|
||||
<label>{ previewsForRoomAccount }</label>
|
||||
</div>
|
||||
|
|
|
@ -27,7 +27,7 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import ScalarMessaging from '../../../ScalarMessaging';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import WidgetUtils from '../../../WidgetUtils';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
// The maximum number of widgets that can be added in a room
|
||||
|
@ -94,15 +94,7 @@ module.exports = React.createClass({
|
|||
const hideWidgetKey = this.props.room.roomId + '_hide_widget_drawer';
|
||||
switch (action.action) {
|
||||
case 'appsDrawer':
|
||||
// When opening the app drawer when there aren't any apps,
|
||||
// auto-launch the integrations manager to skip the awkward
|
||||
// click on "Add widget"
|
||||
if (action.show) {
|
||||
const apps = this._getApps();
|
||||
if (apps.length === 0) {
|
||||
this._launchManageIntegrations();
|
||||
}
|
||||
|
||||
localStorage.removeItem(hideWidgetKey);
|
||||
} else {
|
||||
// Store hidden state of widget
|
||||
|
@ -171,14 +163,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_getApps: function() {
|
||||
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
|
||||
if (!appsStateEvents) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return appsStateEvents.filter((ev) => {
|
||||
return ev.getContent().type && ev.getContent().url;
|
||||
}).map((ev) => {
|
||||
return WidgetUtils.getRoomWidgets(this.props.room).map((ev) => {
|
||||
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -34,6 +34,7 @@ const ContextualMenu = require('../../structures/ContextualMenu');
|
|||
import dis from '../../../dispatcher';
|
||||
import {makeEventPermalink} from "../../../matrix-to";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {EventStatus} from 'matrix-js-sdk';
|
||||
|
||||
const ObjectUtils = require('../../../ObjectUtils');
|
||||
|
||||
|
@ -55,6 +56,7 @@ const stateEventTileTypes = {
|
|||
'm.room.topic': 'messages.TextualEvent',
|
||||
'm.room.power_levels': 'messages.TextualEvent',
|
||||
'm.room.pinned_events': 'messages.TextualEvent',
|
||||
'm.room.server_acl' : 'messages.TextualEvent',
|
||||
|
||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||
};
|
||||
|
@ -442,26 +444,27 @@ module.exports = withMatrixClient(React.createClass({
|
|||
const ev = this.props.mxEvent;
|
||||
const props = {onClick: this.onCryptoClicked};
|
||||
|
||||
|
||||
// event could not be decrypted
|
||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||
return <E2ePadlockUndecryptable {...props} />;
|
||||
} else if (ev.isEncrypted()) {
|
||||
if (this.state.verified) {
|
||||
return <E2ePadlockVerified {...props} />;
|
||||
} else {
|
||||
return <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
} else {
|
||||
// XXX: if the event is being encrypted (ie eventSendStatus ===
|
||||
// encrypting), it might be nice to show something other than the
|
||||
// open padlock?
|
||||
}
|
||||
|
||||
// if the event is not encrypted, but it's an e2e room, show the
|
||||
// open padlock
|
||||
const e2eEnabled = this.props.matrixClient.isRoomEncrypted(ev.getRoomId());
|
||||
if (e2eEnabled) {
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
// event is encrypted, display padlock corresponding to whether or not it is verified
|
||||
if (ev.isEncrypted()) {
|
||||
return this.state.verified ? <E2ePadlockVerified {...props} /> : <E2ePadlockUnverified {...props} />;
|
||||
}
|
||||
|
||||
if (this.props.matrixClient.isRoomEncrypted(ev.getRoomId())) {
|
||||
// else if room is encrypted
|
||||
// and event is being encrypted or is not_sent (Unknown Devices/Network Error)
|
||||
if (ev.status === EventStatus.ENCRYPTING) {
|
||||
return <E2ePadlockEncrypting {...props} />;
|
||||
}
|
||||
if (ev.status === EventStatus.NOT_SENT) {
|
||||
return <E2ePadlockNotSent {...props} />;
|
||||
}
|
||||
// if the event is not encrypted, but it's an e2e room, show the open padlock
|
||||
return <E2ePadlockUnencrypted {...props} />;
|
||||
}
|
||||
|
||||
// no padlock needed
|
||||
|
@ -490,7 +493,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
|
||||
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
|
||||
const isRedacted = (eventType === 'm.room.message') && this.props.isRedacted;
|
||||
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
|
||||
const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure();
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -608,13 +611,14 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
switch (this.props.tileShape) {
|
||||
case 'notif': {
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="mx_EventTile_roomName">
|
||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
|
||||
{ room ? room.name : '' }
|
||||
</a>
|
||||
</EmojiText>
|
||||
</div>
|
||||
<div className="mx_EventTile_senderDetails">
|
||||
{ avatar }
|
||||
|
@ -715,9 +719,15 @@ module.exports = withMatrixClient(React.createClass({
|
|||
},
|
||||
}));
|
||||
|
||||
// XXX this'll eventually be dynamic based on the fields once we have extensible event types
|
||||
const messageTypes = ['m.room.message', 'm.sticker'];
|
||||
function isMessageEvent(ev) {
|
||||
return (messageTypes.includes(ev.getType()));
|
||||
}
|
||||
|
||||
module.exports.haveTileForEvent = function(e) {
|
||||
// Only messages have a tile (black-rectangle) if redacted
|
||||
if (e.isRedacted() && e.getType() !== 'm.room.message') return false;
|
||||
if (e.isRedacted() && !isMessageEvent(e)) return false;
|
||||
|
||||
const handler = getHandlerTile(e);
|
||||
if (handler === undefined) return false;
|
||||
|
@ -736,6 +746,14 @@ function E2ePadlockUndecryptable(props) {
|
|||
);
|
||||
}
|
||||
|
||||
function E2ePadlockEncrypting(props) {
|
||||
return <E2ePadlock alt={_t("Encrypting")} src="img/e2e-encrypting.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockNotSent(props) {
|
||||
return <E2ePadlock alt={_t("Encrypted, not sent")} src="img/e2e-not_sent.svg" width="10" height="12" {...props} />;
|
||||
}
|
||||
|
||||
function E2ePadlockVerified(props) {
|
||||
return (
|
||||
<E2ePadlock alt={_t("Encrypted by a verified device")}
|
||||
|
|
|
@ -332,13 +332,40 @@ module.exports = withMatrixClient(React.createClass({
|
|||
});
|
||||
},
|
||||
|
||||
onMuteToggle: function() {
|
||||
_warnSelfDemote: function() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
return new Promise((resolve) => {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
onFinished: resolve,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
onMuteToggle: async function() {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
if (!room) return;
|
||||
|
||||
// if muting self, warn as it may be irreversible
|
||||
if (target === this.props.matrixClient.getUserId()) {
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
if (!powerLevelEvent) return;
|
||||
|
||||
|
@ -436,7 +463,7 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}).done();
|
||||
},
|
||||
|
||||
onPowerChange: function(powerLevel) {
|
||||
onPowerChange: async function(powerLevel) {
|
||||
const roomId = this.props.member.roomId;
|
||||
const target = this.props.member.userId;
|
||||
const room = this.props.matrixClient.getRoom(roomId);
|
||||
|
@ -455,20 +482,12 @@ module.exports = withMatrixClient(React.createClass({
|
|||
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
if (myUserId === target) {
|
||||
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
}
|
||||
},
|
||||
});
|
||||
try {
|
||||
if (!(await this._warnSelfDemote())) return;
|
||||
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -478,7 +497,8 @@ module.exports = withMatrixClient(React.createClass({
|
|||
title: _t("Warning!"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
|
||||
{ _t("You will not be able to undo this change as you are promoting the user " +
|
||||
"to have the same power level as yourself.") }<br />
|
||||
{ _t("Are you sure?") }
|
||||
</div>,
|
||||
button: _t("Continue"),
|
||||
|
@ -632,6 +652,13 @@ module.exports = withMatrixClient(React.createClass({
|
|||
);
|
||||
},
|
||||
|
||||
onShareUserClick: function() {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room member dialog', '', ShareDialog, {
|
||||
target: this.props.member,
|
||||
});
|
||||
},
|
||||
|
||||
_renderUserOptions: function() {
|
||||
const cli = this.props.matrixClient;
|
||||
const member = this.props.member;
|
||||
|
@ -705,13 +732,18 @@ module.exports = withMatrixClient(React.createClass({
|
|||
}
|
||||
}
|
||||
|
||||
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
|
||||
const shareUserButton = (
|
||||
<AccessibleButton onClick={this.onShareUserClick} className="mx_MemberInfo_field">
|
||||
{ _t('Share Link to User') }
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>{ _t("User Options") }</h3>
|
||||
<div className="mx_MemberInfo_buttons">
|
||||
{ readReceiptButton }
|
||||
{ shareUserButton }
|
||||
{ insertPillButton }
|
||||
{ ignoreButton }
|
||||
{ inviteUserButton }
|
||||
|
@ -902,7 +934,9 @@ module.exports = withMatrixClient(React.createClass({
|
|||
return (
|
||||
<div className="mx_MemberInfo">
|
||||
<GeminiScrollbarWrapper autoshow={true}>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}> <img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
|
||||
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
|
||||
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
|
||||
</AccessibleButton>
|
||||
<div className="mx_MemberInfo_avatar">
|
||||
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />
|
||||
</div>
|
||||
|
|
|
@ -270,7 +270,7 @@ module.exports = React.createClass({
|
|||
|
||||
// console.log("comparing " + this.memberString(memberA) + " and " + this.memberString(memberB));
|
||||
|
||||
if (userA.currentlyActive && userB.currentlyActive) {
|
||||
if ((userA.currentlyActive && userB.currentlyActive) || !this._showPresence) {
|
||||
// console.log(memberA.name + " and " + memberB.name + " are both active");
|
||||
if (memberA.powerLevel === memberB.powerLevel) {
|
||||
// console.log(memberA + " and " + memberB + " have same power level");
|
||||
|
|
|
@ -155,54 +155,20 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// _startCallApp(isAudioConf) {
|
||||
// dis.dispatch({
|
||||
// action: 'appsDrawer',
|
||||
// show: true,
|
||||
// });
|
||||
|
||||
// const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets', '');
|
||||
// let appsStateEvent = {};
|
||||
// if (appsStateEvents) {
|
||||
// appsStateEvent = appsStateEvents.getContent();
|
||||
// }
|
||||
// if (!appsStateEvent.videoConf) {
|
||||
// appsStateEvent.videoConf = {
|
||||
// type: 'jitsi',
|
||||
// // FIXME -- This should not be localhost
|
||||
// url: 'http://localhost:8000/jitsi.html',
|
||||
// data: {
|
||||
// confId: this.props.room.roomId.replace(/[^A-Za-z0-9]/g, '_') + Date.now(),
|
||||
// isAudioConf: isAudioConf,
|
||||
// },
|
||||
// };
|
||||
// MatrixClientPeg.get().sendStateEvent(
|
||||
// this.props.room.roomId,
|
||||
// 'im.vector.modular.widgets',
|
||||
// appsStateEvent,
|
||||
// '',
|
||||
// ).then(() => console.log('Sent state'), (e) => console.error(e));
|
||||
// }
|
||||
// }
|
||||
|
||||
onCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? "screensharing" : "video",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(false);
|
||||
}
|
||||
|
||||
onVoiceCallClick(ev) {
|
||||
// NOTE -- Will be replaced by Jitsi code (currently commented)
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: "voice",
|
||||
room_id: this.props.room.roomId,
|
||||
});
|
||||
// this._startCallApp(true);
|
||||
}
|
||||
|
||||
onInputStateChanged(inputState) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -37,7 +37,7 @@ import Promise from 'bluebird';
|
|||
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||
import SlashCommands from '../../../SlashCommands';
|
||||
import {processCommandInput} from '../../../SlashCommands';
|
||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||
import Modal from '../../../Modal';
|
||||
import sdk from '../../../index';
|
||||
|
@ -54,8 +54,7 @@ import Markdown from '../../../Markdown';
|
|||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||
|
||||
import {MATRIXTO_URL_PATTERN, MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||
const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
|
||||
import {MATRIXTO_MD_LINK_PATTERN} from '../../../linkify-matrix';
|
||||
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
||||
|
||||
import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione';
|
||||
|
@ -314,7 +313,7 @@ export default class MessageComposerInput extends React.Component {
|
|||
switch (payload.action) {
|
||||
case 'reply_to_event':
|
||||
case 'focus_composer':
|
||||
editor.focus();
|
||||
this.focusComposer();
|
||||
break;
|
||||
case 'insert_mention':
|
||||
{
|
||||
|
@ -1511,6 +1510,10 @@ export default class MessageComposerInput extends React.Component {
|
|||
}
|
||||
};
|
||||
|
||||
focusComposer = () => {
|
||||
this.refs.editor.focus();
|
||||
};
|
||||
|
||||
render() {
|
||||
const activeEditorState = this.state.originalEditorState || this.state.editorState;
|
||||
|
||||
|
@ -1519,9 +1522,9 @@ export default class MessageComposerInput extends React.Component {
|
|||
});
|
||||
|
||||
return (
|
||||
<div className="mx_MessageComposer_input_wrapper">
|
||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||
{ SettingsStore.isFeatureEnabled("feature_rich_quoting") && <ReplyPreview /> }
|
||||
<ReplyPreview />
|
||||
<Autocomplete
|
||||
ref={(e) => this.autocomplete = e}
|
||||
room={this.props.room}
|
||||
|
|
|
@ -22,6 +22,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
|||
import MessageEvent from "../messages/MessageEvent";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { _t } from '../../../languageHandler';
|
||||
import {formatFullDate} from '../../../DateUtils';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'PinnedEventTile',
|
||||
|
@ -80,11 +81,20 @@ module.exports = React.createClass({
|
|||
{ unpinButton }
|
||||
</div>
|
||||
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
<span className="mx_PinnedEventTile_senderAvatar">
|
||||
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
|
||||
</span>
|
||||
<span className="mx_PinnedEventTile_sender">
|
||||
{ sender.name }
|
||||
</span>
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
|
||||
<span className="mx_PinnedEventTile_timestamp">
|
||||
{ formatFullDate(new Date(this.props.mxEvent.getTs())) }
|
||||
</span>
|
||||
<div className="mx_PinnedEventTile_message">
|
||||
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" maxImageHeight={150}
|
||||
onWidgetLoad={() => {}} // we need to give this, apparently
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -39,6 +39,19 @@ module.exports = React.createClass({
|
|||
|
||||
componentDidMount: function() {
|
||||
this._updatePinnedMessages();
|
||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("RoomState.events", this._onStateEvent);
|
||||
}
|
||||
},
|
||||
|
||||
_onStateEvent: function(ev) {
|
||||
if (ev.getRoomId() === this.props.room.roomId && ev.getType() === "m.room.pinned_events") {
|
||||
this._updatePinnedMessages();
|
||||
}
|
||||
},
|
||||
|
||||
_updatePinnedMessages: function() {
|
||||
|
|
|
@ -149,6 +149,13 @@ module.exports = React.createClass({
|
|||
dis.dispatch({ action: 'show_right_panel' });
|
||||
},
|
||||
|
||||
onShareRoomClick: function(ev) {
|
||||
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
|
||||
Modal.createTrackedDialog('share room dialog', '', ShareDialog, {
|
||||
target: this.props.room,
|
||||
});
|
||||
},
|
||||
|
||||
_hasUnreadPins: function() {
|
||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||
if (!currentPinEvent) return false;
|
||||
|
@ -379,6 +386,14 @@ module.exports = React.createClass({
|
|||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let shareRoomButton;
|
||||
if (this.props.inRoom) {
|
||||
shareRoomButton =
|
||||
<AccessibleButton className="mx_RoomHeader_button" onClick={this.onShareRoomClick} title={_t('Share room')}>
|
||||
<TintableSvg src="img/icons-share.svg" width="16" height="16" />
|
||||
</AccessibleButton>;
|
||||
}
|
||||
|
||||
let rightPanelButtons;
|
||||
if (this.props.collapsedRhs) {
|
||||
rightPanelButtons =
|
||||
|
@ -400,6 +415,7 @@ module.exports = React.createClass({
|
|||
<div className="mx_RoomHeader_rightRow">
|
||||
{ settingsButton }
|
||||
{ pinnedEventsButton }
|
||||
{ shareRoomButton }
|
||||
{ manageIntegsButton }
|
||||
{ forgetButton }
|
||||
{ searchButton }
|
||||
|
|
|
@ -16,6 +16,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
'use strict';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
||||
const React = require("react");
|
||||
const ReactDOM = require("react-dom");
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -583,14 +585,18 @@ module.exports = React.createClass({
|
|||
}
|
||||
},
|
||||
|
||||
_makeGroupInviteTiles() {
|
||||
_makeGroupInviteTiles(filter) {
|
||||
const ret = [];
|
||||
const lcFilter = filter && filter.toLowerCase();
|
||||
|
||||
const GroupInviteTile = sdk.getComponent('groups.GroupInviteTile');
|
||||
for (const group of MatrixClientPeg.get().getGroups()) {
|
||||
if (group.myMembership !== 'invite') continue;
|
||||
|
||||
ret.push(<GroupInviteTile key={group.groupId} group={group} />);
|
||||
const {groupId, name, myMembership} = group;
|
||||
// filter to only groups in invite state and group_id starts with filter or group name includes it
|
||||
if (myMembership !== 'invite') continue;
|
||||
if (lcFilter && !groupId.toLowerCase().startsWith(lcFilter) &&
|
||||
!(name && name.toLowerCase().includes(lcFilter))) continue;
|
||||
ret.push(<GroupInviteTile key={groupId} group={group} collapsed={this.props.collapsed} />);
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
@ -604,13 +610,17 @@ module.exports = React.createClass({
|
|||
const RoomSubList = sdk.getComponent('structures.RoomSubList');
|
||||
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
|
||||
|
||||
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify
|
||||
// so checking on every render is the sanest thing at this time.
|
||||
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
|
||||
|
||||
const self = this;
|
||||
return (
|
||||
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
|
||||
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
autoshow={true} onScroll={self._whenScrolling} onResize={self._whenScrolling} wrappedRef={this._collectGemini}>
|
||||
<div className="mx_RoomList">
|
||||
<RoomSubList list={[]}
|
||||
extraTiles={this._makeGroupInviteTiles()}
|
||||
extraTiles={this._makeGroupInviteTiles(self.props.searchFilter)}
|
||||
label={_t('Community Invites')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -619,6 +629,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
|
||||
|
@ -631,6 +642,7 @@ module.exports = React.createClass({
|
|||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty}
|
||||
/>
|
||||
|
||||
<RoomSubList list={self.state.lists['m.favourite']}
|
||||
|
@ -643,7 +655,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
|
||||
label={_t('People')}
|
||||
|
@ -657,7 +670,8 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
|
||||
label={_t('Rooms')}
|
||||
|
@ -669,7 +683,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
{ Object.keys(self.state.lists).map((tagName) => {
|
||||
if (!tagName.match(STANDARD_TAGS_REGEX)) {
|
||||
|
@ -684,7 +699,8 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />;
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />;
|
||||
}
|
||||
}) }
|
||||
|
||||
|
@ -698,9 +714,17 @@ module.exports = React.createClass({
|
|||
collapsed={self.props.collapsed}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onHeaderClick={self.onSubListHeaderClick}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
|
||||
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
|
||||
emptyContent={self.props.collapsed ? null :
|
||||
<div className="mx_RoomList_emptySubListTip_container">
|
||||
<div className="mx_RoomList_emptySubListTip">
|
||||
{ _t('You have no historical rooms') }
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
label={_t('Historical')}
|
||||
editable={false}
|
||||
order="recent"
|
||||
|
@ -708,10 +732,11 @@ module.exports = React.createClass({
|
|||
alwaysShowHeader={true}
|
||||
startAsHidden={true}
|
||||
showSpinner={self.state.isLoadingLeftRooms}
|
||||
onHeaderClick= {self.onArchivedHeaderClick}
|
||||
onHeaderClick={self.onArchivedHeaderClick}
|
||||
incomingCall={self.state.incomingCall}
|
||||
searchFilter={self.props.searchFilter}
|
||||
onShowMoreRooms={self.onShowMoreRooms} />
|
||||
onShowMoreRooms={self.onShowMoreRooms}
|
||||
showEmpty={showEmpty} />
|
||||
</div>
|
||||
</GeminiScrollbarWrapper>
|
||||
);
|
||||
|
|
|
@ -395,7 +395,17 @@ module.exports = React.createClass({
|
|||
powerLevels["events"] = Object.assign({}, this.state.powerLevels["events"] || {});
|
||||
powerLevels["events"][powerLevelKey.slice(eventsLevelPrefix.length)] = value;
|
||||
} else {
|
||||
powerLevels[powerLevelKey] = value;
|
||||
const keyPath = powerLevelKey.split('.');
|
||||
let parentObj;
|
||||
let currentObj = powerLevels;
|
||||
for (const key of keyPath) {
|
||||
if (!currentObj[key]) {
|
||||
currentObj[key] = {};
|
||||
}
|
||||
parentObj = currentObj;
|
||||
currentObj = currentObj[key];
|
||||
}
|
||||
parentObj[keyPath[keyPath.length - 1]] = value;
|
||||
}
|
||||
this.setState({
|
||||
powerLevels,
|
||||
|
@ -664,6 +674,10 @@ module.exports = React.createClass({
|
|||
desc: _t('To remove other users\' messages, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
"notifications.room": {
|
||||
desc: _t('To notify everyone in the room, you must be a'),
|
||||
defaultValue: 50,
|
||||
},
|
||||
};
|
||||
|
||||
const banLevel = parseIntWithDefault(powerLevels.ban, powerLevelDescriptors.ban.defaultValue);
|
||||
|
@ -695,26 +709,57 @@ module.exports = React.createClass({
|
|||
relatedGroupsEvent={this.props.room.currentState.getStateEvents('m.room.related_groups', '')}
|
||||
/>;
|
||||
|
||||
let userLevelsSection;
|
||||
let privilegedUsersSection = <div>{ _t('No users have specific privileges in this room') }.</div>; // default
|
||||
let mutedUsersSection;
|
||||
if (Object.keys(userLevels).length) {
|
||||
userLevelsSection =
|
||||
<div>
|
||||
<h3>{ _t('Privileged Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ Object.keys(userLevels).map(function(user, i) {
|
||||
return (
|
||||
<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</li>
|
||||
);
|
||||
const privilegedUsers = [];
|
||||
const mutedUsers = [];
|
||||
|
||||
Object.keys(userLevels).forEach(function(user) {
|
||||
if (userLevels[user] > defaultUserLevel) { // privileged
|
||||
privilegedUsers.push(<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</ul>
|
||||
</div>;
|
||||
} else {
|
||||
userLevelsSection = <div>{ _t('No users have specific privileges in this room') }.</div>;
|
||||
</li>);
|
||||
} else if (userLevels[user] < defaultUserLevel) { // muted
|
||||
mutedUsers.push(<li className="mx_RoomSettings_userLevel" key={user}>
|
||||
{ _t("%(user)s is a %(userRole)s", {
|
||||
user: user,
|
||||
userRole: <PowerSelector value={userLevels[user]} disabled={true} />,
|
||||
}) }
|
||||
</li>);
|
||||
}
|
||||
});
|
||||
|
||||
// comparator for sorting PL users lexicographically on PL descending, MXID ascending. (case-insensitive)
|
||||
const comparator = (a, b) => {
|
||||
const plDiff = userLevels[b.key] - userLevels[a.key];
|
||||
return plDiff !== 0 ? plDiff : a.key.toLocaleLowerCase().localeCompare(b.key.toLocaleLowerCase());
|
||||
};
|
||||
|
||||
privilegedUsers.sort(comparator);
|
||||
mutedUsers.sort(comparator);
|
||||
|
||||
if (privilegedUsers.length) {
|
||||
privilegedUsersSection =
|
||||
<div>
|
||||
<h3>{ _t('Privileged Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ privilegedUsers }
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
if (mutedUsers.length) {
|
||||
mutedUsersSection =
|
||||
<div>
|
||||
<h3>{ _t('Muted Users') }</h3>
|
||||
<ul className="mx_RoomSettings_userLevels">
|
||||
{ mutedUsers }
|
||||
</ul>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
const banned = this.props.room.getMembersWithMembership("ban");
|
||||
|
@ -834,7 +879,16 @@ module.exports = React.createClass({
|
|||
const powerSelectors = Object.keys(powerLevelDescriptors).map((key, index) => {
|
||||
const descriptor = powerLevelDescriptors[key];
|
||||
|
||||
const value = parseIntWithDefault(powerLevels[key], descriptor.defaultValue);
|
||||
const keyPath = key.split('.');
|
||||
let currentObj = powerLevels;
|
||||
for (const prop of keyPath) {
|
||||
if (currentObj === undefined) {
|
||||
break;
|
||||
}
|
||||
currentObj = currentObj[prop];
|
||||
}
|
||||
|
||||
const value = parseIntWithDefault(currentObj, descriptor.defaultValue);
|
||||
return <div key={index} className="mx_RoomSettings_powerLevel">
|
||||
<span className="mx_RoomSettings_powerLevelKey">
|
||||
{ descriptor.desc }
|
||||
|
@ -979,8 +1033,8 @@ module.exports = React.createClass({
|
|||
{ unfederatableSection }
|
||||
</div>
|
||||
|
||||
{ userLevelsSection }
|
||||
|
||||
{ privilegedUsersSection }
|
||||
{ mutedUsersSection }
|
||||
{ bannedUsersSection }
|
||||
|
||||
<h3>{ _t('Advanced') }</h3>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,19 +16,17 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
const ReactDOM = require("react-dom");
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
const classNames = require('classnames');
|
||||
import classNames from 'classnames';
|
||||
import dis from '../../../dispatcher';
|
||||
const MatrixClientPeg = require('../../../MatrixClientPeg');
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
const sdk = require('../../../index');
|
||||
const ContextualMenu = require('../../structures/ContextualMenu');
|
||||
const RoomNotifs = require('../../../RoomNotifs');
|
||||
const FormattingUtils = require('../../../utils/FormattingUtils');
|
||||
import sdk from '../../../index';
|
||||
import {createMenu} from '../../structures/ContextualMenu';
|
||||
import * as RoomNotifs from '../../../RoomNotifs';
|
||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||
|
@ -72,16 +71,12 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_shouldShowMentionBadge: function() {
|
||||
return this.state.notifState != RoomNotifs.MUTE;
|
||||
return this.state.notifState !== RoomNotifs.MUTE;
|
||||
},
|
||||
|
||||
_isDirectMessageRoom: function(roomId) {
|
||||
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||
if (dmRooms) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return Boolean(dmRooms);
|
||||
},
|
||||
|
||||
onRoomTimeline: function(ev, room) {
|
||||
|
@ -99,7 +94,7 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onAccountData: function(accountDataEvent) {
|
||||
if (accountDataEvent.getType() == 'm.push_rules') {
|
||||
if (accountDataEvent.getType() === 'm.push_rules') {
|
||||
this.setState({
|
||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||
});
|
||||
|
@ -187,6 +182,32 @@ module.exports = React.createClass({
|
|||
this.badgeOnMouseLeave();
|
||||
},
|
||||
|
||||
_showContextMenu: function(x, y, chevronOffset) {
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
|
||||
createMenu(RoomTileContextMenu, {
|
||||
chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: () => {
|
||||
this.setState({ menuDisplayed: false });
|
||||
this.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
},
|
||||
|
||||
onContextMenu: function(e) {
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.preventDefault();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
const chevronOffset = 12;
|
||||
this._showContextMenu(e.clientX, e.clientY - (chevronOffset + 8), chevronOffset);
|
||||
},
|
||||
|
||||
badgeOnMouseEnter: function() {
|
||||
// Only allow non-guests to access the context menu
|
||||
// and only change it if it needs to change
|
||||
|
@ -200,37 +221,25 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
onBadgeClicked: function(e) {
|
||||
// Only allow none guests to access the context menu
|
||||
if (!MatrixClientPeg.get().isGuest()) {
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const RoomTileContextMenu = sdk.getComponent('context_menus.RoomTileContextMenu');
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
const self = this;
|
||||
ContextualMenu.createMenu(RoomTileContextMenu, {
|
||||
chevronOffset: chevronOffset,
|
||||
left: x,
|
||||
top: y,
|
||||
room: this.props.room,
|
||||
onFinished: function() {
|
||||
self.setState({ menuDisplayed: false });
|
||||
self.props.refreshSubList();
|
||||
},
|
||||
});
|
||||
this.setState({ menuDisplayed: true });
|
||||
}
|
||||
// Prevent the RoomTile onClick event firing as well
|
||||
e.stopPropagation();
|
||||
// Only allow non-guests to access the context menu
|
||||
if (MatrixClientPeg.get().isGuest()) return;
|
||||
|
||||
// If the badge is clicked, then no longer show tooltip
|
||||
if (this.props.collapsed) {
|
||||
this.setState({ hover: false });
|
||||
}
|
||||
|
||||
const elementRect = e.target.getBoundingClientRect();
|
||||
|
||||
// The window X and Y offsets are to adjust position when zoomed in to page
|
||||
const x = elementRect.right + window.pageXOffset + 3;
|
||||
const chevronOffset = 12;
|
||||
let y = (elementRect.top + (elementRect.height / 2) + window.pageYOffset);
|
||||
y = y - (chevronOffset + 8); // where 8 is half the height of the chevron
|
||||
|
||||
this._showContextMenu(x, y, chevronOffset);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
|
@ -250,7 +259,7 @@ module.exports = React.createClass({
|
|||
'mx_RoomTile_unread': this.props.unread,
|
||||
'mx_RoomTile_unreadNotify': notifBadges,
|
||||
'mx_RoomTile_highlight': mentionBadges,
|
||||
'mx_RoomTile_invited': (me && me.membership == 'invite'),
|
||||
'mx_RoomTile_invited': (me && me.membership === 'invite'),
|
||||
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
|
||||
'mx_RoomTile_noBadges': !badges,
|
||||
'mx_RoomTile_transparent': this.props.transparent,
|
||||
|
@ -268,7 +277,6 @@ module.exports = React.createClass({
|
|||
let name = this.state.roomName;
|
||||
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
|
||||
|
||||
let badge;
|
||||
let badgeContent;
|
||||
|
||||
if (this.state.badgeHover || this.state.menuDisplayed) {
|
||||
|
@ -280,7 +288,7 @@ module.exports = React.createClass({
|
|||
badgeContent = '\u200B';
|
||||
}
|
||||
|
||||
badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
const badge = <div className={badgeClasses} onClick={this.onBadgeClicked}>{ badgeContent }</div>;
|
||||
|
||||
const EmojiText = sdk.getComponent('elements.EmojiText');
|
||||
let label;
|
||||
|
@ -301,7 +309,7 @@ module.exports = React.createClass({
|
|||
}
|
||||
} else if (this.state.hover) {
|
||||
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" room={this.props.room} dir="auto" />;
|
||||
tooltip = <RoomTooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto" />;
|
||||
}
|
||||
|
||||
//var incomingCallBox;
|
||||
|
@ -312,16 +320,22 @@ module.exports = React.createClass({
|
|||
|
||||
const RoomAvatar = sdk.getComponent('avatars.RoomAvatar');
|
||||
|
||||
let directMessageIndicator;
|
||||
let dmIndicator;
|
||||
if (this._isDirectMessageRoom(this.props.room.roomId)) {
|
||||
directMessageIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
dmIndicator = <img src="img/icon_person.svg" className="mx_RoomTile_dm" width="11" height="13" alt="dm" />;
|
||||
}
|
||||
|
||||
return <AccessibleButton className={classes} tabIndex="0" onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave}>
|
||||
return <AccessibleButton tabIndex="0"
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onContextMenu={this.onContextMenu}
|
||||
>
|
||||
<div className={avatarClasses}>
|
||||
<div className="mx_RoomTile_avatar_container">
|
||||
<RoomAvatar room={this.props.room} width={24} height={24} />
|
||||
{ directMessageIndicator }
|
||||
{ dmIndicator }
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx_RoomTile_nameContainer">
|
||||
|
|
|
@ -14,11 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
var dis = require('../../../dispatcher');
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import dis from '../../../dispatcher';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const MIN_TOOLTIP_HEIGHT = 25;
|
||||
|
@ -77,25 +76,21 @@ module.exports = React.createClass({
|
|||
},
|
||||
|
||||
_renderTooltip: function() {
|
||||
var label = this.props.room ? this.props.room.name : this.props.label;
|
||||
|
||||
// Add the parent's position to the tooltips, so it's correctly
|
||||
// positioned, also taking into account any window zoom
|
||||
// NOTE: The additional 6 pixels for the left position, is to take account of the
|
||||
// tooltips chevron
|
||||
var parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
var style = {};
|
||||
const parent = ReactDOM.findDOMNode(this).parentNode;
|
||||
let style = {};
|
||||
style = this._updatePosition(style);
|
||||
style.display = "block";
|
||||
|
||||
const tooltipClasses = classNames(
|
||||
"mx_RoomTooltip", this.props.tooltipClassName,
|
||||
);
|
||||
const tooltipClasses = classNames("mx_RoomTooltip", this.props.tooltipClassName);
|
||||
|
||||
var tooltip = (
|
||||
<div className={tooltipClasses} style={style} >
|
||||
<div className="mx_RoomTooltip_chevron"></div>
|
||||
{ label }
|
||||
const tooltip = (
|
||||
<div className={tooltipClasses} style={style}>
|
||||
<div className="mx_RoomTooltip_chevron" />
|
||||
{ this.props.label }
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
import React from 'react';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import Widgets from '../../../utils/widgets';
|
||||
import AppTile from '../elements/AppTile';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import Modal from '../../../Modal';
|
||||
|
@ -24,9 +23,14 @@ import SdkConfig from '../../../SdkConfig';
|
|||
import ScalarAuthClient from '../../../ScalarAuthClient';
|
||||
import dis from '../../../dispatcher';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import WidgetUtils from '../../../utils/WidgetUtils';
|
||||
|
||||
const widgetType = 'm.stickerpicker';
|
||||
|
||||
// We sit in a context menu, so the persisted element container needs to float
|
||||
// above it, so it needs a greater z-index than the ContextMenu
|
||||
const STICKERPICKER_Z_INDEX = 5000;
|
||||
|
||||
export default class Stickerpicker extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
@ -67,7 +71,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
this.setState({showStickers: false});
|
||||
Widgets.removeStickerpickerWidgets().then(() => {
|
||||
WidgetUtils.removeStickerpickerWidgets().then(() => {
|
||||
this.forceUpdate();
|
||||
}).catch((e) => {
|
||||
console.error('Failed to remove sticker picker widget', e);
|
||||
|
@ -119,7 +123,7 @@ export default class Stickerpicker extends React.Component {
|
|||
}
|
||||
|
||||
_updateWidget() {
|
||||
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
|
||||
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
|
||||
this.setState({
|
||||
stickerpickerWidget,
|
||||
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
|
||||
|
@ -211,7 +215,7 @@ export default class Stickerpicker extends React.Component {
|
|||
width: this.popoverWidth,
|
||||
}}
|
||||
>
|
||||
<PersistedElement>
|
||||
<PersistedElement containerId="mx_persisted_stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
|
||||
<AppTile
|
||||
collectWidgetMessaging={this._collectWidgetMessaging}
|
||||
id={stickerpickerWidget.id}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,36 +15,28 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
const React = require('react');
|
||||
const sdk = require('../../../index');
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
import React from 'react';
|
||||
import sdk from '../../../index';
|
||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
||||
module.exports = React.createClass({
|
||||
displayName: 'ChangeDisplayName',
|
||||
|
||||
_getDisplayName: function() {
|
||||
_getDisplayName: async function() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.getProfileInfo(cli.credentials.userId).then(function(result) {
|
||||
let displayname = result.displayname;
|
||||
if (!displayname) {
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
displayname = "Guest " + MatrixClientPeg.get().getUserIdLocalpart();
|
||||
} else {
|
||||
displayname = MatrixClientPeg.get().getUserIdLocalpart();
|
||||
}
|
||||
}
|
||||
return displayname;
|
||||
}, function(error) {
|
||||
try {
|
||||
const res = await cli.getProfileInfo(cli.getUserId());
|
||||
return res.displayname;
|
||||
} catch (e) {
|
||||
throw new Error("Failed to fetch display name");
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_changeDisplayName: function(new_displayname) {
|
||||
_changeDisplayName: function(newDisplayname) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli.setDisplayName(new_displayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name");
|
||||
return cli.setDisplayName(newDisplayname).catch(function(e) {
|
||||
throw new Error("Failed to set display name", e);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,14 +15,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const React = require('react');
|
||||
import PropTypes from 'prop-types';
|
||||
const MatrixClientPeg = require("../../../MatrixClientPeg");
|
||||
const Modal = require("../../../Modal");
|
||||
const sdk = require("../../../index");
|
||||
|
||||
import dis from "../../../dispatcher";
|
||||
import Promise from 'bluebird';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import { _t } from '../../../languageHandler';
|
||||
|
@ -143,6 +143,9 @@ module.exports = React.createClass({
|
|||
});
|
||||
|
||||
cli.setPassword(authDict, newPassword).then(() => {
|
||||
// Notify SessionStore that the user's password was changed
|
||||
dis.dispatch({action: 'password_changed'});
|
||||
|
||||
if (this.props.shouldAskForEmail) {
|
||||
return this._optionallySetEmail().then((confirmed) => {
|
||||
this.props.onFinished({
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
import Resend from './Resend';
|
||||
import sdk from './index';
|
||||
import dis from './dispatcher';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
|
@ -65,6 +66,10 @@ export function getUnknownDevicesForRoom(matrixClient, room) {
|
|||
});
|
||||
}
|
||||
|
||||
function focusComposer() {
|
||||
dis.dispatch({action: 'focus_composer'});
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the UnknownDeviceDialog for a given room. The dialog will inform the user
|
||||
* that messages they sent to this room have not been sent due to unknown devices
|
||||
|
@ -86,6 +91,7 @@ export function showUnknownDeviceDialogForMessages(matrixClient, room) {
|
|||
sendAnywayLabel: _t("Send anyway"),
|
||||
sendLabel: _t("Send"),
|
||||
onSend: onSendClicked,
|
||||
onFinished: focusComposer,
|
||||
}, 'mx_Dialog_unknownDevice');
|
||||
});
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
"Noisy": "Шумна",
|
||||
"Resend": "Паўторна",
|
||||
"On": "Уключыць",
|
||||
"Permalink": "Пастаянная спасылка",
|
||||
"remove %(name)s from the directory.": "выдаліць %(name)s з каталога.",
|
||||
"Off": "Выключыць",
|
||||
"Delete the room alias %(alias)s and remove %(name)s from the directory?": "Выдаліць псеўданім пакоя %(alias)s і выдаліць %(name)s з каталога?",
|
||||
|
|
|
@ -136,7 +136,6 @@
|
|||
"Missing room_id in request": "Липсва room_id в заявката",
|
||||
"Room %(roomId)s not visible": "Стая %(roomId)s не е видима",
|
||||
"Missing user_id in request": "Липсва user_id в заявката",
|
||||
"Failed to lookup current room": "Неуспешно намиране на текущата стая",
|
||||
"/ddg is not a command": "/ddg не е команда",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "За използване, изчакайте зареждането на списъка с предложения и изберете от него.",
|
||||
"Unrecognised room alias:": "Непознат псевдоним на стая:",
|
||||
|
@ -204,9 +203,7 @@
|
|||
"Not a valid Riot keyfile": "Невалиден файл с ключ за Riot",
|
||||
"Authentication check failed: incorrect password?": "Неуспешна автентикация: неправилна парола?",
|
||||
"Failed to join room": "Неуспешно присъединяване към стаята",
|
||||
"Message Replies": "Отговори на съобщението",
|
||||
"Message Pinning": "Функция за закачане на съобщения",
|
||||
"Tag Panel": "Панел с етикети",
|
||||
"Disable Emoji suggestions while typing": "Изключване на предложенията за емотиконите при писане",
|
||||
"Use compact timeline layout": "Използване на компактно оформление за списъка със съобщения",
|
||||
"Hide removed messages": "Скриване на премахнати съобщения",
|
||||
|
@ -223,7 +220,6 @@
|
|||
"Automatically replace plain text Emoji": "Автоматично откриване и заместване на емотикони в текста",
|
||||
"Mirror local video feed": "Показвай ми огледално моя видео образ",
|
||||
"Disable Peer-to-Peer for 1:1 calls": "Изключване на Peer-to-Peer в 1:1 разговор",
|
||||
"Opt out of analytics": "Отказване от събиране на статистически данни",
|
||||
"Never send encrypted messages to unverified devices from this device": "Никога не изпращай шифровани съобщения от това устройство до непотвърдени устройства",
|
||||
"Never send encrypted messages to unverified devices in this room from this device": "Никога не изпращай шифровани съобщения от това устройство до непотвърдени устройства в тази стая",
|
||||
"Enable inline URL previews by default": "Включване по подразбиране на URL прегледи",
|
||||
|
@ -270,7 +266,7 @@
|
|||
"Enable Notifications": "Включване на известия",
|
||||
"Cannot add any more widgets": "Не могат да се добавят повече приспособления",
|
||||
"The maximum permitted number of widgets have already been added to this room.": "Максимално разрешеният брой приспособления е вече добавен към тази стая.",
|
||||
"Add a widget": "Добавяне на приспособление",
|
||||
"Add a widget": "Добави приспособление",
|
||||
"Drop File Here": "Пусни файла тук",
|
||||
"Drop file here to upload": "Пуснете файла тук, за да се качи",
|
||||
" (unsupported)": " (не се поддържа)",
|
||||
|
@ -492,7 +488,6 @@
|
|||
"Decrypt %(text)s": "Разшифровай %(text)s",
|
||||
"Download %(text)s": "Изтегли %(text)s",
|
||||
"(could not connect media)": "(неуспешно свързване на медийните устройства)",
|
||||
"Must be viewing a room": "Трябва да извършите това в стая",
|
||||
"Usage": "Употреба",
|
||||
"Remove from community": "Премахни от общността",
|
||||
"Disinvite this user from community?": "Оттегляне на поканата към този потребител от общността?",
|
||||
|
@ -521,8 +516,6 @@
|
|||
"Community %(groupId)s not found": "Общност %(groupId)s не е намерена",
|
||||
"Create a new community": "Създаване на нова общност",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Създайте общност, за да групирате потребители и стаи! Изградете персонализирана начална страница, за да маркирате своето пространство в Matrix Вселената.",
|
||||
"Join an existing community": "Присъединяване към съществуваща общност",
|
||||
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "За да се присъедините към вече съществуваща общност, трябва да знаете нейния идентификатор; той изглежда нещо подобно на <i>+example:matrix.org</i>.",
|
||||
"Unknown (user, device) pair:": "Непозната двойка (потребител, устройство):",
|
||||
"The signing key you provided matches the signing key you received from %(userId)s's device %(deviceId)s. Device marked as verified.": "Подписващият ключ, който сте предоставили, съвпада с подписващия ключ, който сте получили от устройството %(deviceId)s на %(userId)s. Устройството е маркирано като потвърдено.",
|
||||
"Hide avatars in user and room mentions": "Скриване на аватара на потребители и стаи при споменаването им",
|
||||
|
@ -532,8 +525,6 @@
|
|||
"You should not yet trust it to secure data": "Все още не трябва да се доверявате на това, че ще защити Вашите данни",
|
||||
"Invalid file%(extra)s": "Невалиден файл%(extra)s",
|
||||
"Error decrypting image": "Грешка при разшифроване на снимка",
|
||||
"This image cannot be displayed.": "Тази снимка не може да бъде показана.",
|
||||
"Image '%(Body)s' cannot be displayed.": "Снимката '%(Body)s' не може да бъде показана.",
|
||||
"Error decrypting video": "Грешка при разшифроване на видео",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s промени аватара на %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s премахна аватара на стаята.",
|
||||
|
@ -697,8 +688,6 @@
|
|||
"Incorrect password": "Неправилна парола",
|
||||
"Deactivate Account": "Деактивация на профила",
|
||||
"Deactivate my account": "Деактивирай моя профил",
|
||||
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Това ще направи профила Ви перманентно неизползваем. Няма да можете да се регистрирате отново със същия потребителски идентификатор.",
|
||||
"This action is irreversible.": "Това действие е необратимо.",
|
||||
"Device name": "Име на устройство",
|
||||
"Device key": "Ключ на устройство",
|
||||
"In future this verification process will be more sophisticated.": "В бъдеще този процес на потвърждение ще бъде по-лесен.",
|
||||
|
@ -713,9 +702,7 @@
|
|||
"Ignore request": "Игнорирай поканата",
|
||||
"Loading device info...": "Зареждане на информация за устройството...",
|
||||
"Encryption key request": "Заявка за ключ за шифроване",
|
||||
"Otherwise, <a>click here</a> to send a bug report.": "В противен случай, <a>натиснете тук</a>, за да изпратите съобщение за грешка.",
|
||||
"Unable to restore session": "Неуспешно възстановяване на сесията",
|
||||
"Continue anyway": "Продължи въпреки това",
|
||||
"Invalid Email Address": "Невалиден имейл адрес",
|
||||
"This doesn't appear to be a valid email address": "Това не изглежда да е валиден имейл адрес",
|
||||
"Please check your email and click on the link it contains. Once this is done, click continue.": "Моля, проверете своя имейл адрес и натиснете връзката, която той съдържа. След като направите това, натиснете продължи.",
|
||||
|
@ -895,7 +882,6 @@
|
|||
"This homeserver doesn't offer any login flows which are supported by this client.": "Този Home сървър не предлага методи за влизане, които се поддържат от този клиент.",
|
||||
"Error: Problem communicating with the given homeserver.": "Грешка: Проблем при комуникацията с дадения Home сървър.",
|
||||
"Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or <a>enable unsafe scripts</a>.": "Не е възможно свързване към Home сървъра чрез HTTP, когато има HTTPS адрес в лентата на браузъра Ви. Или използвайте HTTPS или <a>включете функция небезопасни скриптове</a>.",
|
||||
"Login as guest": "Влез като гост",
|
||||
"Sign in to get started": "Влезте в профила си, за да започнете",
|
||||
"Set a display name:": "Задаване на име:",
|
||||
"Upload an avatar:": "Качване на профилна снимка:",
|
||||
|
@ -962,7 +948,6 @@
|
|||
"Are you sure you wish to remove (delete) this event? Note that if you delete a room name or topic change, it could undo the change.": "Сигурни ли сте, че искате да премахнете (изтриете) това събитие? Забележете, че ако изтриете събитие за промяна на името на стая или тема, това може да обърне промяната.",
|
||||
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "За да потвърдите, че на това устройство може да се вярва, моля свържете се със собственика му по друг начин (напр. на живо или чрез телефонен разговор) и го попитайте дали ключът, който той вижда в неговите настройки на потребителя за това устройство, съвпада с ключа по-долу:",
|
||||
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Ако съвпада, моля натиснете бутона за потвърждение по-долу. Ако не, то тогава някой друг имитира това устройство и вероятно искате вместо това да натиснете бутона за черен списък.",
|
||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Засякохме грешка при опита за възстановяване на предишната Ви сесия. Ако продължите, ще трябва да влезете в профила си отново. Шифрованата история на чата няма да бъде четима.",
|
||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Ако преди сте използвали по-нова версия на Riot, Вашата сесия може да не бъде съвместима с текущата версия. Затворете този прозорец и се върнете в по-новата версия.",
|
||||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Това ще бъде името на профила Ви на <span></span> Home сървъра, или можете да изберете <a>друг сървър</a>.",
|
||||
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Препоръчваме Ви да минете през процеса за потвърждение за всяко устройство, за да потвърдите, че принадлежат на легитимен собственик. Ако предпочитате, можете да изпратите съобщение без потвърждение.",
|
||||
|
@ -996,7 +981,6 @@
|
|||
"Join this community": "Присъединете се в тази общност",
|
||||
"Leave this community": "Напуснете тази общност",
|
||||
"Stickerpack": "Пакет със стикери",
|
||||
"Sticker Messages": "Съобщения със стикери",
|
||||
"You don't currently have any stickerpacks enabled": "В момента нямате включени пакети със стикери",
|
||||
"Add a stickerpack": "Добави пакет със стикери",
|
||||
"Hide Stickers": "Скрий стикери",
|
||||
|
@ -1108,7 +1092,6 @@
|
|||
"Notify me for anything else": "Извести ме за всичко останало",
|
||||
"When I'm invited to a room": "Когато ме поканят в стая",
|
||||
"Keywords": "Ключови думи",
|
||||
"<a>Click here</a> to create a GitHub issue.": "<a>Натиснете тук</a>, за да създадете GitHub issue.",
|
||||
"Can't update user notification settings": "Неуспешно обновяване на потребителски настройки за известяване",
|
||||
"Notify for all other messages/rooms": "Извести ме за всички други съобщения/стаи",
|
||||
"Unable to look up room ID from server": "Стая с такъв идентификатор не е намерена на сървъра",
|
||||
|
@ -1135,7 +1118,6 @@
|
|||
"Set Password": "Задаване на парола",
|
||||
"An error occurred whilst saving your email notification preferences.": "Възникна грешка при запазване на настройките за имейл известяване.",
|
||||
"Enable audible notifications in web client": "Включване на звукови известия в уеб клиент",
|
||||
"Permalink": "Permalink",
|
||||
"Off": "Изключено",
|
||||
"Riot does not know how to join a room on this network": "Riot не знае как да се присъедини към стая от тази мрежа",
|
||||
"Mentions only": "Само при споменаване",
|
||||
|
@ -1167,5 +1149,64 @@
|
|||
"At this time it is not possible to reply with a file so this will be sent without being a reply.": "В момента не може да се отговаря с файл, така че това ще се изпрати без да бъде отговор.",
|
||||
"Unable to reply": "Не може да се отговори",
|
||||
"At this time it is not possible to reply with an emote.": "В момента не може да се отговори с емотикона.",
|
||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Не може да се зареди събитието, на което е отговорено. Или не съществува или нямате достъп да го видите."
|
||||
"Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Не може да се зареди събитието, на което е отговорено. Или не съществува или нямате достъп да го видите.",
|
||||
"Popout widget": "Изкарай в нов прозорец",
|
||||
"Log out and remove encryption keys?": "Изход и изтриване на ключовете за шифроване?",
|
||||
"Clear Storage and Sign Out": "Изчисти запазените данни и излез",
|
||||
"Send Logs": "Изпрати логове",
|
||||
"Refresh": "Опресни",
|
||||
"We encountered an error trying to restore your previous session.": "Възникна грешка при възстановяване на предишната Ви сесия.",
|
||||
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Изчистване на запазените данни в браузъра може да поправи проблема, но ще Ви изкара от профила и ще направи шифрованите съобщения нечетими.",
|
||||
"Collapse Reply Thread": "Свий отговорите",
|
||||
"Enable widget screenshots on supported widgets": "Включи скрийншоти за поддържащи ги приспособления",
|
||||
"Riot bugs are tracked on GitHub: <a>create a GitHub issue</a>.": "Бъговете по Riot се следят в GitHub: <a>създайте проблем в GitHub</a>.",
|
||||
"e.g. %(exampleValue)s": "напр. %(exampleValue)s",
|
||||
"Reload widget": "Презареди приспособлението",
|
||||
"Send analytics data": "Изпращане на статистически данни",
|
||||
"To notify everyone in the room, you must be a": "За да уведомите всички в стаята, трябва да бъдете",
|
||||
"Muted Users": "Заглушени потребители",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Моля, помогнете за подобряването на Riot.im като изпращате <UsageDataLink>анонимни данни за ползване</UsageDataLink>. Това ще използва бисквитка (моля, вижте нашата <PolicyLink>политика за бисквитки</PolicyLink>).",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Моля, помогнете за подобряването на Riot.im като изпращате <UsageDataLink>анонимни данни за ползване</UsageDataLink>. Това ще използва бисквитка.",
|
||||
"Yes, I want to help!": "Да, искам да помогна!",
|
||||
"Warning: This widget might use cookies.": "Внимание: това приспособление може да използва бисквитки.",
|
||||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Това ще направи акаунта Ви неизползваем завинаги. Няма да можете да влезете пак, а регистрирането повторно на същия потребителски идентификатор няма да е възможно. Акаунтът Ви да напусне всички стаи, в които участва. Ще бъдат премахнати и данните за акаунта Ви от сървъра за самоличност. <b>Действието е необратимо.</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Деактивирането на акаунта Ви <b>по подразбиране не прави така, че изпратените съобщения да бъдат забравени.</b> Ако искате да забравим съобщенията Ви, моля отбележете с отметка по-долу.",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Видимостта на съобщенията в Matrix е подобно на имейл системата. Нашето забравяне означава, че: изпратените от Вас съобщения няма да бъдат споделяни с нови или нерегистрирани потребители, но регистрираните потребители имащи достъп до тях ще продължат да имат достъп до своето копие.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Моля, забравете всички изпратени от мен съобщения, когато акаунта ми се деактивира (<b>Внимание:</b> това ще направи бъдещите потребители да имат само частичен поглед върху кореспонденцията)",
|
||||
"To continue, please enter your password:": "За да продължите, моля въведете паролата си:",
|
||||
"password": "парола",
|
||||
"Can't leave Server Notices room": "Не може да напуснете стая \"Server Notices\"",
|
||||
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Тази стая се използва за важни съобщения от сървъра, така че не можете да я напуснете.",
|
||||
"Terms and Conditions": "Правила и условия",
|
||||
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "За да продължите да ползвате %(homeserverDomain)s е необходимо да прегледате и да се съгласите с правилата и условията за ползване.",
|
||||
"Review terms and conditions": "Прегледай правилата и условията",
|
||||
"Failed to indicate account erasure": "Неуспешно указване на желанието за изтриване на акаунта",
|
||||
"Try the app first": "Първо пробвайте приложението",
|
||||
"Encrypting": "Шифроване",
|
||||
"Encrypted, not sent": "Шифровано, неизпратено",
|
||||
"Share Link to User": "Сподели връзка с потребител",
|
||||
"Share room": "Сподели стая",
|
||||
"Share Room": "Споделяне на стая",
|
||||
"Link to most recent message": "Създай връзка към най-новото съобщение",
|
||||
"Share User": "Споделяне на потребител",
|
||||
"Share Community": "Споделяне на общност",
|
||||
"Share Room Message": "Споделяне на съобщение от стая",
|
||||
"Link to selected message": "Създай връзка към избраното съобщение",
|
||||
"COPY": "КОПИРАЙ",
|
||||
"Share Message": "Сподели съобщението",
|
||||
"No Audio Outputs detected": "Не са открити аудио изходи",
|
||||
"Audio Output": "Аудио изходи",
|
||||
"Jitsi Conference Calling": "Jitsi конферентни разговори",
|
||||
"Call in Progress": "Тече разговор",
|
||||
"A call is already in progress!": "В момента вече тече разговор!",
|
||||
"You have no historical rooms": "Нямате стаи в архива",
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "В шифровани стаи като тази, по подразбиране URL прегледите са изключени, за да се подсигури че сървърът (където става генерирането на прегледите) не може да събира информация за връзките споделени в стаята.",
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Когато някой сподели URL връзка в съобщение, може да бъде показан URL преглед даващ повече информация за връзката (заглавие, описание и картинка от уебсайта).",
|
||||
"The email field must not be blank.": "Имейл полето не може да бъде празно.",
|
||||
"The user name field must not be blank.": "Полето за потребителско име не може да е празно.",
|
||||
"The phone number field must not be blank.": "Полето за телефонен номер не може да е празно.",
|
||||
"The password field must not be blank.": "Полето за парола не може да е празно.",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Не можете да изпращате съобщения докато не прегледате и се съгласите с <consentLink>нашите правила и условия</consentLink>.",
|
||||
"Demote yourself?": "Понижете себе си?",
|
||||
"Demote": "Понижение"
|
||||
}
|
||||
|
|
|
@ -134,10 +134,8 @@
|
|||
"You are not in this room.": "No heu entrat a aquesta sala.",
|
||||
"You do not have permission to do that in this room.": "No teniu el permís per realitzar aquesta acció en aquesta sala.",
|
||||
"Missing room_id in request": "Falta l'ID de la sala en la vostra sol·licitud",
|
||||
"Must be viewing a room": "Hauríeu de veure una sala",
|
||||
"Room %(roomId)s not visible": "La sala %(roomId)s no és visible",
|
||||
"Missing user_id in request": "Falta l'ID d'usuari a la vostre sol·licitud",
|
||||
"Failed to lookup current room": "No s'ha pogut buscar la sala actual",
|
||||
"Usage": "Ús",
|
||||
"/ddg is not a command": "/ddg no és un comandament",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "Per utilitzar-lo, simplement espereu que es completin els resultats automàticament i seleccioneu-ne el desitjat.",
|
||||
|
@ -211,9 +209,7 @@
|
|||
"Not a valid Riot keyfile": "El fitxer no és un fitxer de claus de Riot valid",
|
||||
"Authentication check failed: incorrect password?": "Ha fallat l'autenticació: heu introduït correctament la contrasenya?",
|
||||
"Failed to join room": "No s'ha pogut entrar a la sala",
|
||||
"Message Replies": "Respostes del missatge",
|
||||
"Message Pinning": "Fixació de missatges",
|
||||
"Tag Panel": "Tauler d'etiquetes",
|
||||
"Disable Emoji suggestions while typing": "Desactiva els suggeriments d'Emoji mentre s'escriu",
|
||||
"Use compact timeline layout": "Utilitza el disseny compacte de la línia de temps",
|
||||
"Hide join/leave messages (invites/kicks/bans unaffected)": "Amaga els missatges d'entrada i sortida (no afecta a les invitacions, expulsions o prohibicions)",
|
||||
|
@ -348,7 +344,6 @@
|
|||
"Hide Text Formatting Toolbar": "Amaga la barra d'eines de format de text",
|
||||
"Server error": "S'ha produït un error al servidor",
|
||||
"Mirror local video feed": "Mostra el vídeo local com un mirall",
|
||||
"Opt out of analytics": "No acceptis analítiques",
|
||||
"Server unavailable, overloaded, or something else went wrong.": "El servidor no està disponible, està sobrecarregat o alguna altra cosa no ha funcionat correctament.",
|
||||
"Command error": "S'ha produït un error en l'ordre",
|
||||
"bold": "negreta",
|
||||
|
@ -499,8 +494,6 @@
|
|||
"Download %(text)s": "Baixa %(text)s",
|
||||
"Invalid file%(extra)s": "Fitxer invàlid%(extra)s",
|
||||
"Error decrypting image": "S'ha produït un error en desencriptar la imatge",
|
||||
"Image '%(Body)s' cannot be displayed.": "La imatge '%(Body)s' no es pot mostrar.",
|
||||
"This image cannot be displayed.": "Aquesta imatge no es pot mostrar.",
|
||||
"Error decrypting video": "S'ha produït un error en desencriptar el vídeo",
|
||||
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s ha canviat el seu avatar per a la sala %(roomName)s",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s ha eliminat l'avatar de la sala.",
|
||||
|
@ -582,7 +575,6 @@
|
|||
"%(nameList)s %(transitionList)s": "%(transitionList)s%(nameList)s",
|
||||
"%(severalUsers)sjoined %(count)s times|one": "%(severalUsers)s han entrat",
|
||||
"Guest access is disabled on this Home Server.": "L'accés a usuaris d'altres xarxes no està permès en aquest servidor.",
|
||||
"Login as guest": "Inicia sessió com a convidat",
|
||||
"Unblacklist": "Treure de la llista negre",
|
||||
"%(oneUser)sjoined %(count)s times|one": "%(oneUser)s s'ha unit",
|
||||
"%(severalUsers)sleft %(count)s times|one": "%(severalUsers)s han sortit",
|
||||
|
@ -677,8 +669,6 @@
|
|||
"Unknown error": "S'ha produït un error desconegut",
|
||||
"Incorrect password": "Contrasenya incorrecta",
|
||||
"Deactivate Account": "Desactivar el compte",
|
||||
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Això farà que el vostre compte no es pugui utilitzar mai més. No podreu tornar a registrar la mateixa identificació d'usuari.",
|
||||
"This action is irreversible.": "Aquesta acció és irreversible.",
|
||||
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Per verificar que es pot confiar en aquest dispositiu, poseu-vos en contacte amb el propietari mitjançant altres mitjans (per exemple, en persona o amb una trucada telefònica) i pregunteu-li si la clau que veuen a la configuració del seu usuari, aquest dispositiu coincideix amb la següent clau:",
|
||||
"Device name": "Nom del dispositiu",
|
||||
"Device key": "Clau del dispositiu",
|
||||
|
@ -694,9 +684,7 @@
|
|||
"Ignore request": "Ignora la sol·licitud",
|
||||
"Loading device info...": "S'està carregant la informació del dispositiu...",
|
||||
"Encryption key request": "Sol·licitud de claus",
|
||||
"Otherwise, <a>click here</a> to send a bug report.": "D'una altra manera, <a>click here</a> per a enviar un informe d'error.",
|
||||
"Unable to restore session": "No s'ha pogut restaurar la sessió",
|
||||
"Continue anyway": "Continua de totes maneres",
|
||||
"Invalid Email Address": "El correu electrònic no és vàlid",
|
||||
"This doesn't appear to be a valid email address": "Aquest no sembla ser un correu electrònic vàlid",
|
||||
"Verification Pending": "Verificació pendent",
|
||||
|
@ -714,7 +702,6 @@
|
|||
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "Aquest serà el nom del seu compte al <span></span> servidor amfitrió, o bé trieu-ne un altre <a>different server</a>.",
|
||||
"If you already have a Matrix account you can <a>log in</a> instead.": "Si ja teniu un compte a Matrix, podeu <a>log in</a>.",
|
||||
"Block users on other matrix homeservers from joining this room": "Impedeix als usuaris d'altres servidors de Matrix d'entrar a aquesta sala",
|
||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Hem trobat un error en intentar restaurar la vostra sessió anterior. Si continueu, haureu d'iniciar la sessió de nou i l'historial de xat encriptat serà il·legible.",
|
||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Si anteriorment heu utilitzat un versió de Riot més recent, la vostra sessió podría ser incompatible amb aquesta versió. Tanqueu aquesta finestra i torneu a la versió més recent.",
|
||||
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Actualment teniu a la llista negre els dispositius no verificats; per enviar missatges a aquests dispositius, els heu de verificar abans.",
|
||||
"We recommend you go through the verification process for each device to confirm they belong to their legitimate owner, but you can resend the message without verifying if you prefer.": "Recomanem que dugueu a terme el procès de verificació per a cada dispositiu per tal de confirmar que són del legítim propietari, però podeu enviar el missatge sense verificar-ho si ho preferiu.",
|
||||
|
@ -780,8 +767,6 @@
|
|||
"Error whilst fetching joined communities": "S'ha produït un error en buscar comunitats unides",
|
||||
"Create a new community": "Crea una nova comunitat",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Crea una comunitat per agrupar usuaris i sales! Creeu una pàgina d'inici personalitzada per definir el vostre espai a l'univers Matrix.",
|
||||
"Join an existing community": "Uneix-te a una comunitat existent",
|
||||
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "Per unir-se a una comunitat existent, haureu de conèixer l'identificador de la comunitat; això es veurà com <i>+exemple:matrix.org</i>.",
|
||||
"You have no visible notifications": "No teniu cap notificació visible",
|
||||
"Scroll to bottom of page": "Desplaça't fins a la part inferior de la pàgina",
|
||||
"Message not sent due to unknown devices being present": "El missatge no s'ha enviat perquè hi ha dispositius desconeguts presents",
|
||||
|
@ -986,7 +971,6 @@
|
|||
"Notify me for anything else": "Notifica'm per a qualsevol altra cosa",
|
||||
"View Source": "Mostra el codi",
|
||||
"Keywords": "Paraules clau",
|
||||
"<a>Click here</a> to create a GitHub issue.": "<a>Clica aquí</a> per crear una issue a GitHub.",
|
||||
"Can't update user notification settings": "No es poden actualitzar els paràmetres de les notificacions de l'usuari",
|
||||
"Notify for all other messages/rooms": "Notifica per a tots els altres missatges o sales",
|
||||
"Unable to look up room ID from server": "No s'ha pogut cercar l'ID de la sala en el servidor",
|
||||
|
@ -1013,7 +997,6 @@
|
|||
"Unable to fetch notification target list": "No s'ha pogut obtenir la llista d'objectius de les notificacions",
|
||||
"Set Password": "Establiu una contrasenya",
|
||||
"Enable audible notifications in web client": "Habilita les notificacions d'àudio al client web",
|
||||
"Permalink": "Enllaç permanent",
|
||||
"Off": "Apagat",
|
||||
"Riot does not know how to join a room on this network": "El Riot no sap com unir-se a una sala en aquesta xarxa",
|
||||
"Mentions only": "Només mencions",
|
||||
|
|
|
@ -239,7 +239,6 @@
|
|||
"Level:": "Úroveň:",
|
||||
"Local addresses for this room:": "Místní adresy této místnosti:",
|
||||
"Logged in as:": "Přihlášen/a jako:",
|
||||
"Login as guest": "Přihlášen/a jako host",
|
||||
"matrix-react-sdk version:": "Verze matrix-react-sdk:",
|
||||
"Mobile phone number": "Číslo mobilního telefonu",
|
||||
"Mobile phone number (optional)": "Číslo mobilního telefonu (nepovinné)",
|
||||
|
@ -262,7 +261,6 @@
|
|||
"olm version:": "verze olm:",
|
||||
"Once encryption is enabled for a room it cannot be turned off again (for now)": "Jakmile je jednou šifrování v místnosti zapnuto, nelze už vypnout (prozatím)",
|
||||
"Only people who have been invited": "Pouze lidé, kteří byli pozváni",
|
||||
"Otherwise, <a>click here</a> to send a bug report.": "V opačném případě <a>klikněte zde</a> a pošlete hlášení o chybě.",
|
||||
"Password": "Heslo",
|
||||
"Password:": "Heslo:",
|
||||
"Passwords can't be empty": "Hesla nemohou být prázdná",
|
||||
|
@ -494,8 +492,6 @@
|
|||
"You need to be able to invite users to do that.": "Pro tuto akci musíte mít právo zvát uživatele.",
|
||||
"Delete Widget": "Smazat widget",
|
||||
"Error decrypting image": "Chyba při dešifrování obrázku",
|
||||
"Image '%(Body)s' cannot be displayed.": "Obrázek '%(Body)s' nemůže být zobrazen.",
|
||||
"This image cannot be displayed.": "Tento obrázek nelze zobrazit.",
|
||||
"Error decrypting video": "Chyba při dešifrování videa",
|
||||
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odstranil/a avatar místnosti.",
|
||||
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s změnil/a avatar místnosti na <img/>",
|
||||
|
@ -636,9 +632,7 @@
|
|||
"Show these rooms to non-members on the community page and room list?": "Zobrazovat tyto místnosti na domovské stránce skupiny a v seznamu místností i pro nečleny?",
|
||||
"Restricted": "Omezené",
|
||||
"Missing room_id in request": "V zadání chybí room_id",
|
||||
"Must be viewing a room": "Musí být zobrazena místnost",
|
||||
"Missing user_id in request": "V zadání chybí user_id",
|
||||
"Failed to lookup current room": "Nepodařilo se vyhledat aktuální místnost",
|
||||
"(could not connect media)": "(média se nepodařilo spojit)",
|
||||
"%(senderName)s placed a %(callType)s call.": "%(senderName)s uskutečnil %(callType)s hovor.",
|
||||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s zpřístupnil budoucí historii místnosti neznámým (%(visibility)s).",
|
||||
|
@ -650,7 +644,6 @@
|
|||
"Disable big emoji in chat": "Zakázat velké Emoji v konverzaci",
|
||||
"Mirror local video feed": "Zrcadlit lokání video",
|
||||
"Disable Peer-to-Peer for 1:1 calls": "Zakázat Peer-to-Peer pro 1:1 hovory",
|
||||
"Opt out of analytics": "Odhlásit se z analytiky údajů",
|
||||
"Never send encrypted messages to unverified devices from this device": "Z tohoto zařízení nikdy neodesílat šifrované zprávy na neověřená zařízení",
|
||||
"Enable inline URL previews by default": "Nastavit povolení náhledů URL adres jako výchozí",
|
||||
"Enable URL previews for this room (only affects you)": "Povolit náhledy URL adres pro tuto místnost (ovlivňuje pouze vás)",
|
||||
|
@ -792,8 +785,6 @@
|
|||
"This setting cannot be changed later!": "Toto nastavení nelze v budoucnu změnit!",
|
||||
"Unknown error": "Neznámá chyba",
|
||||
"Incorrect password": "Nesprávné heslo",
|
||||
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Toto způsobí, že váš účet nebude již nikdy použitelný. Zároveň nebude možné se znovu zaregistrovat pod stejným uživatelským ID.",
|
||||
"This action is irreversible.": "Tuto operaci nebude možné vrátit zpět.",
|
||||
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Pokud si chcete ověřit, zda je zařízení skutečně důvěryhodné, kontaktujte vlastníka jiným způsobem (např. osobně anebo telefonicky) a zeptejte se ho na klíč, který má pro toto zařízení zobrazený v nastavení a zda se shoduje s klíčem zobrazeným níže:",
|
||||
"Device name": "Název zařízení",
|
||||
"Device key": "Klíč zařízení",
|
||||
|
@ -808,9 +799,7 @@
|
|||
"Ignore request": "Ignorovat žádost",
|
||||
"Encryption key request": "Žádost o šifrovací klíč",
|
||||
"Unable to restore session": "Nelze obnovit relaci",
|
||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Při pokusu o obnovení vaší předcházející relace se vyskytla chyba. Pokud budete pokračovat musíte se znovu přihlásit a historie šifrovaného rozhovoru nebude již dostupná.",
|
||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Pokud jste se v minulosti již přihlásili s novější verzi programu Riot, vaše relace nemusí být kompatibilní s touto verzí. Zavřete prosím toto okno a přihlaste se znovu pomocí nové verze.",
|
||||
"Continue anyway": "Přesto pokračovat",
|
||||
"Please check your email and click on the link it contains. Once this is done, click continue.": "Prosím, zkontrolujte si email a klikněte na odkaz ve zprávě, kterou jsme vám zaslali. V případě, že jste tak již učinili, klikněte na tlačítko Pokračovat.",
|
||||
"This will allow you to reset your password and receive notifications.": "Toto vám umožní obnovit si heslo a přijímat oznámení emailem.",
|
||||
"Skip": "Přeskočit",
|
||||
|
@ -868,8 +857,6 @@
|
|||
"Error whilst fetching joined communities": "Při získávání vašich skupin se vyskytla chyba",
|
||||
"Create a new community": "Vytvořit novou skupinu",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Vytvořte skupinu s cílem seskupit uživatele a místnosti! Vytvořte si vlastní domovskou stránku a vymezte tak váš prostor ve světe Matrix.",
|
||||
"Join an existing community": "Vstoupit do existující skupiny",
|
||||
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "Aby jste mohli vstoupit do existující skupiny, musíte znát její identifikátor; Měl by vypadat asi takto <i>+priklad:matrix.org</i>.",
|
||||
"You have no visible notifications": "Nejsou dostupná žádná oznámení",
|
||||
"Connectivity to the server has been lost.": "Spojení se serverem bylo přerušené.",
|
||||
"Sent messages will be stored until your connection has returned.": "Odeslané zprávy zůstanou uložené, dokud se spojení znovu neobnoví.",
|
||||
|
@ -927,7 +914,6 @@
|
|||
"Claimed Ed25519 fingerprint key": "Údajný klíč s otiskem prstu Ed25519",
|
||||
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Tento proces vás provede importem šifrovacích klíčů, které jste si stáhli z jiného Matrix klienta. Po úspěšném naimportování budete v tomto klientovi moci dešifrovat všechny zprávy, které jste mohli dešifrovat v původním klientovi.",
|
||||
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "Stažený soubor je chráněn heslem. Soubor můžete naimportovat pouze pokud zadáte odpovídající heslo.",
|
||||
"Tag Panel": "Připnout panel",
|
||||
"Call Failed": "Hovor selhal",
|
||||
"There are unknown devices in this room: if you proceed without verifying them, it will be possible for someone to eavesdrop on your call.": "V této místnosti jsou neznámá zařízení: Pokud budete pokračovat bez jejich ověření, někdo může Váš hovor odposlouchávat.",
|
||||
"Review Devices": "Ověřit zařízení",
|
||||
|
@ -1070,7 +1056,6 @@
|
|||
"Set Password": "Nastavit heslo",
|
||||
"An error occurred whilst saving your email notification preferences.": "Při ukládání nastavení e-mailových upozornění nastala chyba.",
|
||||
"Enable audible notifications in web client": "Povolit zvuková upozornění ve webové aplikaci",
|
||||
"Permalink": "Trvalý odkaz",
|
||||
"Off": "Vypnout",
|
||||
"#example": "#příklad",
|
||||
"Mentions only": "Pouze zmínky",
|
||||
|
@ -1091,5 +1076,9 @@
|
|||
"Collapse panel": "Sbalit panel",
|
||||
"With your current browser, the look and feel of the application may be completely incorrect, and some or all features may not function. If you want to try it anyway you can continue, but you are on your own in terms of any issues you may encounter!": "Vzhled a chování aplikace může být ve vašem aktuální prohlížeči nesprávné a některé nebo všechny funkce mohou být chybné. Chcete-li i přes to pokračovat, nebudeme vám bránit, ale se všemi problémy, na které narazíte, si musíte poradit sami!",
|
||||
"Checking for an update...": "Kontrola aktualizací...",
|
||||
"There are advanced notifications which are not shown here": "Jsou k dispozici pokročilá upozornění, která zde nejsou zobrazena"
|
||||
"There are advanced notifications which are not shown here": "Jsou k dispozici pokročilá upozornění, která zde nejsou zobrazena",
|
||||
"The platform you're on": "Platforma na které jsi",
|
||||
"The version of Riot.im": "Verze Riot.im",
|
||||
"Whether or not you're logged in (we don't record your user name)": "Jestli jsi, nebo nejsi přihlášen (tvou přezdívku neukládáme)",
|
||||
"Your language of choice": "Tvá jazyková volba"
|
||||
}
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
"Searches DuckDuckGo for results": "Søger DuckDuckGo for resultater",
|
||||
"Commands": "kommandoer",
|
||||
"Emoji": "Emoji",
|
||||
"Login as guest": "Log ind som gæst",
|
||||
"Sign in": "Log ind",
|
||||
"Warning!": "Advarsel!",
|
||||
"Account": "Konto",
|
||||
|
@ -194,10 +193,8 @@
|
|||
"You are not in this room.": "Du er ikke i dette rum.",
|
||||
"You do not have permission to do that in this room.": "Du har ikke tilladelse til at gøre dét i dette rum.",
|
||||
"Missing room_id in request": "Mangler room_id i forespørgsel",
|
||||
"Must be viewing a room": "Du skal være i gang med at se på rummet",
|
||||
"Room %(roomId)s not visible": "rum %(roomId)s ikke synligt",
|
||||
"Missing user_id in request": "Manglende user_id i forespørgsel",
|
||||
"Failed to lookup current room": "Kunne ikke slå nuværende rum op",
|
||||
"Usage": "Brug",
|
||||
"/ddg is not a command": "/ddg er ikke en kommando",
|
||||
"To use it, just wait for autocomplete results to load and tab through them.": "For at bruge det skal du bare vente på autocomplete resultaterne indlæser og tab'e igennem dem.",
|
||||
|
@ -371,7 +368,6 @@
|
|||
"Unable to fetch notification target list": "Kan ikke hente meddelelsesmålliste",
|
||||
"Set Password": "Indstil Password",
|
||||
"Enable audible notifications in web client": "Aktivér hørbare underretninger i webklienten",
|
||||
"Permalink": "Permanent link",
|
||||
"Resend": "Send igen",
|
||||
"Riot does not know how to join a room on this network": "Riot ved ikke, hvordan man kan deltage i et rum på dette netværk",
|
||||
"Mentions only": "Kun nævninger",
|
||||
|
@ -393,7 +389,6 @@
|
|||
"There are advanced notifications which are not shown here": "Der er avancerede meddelelser, som ikke vises her",
|
||||
"%(count)s Members|other": "%(count)s medlemmer",
|
||||
"Logs sent": "Logfiler sendt",
|
||||
"<a>Click here</a> to create a GitHub issue.": "<a>Klik her</a> for at oprette et GitHub-issue.",
|
||||
"Reply": "Besvar",
|
||||
"All messages (noisy)": "Alle meddelelser (højlydt)",
|
||||
"GitHub issue link:": "Link til GitHub issue:",
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
"Searches DuckDuckGo for results": "Verwendet DuckDuckGo für Suchergebnisse",
|
||||
"Commands": "Kommandos",
|
||||
"Emoji": "Emoji",
|
||||
"Login as guest": "Als Gast anmelden",
|
||||
"Sign in": "Anmelden",
|
||||
"Warning!": "Warnung!",
|
||||
"Error": "Fehler",
|
||||
|
@ -250,7 +249,6 @@
|
|||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s hat das Thema geändert in \"%(topic)s\".",
|
||||
"/ddg is not a command": "/ddg ist kein Kommando",
|
||||
"%(senderName)s ended the call.": "%(senderName)s hat den Anruf beendet.",
|
||||
"Failed to lookup current room": "Fehler beim Nachschlagen des Raums",
|
||||
"Failed to send request.": "Anfrage konnte nicht gesendet werden.",
|
||||
"%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s": "%(userId)s von %(fromPowerLevel)s zu %(toPowerLevel)s",
|
||||
"%(senderName)s invited %(targetName)s.": "%(senderName)s hat %(targetName)s eingeladen.",
|
||||
|
@ -265,7 +263,6 @@
|
|||
"%(senderName)s made future room history visible to unknown (%(visibility)s).": "%(senderName)s hat den zukünftigen Chatverlauf sichtbar gemacht für unbekannt (%(visibility)s).",
|
||||
"Missing room_id in request": "Fehlende room_id in Anfrage",
|
||||
"Missing user_id in request": "Fehlende user_id in Anfrage",
|
||||
"Must be viewing a room": "Muss einen Raum ansehen",
|
||||
"(not supported by this browser)": "(wird von diesem Browser nicht unterstützt)",
|
||||
"%(senderName)s placed a %(callType)s call.": "%(senderName)s startete einen %(callType)s-Anruf.",
|
||||
"Power level must be positive integer.": "Berechtigungslevel muss eine positive ganze Zahl sein.",
|
||||
|
@ -423,7 +420,6 @@
|
|||
"Confirm Removal": "Entfernen bestätigen",
|
||||
"Unknown error": "Unbekannter Fehler",
|
||||
"Incorrect password": "Ungültiges Passwort",
|
||||
"This action is irreversible.": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"To continue, please enter your password.": "Zum fortfahren bitte Passwort eingeben.",
|
||||
"Device name": "Geräte-Name",
|
||||
"Device key": "Geräte-Schlüssel",
|
||||
|
@ -431,7 +427,6 @@
|
|||
"Verify device": "Gerät verifizieren",
|
||||
"I verify that the keys match": "Ich bestätige, dass die Schlüssel identisch sind",
|
||||
"Unable to restore session": "Sitzungswiederherstellung fehlgeschlagen",
|
||||
"Continue anyway": "Trotzdem fortfahren",
|
||||
"You are currently blacklisting unverified devices; to send messages to these devices you must verify them.": "Nicht verifizierte Geräte werden aktuell blockiert und auf die Sperrliste gesetzt. Um Nachrichten an diese Geräte senden zu können, müssen diese zunächst verifiziert werden.",
|
||||
"\"%(RoomName)s\" contains devices that you haven't seen before.": "\"%(RoomName)s\" enthält Geräte, die du bislang noch nicht gesehen hast.",
|
||||
"Unknown devices": "Unbekannte Geräte",
|
||||
|
@ -458,8 +453,6 @@
|
|||
"What does this mean?": "Was bedeutet das?",
|
||||
"Error decrypting audio": "Audio-Entschlüsselung fehlgeschlagen",
|
||||
"Error decrypting image": "Bild-Entschlüsselung fehlgeschlagen",
|
||||
"Image '%(Body)s' cannot be displayed.": "Das Bild '%(Body)s' kann nicht angezeigt werden.",
|
||||
"This image cannot be displayed.": "Dieses Bild kann nicht angezeigt werden.",
|
||||
"Error decrypting video": "Video-Entschlüsselung fehlgeschlagen",
|
||||
"Import room keys": "Raum-Schlüssel importieren",
|
||||
"File to import": "Zu importierende Datei",
|
||||
|
@ -468,7 +461,6 @@
|
|||
"This process allows you to export the keys for messages you have received in encrypted rooms to a local file. You will then be able to import the file into another Matrix client in the future, so that client will also be able to decrypt these messages.": "Dieser Prozess erlaubt es dir, die Schlüssel für die in verschlüsselten Räumen empfangenen Nachrichten in eine lokale Datei zu exportieren. In Zukunft wird es möglich sein, diese Datei in einen anderen Matrix-Client zu importieren, sodass dieser Client diese Nachrichten ebenfalls entschlüsseln kann.",
|
||||
"The exported file will allow anyone who can read it to decrypt any encrypted messages that you can see, so you should be careful to keep it secure. To help with this, you should enter a passphrase below, which will be used to encrypt the exported data. It will only be possible to import the data by using the same passphrase.": "Mit der exportierten Datei kann jeder, der diese Datei lesen kann, jede verschlüsselte Nachricht entschlüsseln, die für dich lesbar ist. Du solltest die Datei also unbedingt sicher verwahren. Um den Vorgang sicherer zu gestalten, solltest du unten eine Passphrase eingeben, die dazu verwendet wird, die exportierten Daten zu verschlüsseln. Anschließend wird es nur möglich sein, die Daten zu importieren, wenn dieselbe Passphrase verwendet wird.",
|
||||
"Analytics": "Anonymisierte Analysedaten",
|
||||
"Opt out of analytics": "Zustimmung zur Übermittlung von anonymisierten Analysedaten verweigern",
|
||||
"Riot collects anonymous analytics to allow us to improve the application.": "Riot sammelt anonymisierte Analysedaten, um die Anwendung kontinuierlich verbessern zu können.",
|
||||
"Add an Integration": "Eine Integration hinzufügen",
|
||||
"Removed or unknown message type": "Gelöschte Nachricht oder unbekannter Nachrichten-Typ",
|
||||
|
@ -477,10 +469,8 @@
|
|||
"Online": "Online",
|
||||
" (unsupported)": " (nicht unterstützt)",
|
||||
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "Dieser Prozess erlaubt es dir, die zuvor von einem anderen Matrix-Client exportierten Verschlüsselungs-Schlüssel zu importieren. Danach kannst du alle Nachrichten entschlüsseln, die auch bereits auf dem anderen Client entschlüsselt werden konnten.",
|
||||
"This will make your account permanently unusable. You will not be able to re-register the same user ID.": "Dies wird dein Benutzerkonto dauerhaft unbenutzbar machen. Du wirst nicht in der Lage sein, dich mit derselben Benutzer-ID erneut zu registrieren.",
|
||||
"To verify that this device can be trusted, please contact its owner using some other means (e.g. in person or a phone call) and ask them whether the key they see in their User Settings for this device matches the key below:": "Um sicherzustellen, dass diesem Gerät vertraut werden kann, kontaktiere bitte den Eigentümer des Geräts über ein anderes Kommunikationsmittel (z.B. im persönlichen Gespräch oder durch einen Telefonanruf) und vergewissere dich, dass der Schlüssel, den der Eigentümer in den Benutzer-Einstellungen für dieses Gerät sieht, mit dem folgenden Schlüssel identisch ist:",
|
||||
"If it matches, press the verify button below. If it doesn't, then someone else is intercepting this device and you probably want to press the blacklist button instead.": "Wenn er identisch ist, bitte den Bestätigen-Button unten verwenden. Falls er nicht identisch sein sollte, hat eine Fremdperson Kontrolle über dieses Gerät und es sollte gesperrt werden.",
|
||||
"We encountered an error trying to restore your previous session. If you continue, you will need to log in again, and encrypted chat history will be unreadable.": "Bei der Wiederherstellung deiner letzten Sitzung ist ein Fehler aufgetreten. Um fortzufahren, musst du dich erneut anmelden. Ein zuvor verschlüsselter Chatverlauf wird in der Folge nicht mehr lesbar sein.",
|
||||
"If you have previously used a more recent version of Riot, your session may be incompatible with this version. Close this window and return to the more recent version.": "Wenn du zuvor eine aktuellere Version von Riot verwendet hast, ist deine Sitzung eventuell inkompatibel mit dieser Version. Bitte schließe dieses Fenster und kehre zur aktuelleren Version zurück.",
|
||||
"Blacklist": "Blockieren",
|
||||
"Unblacklist": "Entblockieren",
|
||||
|
@ -590,7 +580,6 @@
|
|||
"Last seen": "Zuletzt gesehen",
|
||||
"Level:": "Berechtigungslevel:",
|
||||
"No display name": "Kein Anzeigename",
|
||||
"Otherwise, <a>click here</a> to send a bug report.": "Alternativ <a>hier klicken</a>, um einen Fehlerbericht zu senden.",
|
||||
"Private Chat": "Privater Chat",
|
||||
"Public Chat": "Öffentlicher Chat",
|
||||
"Reason: %(reasonText)s": "Grund: %(reasonText)s",
|
||||
|
@ -793,8 +782,6 @@
|
|||
"Failed to load %(groupId)s": "'%(groupId)s' konnte nicht geladen werden",
|
||||
"Error whilst fetching joined communities": "Fehler beim Laden beigetretener Communities",
|
||||
"Create a new community": "Neue Community erstellen",
|
||||
"Join an existing community": "Einer bestehenden Community beitreten",
|
||||
"To join an existing community you'll have to know its community identifier; this will look something like <i>+example:matrix.org</i>.": "Um einer bereits bestehenden Community beitreten zu können, musst dir deren Community-ID bekannt sein. Diese sieht z. B. aus wie <i>+example:matrix.org</i>.",
|
||||
"Your Communities": "Deine Communities",
|
||||
"You're not currently a member of any communities.": "Du gehörst aktuell keiner Community an.",
|
||||
"Create a community to group together users and rooms! Build a custom homepage to mark out your space in the Matrix universe.": "Erstelle eine Community, um Benutzer und Räume miteinander zu verbinden! Erstelle zusätzlich eine eigene Homepage, um deinen individuellen Bereich im Matrix-Universum zu gestalten.",
|
||||
|
@ -959,8 +946,6 @@
|
|||
"Your homeserver's URL": "Die URL deines Homeservers",
|
||||
"Your identity server's URL": "Die URL deines Identitätsservers",
|
||||
"%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s": "%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s",
|
||||
"Tag Panel": "Beschriftungsfeld",
|
||||
"Message Replies": "Antworten auf Nachrichten",
|
||||
"You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.": "Du wirst nicht in der Lage sein, die Änderung zurückzusetzen, da du dich degradierst. Wenn du der letze Nutzer mit Berechtigungen bist, wird es unmöglich sein die Privilegien zurückzubekommen.",
|
||||
"Community IDs cannot not be empty.": "Community-IDs können nicht leer sein.",
|
||||
"<showDevicesText>Show devices</showDevicesText>, <sendAnywayText>send anyway</sendAnywayText> or <cancelText>cancel</cancelText>.": "<showDevicesText>Geräte anzeigen</showDevicesText>, <sendAnywayText>trotzdem senden</sendAnywayText> oder <cancelText>abbrechen</cancelText>.",
|
||||
|
@ -977,7 +962,7 @@
|
|||
"Did you know: you can use communities to filter your Riot.im experience!": "Wusstest du: Du kannst Communities nutzen um deine Riot.im-Erfahrung zu filtern!",
|
||||
"To set up a filter, drag a community avatar over to the filter panel on the far left hand side of the screen. You can click on an avatar in the filter panel at any time to see only the rooms and people associated with that community.": "Um einen Filter zu setzen, siehe einen Community-Bild auf das Filter-Panel ganz links. Du kannst jederzeit auf einen Avatar im Filter-Panel klicken um nur die Räume und Personen aus der Community zu sehen.",
|
||||
"Clear filter": "Filter zurücksetzen",
|
||||
"Disable Community Filter Panel": "Deaktivere Community-Filter-Panel",
|
||||
"Disable Community Filter Panel": "Deaktiviere Community-Filter-Panel",
|
||||
"Your key share request has been sent - please check your other devices for key share requests.": "Deine Schlüssel-Teil-Anfragen wurden gesendet. Bitte prüfe deine anderen Geräte auf die Schlüssel-Teil-Anfragen.",
|
||||
"Key share requests are sent to your other devices automatically. If you rejected or dismissed the key share request on your other devices, click here to request the keys for this session again.": "Schlüssel-Anfragen wurden automatisch zu den anderen Geräten gesendet. Wenn du diese Anfragen auf deinen anderen Geräten abgelehnt oder verpasst hast, klicke hier um die Schlüssel für diese Sitzung erneut anzufragen.",
|
||||
"If your other devices do not have the key for this message you will not be able to decrypt them.": "Wenn deine anderen Geräte keine Schlüssel für diese Nachricht haben, wirst du diese nicht entschlüsseln können.",
|
||||
|
@ -995,7 +980,6 @@
|
|||
"Changes made to your community <bold1>name</bold1> and <bold2>avatar</bold2> might not be seen by other users for up to 30 minutes.": "Änderungen am <bold1>Namen</bold1> und <bold2>Bild</bold2> deiner Community werden evtl. erst nach 30 Minuten von anderen Nutzern gesehen werden.",
|
||||
"Join this community": "Community beitreten",
|
||||
"Leave this community": "Community verlassen",
|
||||
"Sticker Messages": "Sticker-Nachrichten",
|
||||
"You don't currently have any stickerpacks enabled": "Du hast aktuell keine Stickerpacks aktiviert",
|
||||
"Add a stickerpack": "Füge ein Stickerpack hinzu",
|
||||
"Hide Stickers": "Sticker verbergen",
|
||||
|
@ -1110,7 +1094,6 @@
|
|||
"What's new?": "Was ist neu?",
|
||||
"Notify me for anything else": "Über alles andere benachrichtigen",
|
||||
"When I'm invited to a room": "Wenn ich in einen Raum eingeladen werde",
|
||||
"<a>Click here</a> to create a GitHub issue.": "<a>Klicke hier</a> um ein GithHub-Problem zu erstellen.",
|
||||
"Can't update user notification settings": "Benachrichtigungs-Einstellungen des Benutzers konnten nicht aktualisiert werden",
|
||||
"Notify for all other messages/rooms": "Benachrichtigungen für alle anderen Mitteilungen/Räume aktivieren",
|
||||
"Unable to look up room ID from server": "Es ist nicht möglich, die Raum-ID auf dem Server nachzuschlagen",
|
||||
|
@ -1136,7 +1119,6 @@
|
|||
"Unable to fetch notification target list": "Liste der Benachrichtigungsempfänger konnte nicht abgerufen werden",
|
||||
"Set Password": "Passwort einrichten",
|
||||
"Enable audible notifications in web client": "Audio-Benachrichtigungen im Web-Client aktivieren",
|
||||
"Permalink": "Permanenter Link",
|
||||
"Off": "Aus",
|
||||
"Riot does not know how to join a room on this network": "Riot weiß nicht, wie es einem Raum auf diesem Netzwerk beitreten soll",
|
||||
"Mentions only": "Nur, wenn du erwähnt wirst",
|
||||
|
@ -1177,5 +1159,52 @@
|
|||
"Clearing your browser's storage may fix the problem, but will sign you out and cause any encrypted chat history to become unreadable.": "Den Browser-Speicher zu löschen kann das Problem lösen, wird dich aber abmelden und verschlüsselte Chats unlesbar machen.",
|
||||
"Collapse Reply Thread": "Antwort-Thread zusammenklappen",
|
||||
"At this time it is not possible to reply with an emote.": "An dieser Stelle ist es nicht möglich mit einer Umschreibung zu antworten.",
|
||||
"Enable widget screenshots on supported widgets": "Widget-Screenshots bei unterstützten Widgets aktivieren"
|
||||
"Enable widget screenshots on supported widgets": "Widget-Screenshots bei unterstützten Widgets aktivieren",
|
||||
"Send analytics data": "Analysedaten senden",
|
||||
"e.g. %(exampleValue)s": "z.B. %(exampleValue)s",
|
||||
"Reload widget": "Widget neu laden",
|
||||
"To notify everyone in the room, you must be a": "Notwendiges Berechtigungslevel, um jeden im Raum zu benachrichten:",
|
||||
"Muted Users": "Stummgeschaltete Benutzer",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie (please see our <PolicyLink>Cookie Policy</PolicyLink>).": "Bitte helfe uns Riot.im zu verbessern, in dem du <UsageDataLink>anonyme Nutzungsdaten</UsageDataLink> schickst. Dies wird ein Cookie benutzen (bitte beachte auch unsere <PolicyLink>Cookie-Richtlinie</PolicyLink>).",
|
||||
"Please help improve Riot.im by sending <UsageDataLink>anonymous usage data</UsageDataLink>. This will use a cookie.": "Bitte helfe uns Riot.im zu verbessern, in dem du <UsageDataLink>anonyme Nutzungsdaten</UsageDataLink> schickst. Dies wird ein Cookie benutzen.",
|
||||
"Yes, I want to help!": "Ja, ich möchte helfen!",
|
||||
"Warning: This widget might use cookies.": "Warnung: Diese Widget mag Cookies verwenden.",
|
||||
"Failed to indicate account erasure": "Fehler beim Signalisieren der Account-Löschung",
|
||||
"This will make your account permanently unusable. You will not be able to log in, and no one will be able to re-register the same user ID. This will cause your account to leave all rooms it is participating in, and it will remove your account details from your identity server. <b>This action is irreversible.</b>": "Dies wird deinen Account permanent unbenutzbar machen. Du wirst nicht in der Lage sein, dich anzumelden und keiner wird dieselbe Benutzer-ID erneut registrieren können. Alle Räume, in denen der Account ist, werden verlassen und deine Account-Daten werden vom Identitätsserver gelöscht. <b>Diese Aktion ist irreversibel!</b>",
|
||||
"Deactivating your account <b>does not by default cause us to forget messages you have sent.</b> If you would like us to forget your messages, please tick the box below.": "Standardmäßig werden <b>die von dir gesendeten Nachrichten beim Deaktiveren nicht gelöscht</b>. Wenn du dies von uns möchtest, aktivere das Auswalfeld unten.",
|
||||
"Message visibility in Matrix is similar to email. Our forgetting your messages means that messages you have sent will not be shared with any new or unregistered users, but registered users who already have access to these messages will still have access to their copy.": "Sie Sichtbarkeit der Nachrichten in Matrix ist vergleichbar mit E-Mails: Wenn wir deine Nachrichten vergessen heißt das, dass diese nicht mit neuen oder nicht registrierten Nutzern teilen werden, aber registrierte Nutzer, die bereits zugriff haben, werden Zugriff auf ihre Kopie behalten.",
|
||||
"Please forget all messages I have sent when my account is deactivated (<b>Warning:</b> this will cause future users to see an incomplete view of conversations)": "Bitte vergesst alle Nachrichten, die ich gesendet habe, wenn mein Account deaktiviert wird. (<b>Warnung:</b> Zukünftige Nutzer werden eine unvollständige Konversation sehen)",
|
||||
"To continue, please enter your password:": "Um fortzufahren, bitte Password eingeben:",
|
||||
"password": "Passwort",
|
||||
"Can't leave Server Notices room": "Du kannst den Raum für Server-Notizen nicht verlassen",
|
||||
"This room is used for important messages from the Homeserver, so you cannot leave it.": "Du kannst diesen Raum nicht verlassen, da dieser Raum für wichtige Nachrichten vom Heimserver verwendet wird.",
|
||||
"Terms and Conditions": "Geschäftsbedingungen",
|
||||
"To continue using the %(homeserverDomain)s homeserver you must review and agree to our terms and conditions.": "Um den %(homeserverDomain)s -Heimserver weiter zu verwenden, musst du die Geschäftsbedingungen sichten und ihnen zustimmen.",
|
||||
"Review terms and conditions": "Geschäftsbedingungen anzeigen",
|
||||
"Encrypting": "Verschlüssele",
|
||||
"Encrypted, not sent": "Verschlüsselt, nicht gesendet",
|
||||
"Share Link to User": "Sende Link an Benutzer",
|
||||
"Share room": "Teile Raum",
|
||||
"Share Room": "Teile Raum",
|
||||
"Link to most recent message": "Link zur aktuellsten Nachricht",
|
||||
"Share User": "Teile Benutzer",
|
||||
"Share Community": "Teile Community",
|
||||
"Share Room Message": "Teile Raumnachricht",
|
||||
"Link to selected message": "Link zur ausgewählten Nachricht",
|
||||
"COPY": "KOPIEREN",
|
||||
"Share Message": "Teile Nachricht",
|
||||
"No Audio Outputs detected": "Keine Ton-Ausgabe erkannt",
|
||||
"Audio Output": "Ton-Ausgabe",
|
||||
"Try the app first": "App erst ausprobieren",
|
||||
"Jitsi Conference Calling": "Jitsi-Konferenz Anruf",
|
||||
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In verschlüsselten Räumen, wie diesem, ist die Link-Vorschau standardmäßig deaktiviert damit dein Heimserver (auf dem die Vorschau erzeugt wird) keine Informationen über Links in diesem Raum bekommt.",
|
||||
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "Wenn jemand eine Nachricht mit einem Link schickt, kann die Link-Vorschau mehr Informationen, wie Titel, Beschreibung und Bild der Webseite, über den Link anzeigen.",
|
||||
"The email field must not be blank.": "Das E-Mail-Feld darf nicht leer sein.",
|
||||
"The user name field must not be blank.": "Das Benutzername-Feld darf nicht leer sein.",
|
||||
"The phone number field must not be blank.": "Das Telefonnummern-Feld darf nicht leer sein.",
|
||||
"The password field must not be blank.": "Das Passwort-Feld darf nicht leer sein.",
|
||||
"Call in Progress": "Gespräch läuft",
|
||||
"A call is already in progress!": "Ein Gespräch läuft bereits!",
|
||||
"You have no historical rooms": "Du hast keine historischen Räume",
|
||||
"You can't send any messages until you review and agree to <consentLink>our terms and conditions</consentLink>.": "Du kannst keine Nachrichten senden bis du die <consentLink>unsere Geschläftsbedingungen</consentLink> gelesen und akzeptiert hast."
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue