Merge branch 'develop' into travis/presence

This commit is contained in:
Travis Ralston 2017-10-14 17:26:54 -06:00
commit 49c19bc9b4
30 changed files with 210 additions and 40 deletions

View file

@ -172,7 +172,7 @@ const sanitizeHtmlParams = {
// Lots of these won't come up by default because we don't allow them
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'magnet'],
allowProtocolRelative: false,
@ -385,10 +385,9 @@ class TextHighlighter extends BaseHighlighter {
* highlights: optional list of words to highlight, ordered by longest word first
*
* opts.highlightLink: optional href to add to highlighted words
* opts.disableBigEmoji: optional argument to disable the big emoji class.
*/
export function bodyToHtml(content, highlights, opts) {
opts = opts || {};
export function bodyToHtml(content, highlights, opts={}) {
const isHtml = (content.format === "org.matrix.custom.html");
const body = isHtml ? content.formatted_body : escape(content.body);
@ -418,7 +417,7 @@ export function bodyToHtml(content, highlights, opts) {
}
let emojiBody = false;
if (bodyHasEmoji) {
if (!opts.disableBigEmoji && bodyHasEmoji) {
EMOJI_REGEX.lastIndex = 0;
const contentBodyTrimmed = content.body !== undefined ? content.body.trim() : '';
const match = EMOJI_REGEX.exec(contentBodyTrimmed);

View file

@ -80,10 +80,11 @@ const Notifier = {
if (ev.getContent().body) msg = ev.getContent().body;
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(
ev.sender, 40, 40, 'crop',
) : null;
if (!this.isBodyEnabled()) {
msg = '';
}
const avatarUrl = ev.sender ? Avatar.avatarUrlForMember(ev.sender, 40, 40, 'crop') : null;
const notif = plaf.displayNotification(title, msg, avatarUrl, room);
// if displayNotification returns non-null, the platform supports
@ -195,6 +196,19 @@ const Notifier = {
return enabled === 'true';
},
setBodyEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_body_enabled', enable ? 'true' : 'false');
},
isBodyEnabled: function() {
if (!global.localStorage) return true;
const enabled = global.localStorage.getItem('notifications_body_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
return enabled === 'true';
},
setAudioEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',

View file

@ -98,6 +98,17 @@ export default {
Notifier.setEnabled(enable);
},
getEnableNotificationBody: function() {
return Notifier.isBodyEnabled();
},
setEnableNotificationBody: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setBodyEnabled(enable);
},
getEnableAudioNotifications: function() {
return Notifier.isAudioEnabled();
},

View file

@ -43,6 +43,10 @@ module.exports = React.createClass({
// the end of the live timeline.
atEndOfLiveTimeline: React.PropTypes.bool,
// This is true when the user is alone in the room, but has also sent a message.
// Used to suggest to the user to invite someone
sentMessageAndIsAlone: React.PropTypes.bool,
// true if there is an active call in this room (means we show
// the 'Active Call' text in the status bar if there is nothing
// more interesting)
@ -60,6 +64,14 @@ module.exports = React.createClass({
// 'unsent messages' bar
onCancelAllClick: React.PropTypes.func,
// callback for when the user clicks on the 'invite others' button in the
// 'you are alone' bar
onInviteClick: React.PropTypes.func,
// callback for when the user clicks on the 'stop warning me' button in the
// 'you are alone' bar
onStopWarningClick: React.PropTypes.func,
// callback for when the user clicks on the 'scroll to bottom' button
onScrollToBottomClick: React.PropTypes.func,
@ -140,7 +152,8 @@ module.exports = React.createClass({
(this.state.usersTyping.length > 0) ||
this.props.numUnreadMessages ||
!this.props.atEndOfLiveTimeline ||
this.props.hasActiveCall
this.props.hasActiveCall ||
this.props.sentMessageAndIsAlone
) {
return STATUS_BAR_EXPANDED;
} else if (this.props.unsentMessageError) {
@ -305,6 +318,21 @@ module.exports = React.createClass({
);
}
// If you're alone in the room, and have sent a message, suggest to invite someone
if (this.props.sentMessageAndIsAlone) {
return (
<div className="mx_RoomStatusBar_isAlone">
{ _tJsx("There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
[/<a>(.*?)<\/a>/, /<a>(.*?)<\/a>/],
[
(sub) => <a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
(sub) => <a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
],
) }
</div>
);
}
return null;
},

View file

@ -117,6 +117,7 @@ module.exports = React.createClass({
guestsCanJoin: false,
canPeek: false,
showApps: false,
isAlone: false,
isPeeking: false,
// error object, as from the matrix client/server API
@ -461,6 +462,8 @@ module.exports = React.createClass({
switch (payload.action) {
case 'message_send_failed':
case 'message_sent':
this._checkIfAlone(this.state.room);
// no break; to intentionally fall through
case 'message_send_cancelled':
this.setState({
unsentMessageError: this._getUnsentMessageError(this.state.room),
@ -740,6 +743,20 @@ module.exports = React.createClass({
}
}, 500),
_checkIfAlone: function(room) {
let warnedAboutLonelyRoom = false;
if (localStorage) {
warnedAboutLonelyRoom = localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId);
}
if (warnedAboutLonelyRoom) {
if (this.state.isAlone) this.setState({isAlone: false});
return;
}
const joinedMembers = room.currentState.getMembers().filter(m => m.membership === "join" || m.membership === "invite");
this.setState({isAlone: joinedMembers.length === 1});
},
_getUnsentMessageError: function(room) {
const unsentMessages = this._getUnsentMessages(room);
if (!unsentMessages.length) return "";
@ -821,6 +838,22 @@ module.exports = React.createClass({
Resend.cancelUnsentEvents(this.state.room);
},
onInviteButtonClick: function() {
// call AddressPickerDialog
dis.dispatch({
action: 'view_invite',
roomId: this.state.room.roomId,
});
this.setState({isAlone: false}); // there's a good chance they'll invite someone
},
onStopAloneWarningClick: function() {
if (localStorage) {
localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, true);
}
this.setState({isAlone: false});
},
onJoinButtonClicked: function(ev) {
const cli = MatrixClientPeg.get();
@ -1581,9 +1614,12 @@ module.exports = React.createClass({
numUnreadMessages={this.state.numUnreadMessages}
unsentMessageError={this.state.unsentMessageError}
atEndOfLiveTimeline={this.state.atEndOfLiveTimeline}
sentMessageAndIsAlone={this.state.isAlone}
hasActiveCall={inCall}
onResendAllClick={this.onResendAllClick}
onCancelAllClick={this.onCancelAllClick}
onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onScrollToBottomClick={this.jumpToLiveTimeline}
onResize={this.onChildResize}
onVisible={this.onStatusBarVisible}

View file

@ -114,6 +114,10 @@ const SETTINGS_LABELS = [
id: 'Pill.shouldHidePillAvatar',
label: _td('Hide avatars in user and room mentions'),
},
{
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
},
/*
{
id: 'useFixedWidthFont',
@ -423,6 +427,11 @@ module.exports = React.createClass({
});
},
onAvatarRemoveClick: function() {
MatrixClientPeg.get().setAvatarUrl(null);
this.setState({avatarUrl: null}); // the avatar update will complete async for us
},
onLogoutClicked: function(ev) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
Modal.createTrackedDialog('Logout E2E Export', '', QuestionDialog, {
@ -1318,7 +1327,11 @@ module.exports = React.createClass({
</div>
<div className="mx_UserSettings_avatarPicker">
<div onClick={this.onAvatarPickerClick}>
<div className="mx_UserSettings_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
<img src="img/cancel.svg" width="15" height="15"
alt={_t("Remove avatar")} title={_t("Remove avatar")} />
</div>
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}
showUploadSection={false} className="mx_UserSettings_avatarPicker_img" />
</div>

View file

@ -70,9 +70,9 @@ module.exports = React.createClass({
// it sucks that _tJsx doesn't support normal _t substitutions :((
return (
<div className="mx_RoomAvatarEvent">
{ _tJsx('$senderDisplayName changed the room avatar to <img/>',
{ _tJsx('%(senderDisplayName)s changed the room avatar to <img/>',
[
/\$senderDisplayName/,
/%\(senderDisplayName\)s/,
/<img\/>/,
],
[

View file

@ -354,7 +354,9 @@ module.exports = React.createClass({
const mxEvent = this.props.mxEvent;
const content = mxEvent.getContent();
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {});
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false),
});
if (this.props.highlightLink) {
body = <a href={this.props.highlightLink}>{ body }</a>;

View file

@ -34,7 +34,7 @@ const ROOM_COLORS = [
["#dad658", "#f5f4ea"],
["#80c553", "#eef5ea"],
["#bb814e", "#eee8e3"],
["#595959", "#ececec"],
//["#595959", "#ececec"], // Grey makes everything appear disabled, so remove it for now
];
module.exports = React.createClass({

View file

@ -625,22 +625,49 @@ module.exports = withMatrixClient(React.createClass({
},
_renderUserOptions: function() {
// Only allow the user to ignore the user if its not ourselves
const cli = this.props.matrixClient;
const member = this.props.member;
let ignoreButton = null;
if (this.props.member.userId !== this.props.matrixClient.getUserId()) {
let readReceiptButton = null;
// Only allow the user to ignore the user if its not ourselves
// same goes for jumping to read receipt
if (member.userId !== cli.getUserId()) {
ignoreButton = (
<AccessibleButton onClick={this.onIgnoreToggle} className="mx_MemberInfo_field">
{ this.state.isIgnoring ? _t("Unignore") : _t("Ignore") }
</AccessibleButton>
);
if (member.roomId) {
const room = cli.getRoom(member.roomId);
const eventId = room.getEventReadUpTo(member.userId);
const onReadReceiptButton = function() {
dis.dispatch({
action: 'view_room',
highlighted: true,
event_id: eventId,
room_id: member.roomId,
});
};
readReceiptButton = (
<AccessibleButton onClick={onReadReceiptButton} className="mx_MemberInfo_field">
{ _t('Jump to read receipt') }
</AccessibleButton>
);
}
}
if (!ignoreButton) return null;
if (!ignoreButton && !readReceiptButton) return null;
return (
<div>
<h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons">
{ readReceiptButton }
{ ignoreButton }
</div>
</div>

View file

@ -146,8 +146,8 @@ module.exports = React.createClass({
const newState = {
members: this.roomMembers(),
};
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join');
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite');
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery);
this.setState(newState);
}, 500),
@ -187,7 +187,7 @@ module.exports = React.createClass({
const user_id = all_user_ids[i];
const m = all_members[user_id];
if (m.membership == 'join' || m.membership == 'invite') {
if (m.membership === 'join' || m.membership === 'invite') {
if ((ConferenceHandler && !ConferenceHandler.isConferenceUser(user_id)) || !ConferenceHandler) {
to_display.push(user_id);
++count;
@ -302,6 +302,7 @@ module.exports = React.createClass({
const m = this.memberDict[userId];
if (query) {
query = query.toLowerCase();
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
const matchesId = m.userId.toLowerCase().indexOf(query) !== -1;
@ -310,7 +311,7 @@ module.exports = React.createClass({
}
}
return m.membership == membership;
return m.membership === membership;
});
},

View file

@ -129,6 +129,10 @@ module.exports = React.createClass({
}).done();
},
onAvatarRemoveClick: function() {
MatrixClientPeg.get().sendStateEvent(this.props.room.roomId, 'm.room.avatar', {url: null}, '');
},
onShowRhsClick: function(ev) {
dis.dispatch({ action: 'show_right_panel' });
},
@ -268,11 +272,15 @@ module.exports = React.createClass({
<div className="mx_RoomHeader_avatarPicker_edit">
<label htmlFor="avatarInput" ref="file_label">
<img src="img/camera.svg"
alt={_t("Upload avatar")} title={_t("Upload avatar")}
width="17" height="15" />
alt={_t("Upload avatar")} title={_t("Upload avatar")}
width="17" height="15" />
</label>
<input id="avatarInput" type="file" onChange={this.onAvatarSelected} />
</div>
<div className="mx_RoomHeader_avatarPicker_remove" onClick={this.onAvatarRemoveClick}>
<img src="img/cancel.svg" width="10"
alt={_t("Remove avatar")} title={_t("Remove avatar")} />
</div>
</div>
);
} else if (this.props.room || (this.props.oobData && this.props.oobData.name)) {

View file

@ -53,6 +53,10 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents);
},
componentWillReceiveProps: function(newProps) {
if (this.avatarSet) {
// don't clobber what the user has just set
@ -63,6 +67,28 @@ module.exports = React.createClass({
});
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
}
},
onRoomStateEvents: function(ev) {
if (!this.props.room) {
return;
}
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'm.room.avatar'
|| ev.getSender() !== MatrixClientPeg.get().getUserId()) {
return;
}
if (!ev.getContent().url) {
this.avatarSet = false;
this.setState({}); // force update
}
},
setAvatarFromFile: function(file) {
let newUrl = null;

View file

@ -728,7 +728,7 @@
"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!": "WARNUNG: SCHLÜSSEL-VERIFIZIERUNG FEHLGESCHLAGEN! Der Signatur-Schlüssel für %(userId)s und das Gerät %(deviceId)s ist \"%(fprint)s\", welcher nicht mit dem bereitgestellten Schlüssel \"%(fingerprint)s\" übereinstimmt. Dies kann bedeuten, dass deine Kommunikation abgehört wird!",
"You have <a>disabled</a> URL previews by default.": "Du hast die URL-Vorschau standardmäßig <a>deaktiviert</a>.",
"You have <a>enabled</a> URL previews by default.": "Du hast die URL-Vorschau standardmäßig <a>aktiviert</a>.",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName hat das Raum-Bild geändert zu <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s hat das Raum-Bild geändert zu <img/>",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s hat das Raum-Bild für %(roomName)s geändert",
"Hide removed messages": "Gelöschte Nachrichten verbergen",
"Start new chat": "Neuen Chat starten",

View file

@ -639,7 +639,7 @@
"Disable URL previews by default for participants in this room": "Απενεργοποίηση της προεπισκόπησης συνδέσμων για όλους τους συμμετέχοντες στο δωμάτιο",
"Disable URL previews for this room (affects only you)": "Απενεργοποίηση της προεπισκόπησης συνδέσμων για αυτό το δωμάτιο (επηρεάζει μόνο εσάς)",
" (unsupported)": " (μη υποστηριζόμενο)",
"$senderDisplayName changed the room avatar to <img/>": "Ο $senderDisplayName άλλαξε την εικόνα του δωματίου σε <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "Ο %(senderDisplayName)s άλλαξε την εικόνα του δωματίου σε <img/>",
"Missing Media Permissions, click here to request.": "Λείπουν τα δικαιώματα πολύμεσων, κάντε κλικ για να ζητήσετε.",
"You may need to manually permit Riot to access your microphone/webcam": "Μπορεί να χρειαστεί να ορίσετε χειροκίνητα την πρόσβαση του Riot στο μικρόφωνο/κάμερα",
"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>πιστοποιητικό SSL</a> του διακομιστή είναι έμπιστο και ότι κάποιο πρόσθετο περιηγητή δεν αποτρέπει τα αιτήματα.",

View file

@ -252,6 +252,7 @@
"%(targetName)s joined the room.": "%(targetName)s joined the room.",
"Joins room with given alias": "Joins room with given alias",
"Jump to first unread message.": "Jump to first unread message.",
"Jump to read receipt": "Jump to read receipt",
"%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.",
"Kick": "Kick",
"Kicks user with given id": "Kicks user with given id",
@ -289,6 +290,7 @@
"matrix-react-sdk version:": "matrix-react-sdk version:",
"Matrix Apps": "Matrix Apps",
"Members only": "Members only",
"Disable big emoji in chat": "Disable big emoji in chat",
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
"Message not sent due to unknown devices being present": "Message not sent due to unknown devices being present",
"Missing room_id in request": "Missing room_id in request",
@ -609,6 +611,7 @@
"Room": "Room",
"Copied!": "Copied!",
"Failed to copy": "Failed to copy",
"There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?": "There's no one else here! Would you like to <a>invite others</a> or <a>stop warning about the empty room</a>?",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.": "<a>Resend all</a> or <a>cancel all</a> now. You can also select individual messages to resend or cancel.",
@ -632,6 +635,7 @@
"quote": "quote",
"bullet": "bullet",
"numbullet": "numbullet",
"Remove avatar": "Remove avatar",
"%(severalUsers)sjoined %(repeats)s times": "%(severalUsers)sjoined %(repeats)s times",
"%(oneUser)sjoined %(repeats)s times": "%(oneUser)sjoined %(repeats)s times",
"%(severalUsers)sjoined": "%(severalUsers)sjoined",
@ -785,7 +789,7 @@
"Start chatting": "Start chatting",
"Start Chatting": "Start Chatting",
"Click on the button below to start chatting!": "Click on the button below to start chatting!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName changed the room avatar to <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
"Username available": "Username available",

View file

@ -705,7 +705,7 @@
"Idle": "Idle",
"Offline": "Offline",
"Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName changed the room avatar to <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s changed the room avatar to <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removed the room avatar.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s changed the avatar for %(roomName)s",
"Active call (%(roomName)s)": "Active call (%(roomName)s)",

View file

@ -736,7 +736,7 @@
"Start chatting": "Hasi txateatzen",
"Start Chatting": "Hasi txateatzen",
"Click on the button below to start chatting!": "Egin klik beheko botoian txateatzen hasteko!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName erabiltzaileak gelaren abatarra aldatu du beste honetara: <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s erabiltzaileak gelaren abatarra aldatu du beste honetara: <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s erabiltzaileak gelaren abatarra ezabatu du.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s erabiltzaileak %(roomName)s gelaren abatarra aldatu du",
"Username available": "Erabiltzaile-izena eskuragarri dago",

View file

@ -637,7 +637,7 @@
"for %(amount)sm": "depuis %(amount)sm",
"for %(amount)sh": "depuis %(amount)sh",
"for %(amount)sd": "depuis %(amount)sj",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName a changé lavatar du salon en <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s a changé lavatar du salon en <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s a supprimé l'avatar du salon.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s a changé lavatar de %(roomName)s",
"Device already verified!": "Appareil déjà vérifié !",

View file

@ -723,7 +723,7 @@
"Start chatting": "Csevegés indítása",
"Start Chatting": "Csevegés indítása",
"Click on the button below to start chatting!": "Csevegés indításához kattints a gombra alább!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName megváltoztatta a szoba avatar képét: <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s megváltoztatta a szoba avatar képét: <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s törölte a szoba avatar képét.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s megváltoztatta %(roomName)s szoba avatar képét",
"Username available": "Szabad felhasználói név",

View file

@ -743,7 +743,7 @@
"Start chatting": "이야기하기",
"Start Chatting": "이야기하기",
"Click on the button below to start chatting!": "이야기하려면 아래 버튼을 누르세요!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName님이 방 아바타를 <img/>로 바꾸셨어요",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s님이 방 아바타를 <img/>로 바꾸셨어요",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s님이 방 아바타를 지우셨어요.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s가 %(roomName)s 방의 아바타를 바꾸셨어요",
"Username available": "쓸 수 있는 사용자 이름",

View file

@ -619,7 +619,7 @@
"Dec": "Dec.",
"Set a display name:": "Iestatīt redzamo vārdu:",
"This image cannot be displayed.": "Šo attēlu nav iespējams parādīt.",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName nomainīja istabas attēlu uz <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s nomainīja istabas attēlu uz <img/>",
"Upload an avatar:": "Augšuplādē profila attēlu:",
"This server does not support authentication with a phone number.": "Šis serveris neatbalsta autentifikāciju pēc telefona numura.",
"Missing password.": "Trūkst parole.",

View file

@ -746,7 +746,7 @@
"Start chatting": "Start met praten",
"Start Chatting": "Start Met Praten",
"Click on the button below to start chatting!": "Klik op de knop hieronder om te starten met praten!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName heeft de ruimte avatar aangepast naar <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s heeft de ruimte avatar aangepast naar <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s heeft de ruimte avatar verwijderd.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s veranderde de avatar voor %(roomName)s",
"Username available": "Gebruikersnaam beschikbaar",

View file

@ -771,7 +771,7 @@
"for %(amount)sd": "%(amount)s dni",
"Idle": "Bezczynny",
"Check for update": "Sprawdź aktualizacje",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName zmienił awatar pokoju na <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s zmienił awatar pokoju na <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s usunął awatar pokoju.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s zmienił awatar %(roomName)s",
"This will be your account name on the <span></span> homeserver, or you can pick a <a>different server</a>.": "To będzie twoja nazwa konta na <span></span> serwerze domowym; możesz też wybrać <a>inny serwer</a>.",

View file

@ -699,7 +699,7 @@
"for %(amount)sd": "por %(amount)sd",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removeu a imagem da sala.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s alterou a imagem da sala %(roomName)s",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName alterou a imagem da sala para <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s alterou a imagem da sala para <img/>",
"Missing Media Permissions, click here to request.": "Faltam permissões para uso de mídia no seu computador. Clique aqui para solicitá-las.",
"No Microphones detected": "Não foi detetado nenhum microfone",
"No Webcams detected": "Não foi detetada nenhuma Webcam",

View file

@ -696,7 +696,7 @@
"for %(amount)sd": "por %(amount)sd",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s removeu a imagem da sala.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s alterou a imagem da sala %(roomName)s",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName alterou a imagem da sala para <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s alterou a imagem da sala para <img/>",
"Missing Media Permissions, click here to request.": "Faltam permissões para uso de mídia no seu computador. Clique aqui para solicitá-las.",
"No Microphones detected": "Não foi detectado nenhum microfone",
"No Webcams detected": "Não foi detectada nenhuma Webcam",

View file

@ -705,7 +705,7 @@
"Idle": "Неактивен",
"Offline": "Не в сети",
"Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL-адресов для этой комнаты (влияет только на вас)",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName сменил аватар комнаты на <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s сменил аватар комнаты на <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s удалил аватар комнаты.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s сменил аватар для %(roomName)s",
"Create new room": "Создать новую комнату",

View file

@ -738,7 +738,7 @@
"Start chatting": "Sohbeti başlat",
"Start Chatting": "Sohbeti Başlat",
"Click on the button below to start chatting!": "Sohbeti başlatmak için aşağıdaki butona tıklayın!",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName odanın avatarını <img/> olarak çevirdi",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s odanın avatarını <img/> olarak çevirdi",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s odanın avatarını kaldırdı.",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s %(roomName)s için avatarı değiştirdi",
"Username available": "Kullanıcı ismi uygun",

View file

@ -228,7 +228,7 @@
"Idle": "閒置",
"Offline": "下線",
"Disable URL previews for this room (affects only you)": "在這個房間禁止URL預覽只影響你",
"$senderDisplayName changed the room avatar to <img/>": "$senderDisplayName 更改了聊天室的圖像為 <img/>",
"%(senderDisplayName)s changed the room avatar to <img/>": "%(senderDisplayName)s 更改了聊天室的圖像為 <img/>",
"%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室圖片。",
"%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s 更改了聊天室 %(roomName)s 圖像",
"Cancel": "取消",

View file

@ -109,8 +109,9 @@ export function _tJsx(jsxText, patterns, subs) {
}
// The translation returns text so there's no XSS vector here (no unsafe HTML, no code execution)
const tJsxText = _t(jsxText);
const tJsxText = _t(jsxText, {interpolate: false});
const output = [tJsxText];
for (let i = 0; i < patterns.length; i++) {
// convert the last element in 'output' into 3 elements (pre-text, sub function, post-text).
// Rinse and repeat for other patterns (using post-text).