Merge branch 'develop' into luke/groups-are-communities

This commit is contained in:
Luke Barnard 2017-10-16 17:34:06 +01:00
commit 0c34e943fb
38 changed files with 642 additions and 58 deletions

View file

@ -1,3 +1,160 @@
Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7)
* Update to latest js-sdk
Changes in [0.10.7-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.3) (2017-10-13)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.2...v0.10.7-rc.3)
* Fix the enableLabs flag, again
[\#1474](https://github.com/matrix-org/matrix-react-sdk/pull/1474)
Changes in [0.10.7-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.2) (2017-10-13)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.1...v0.10.7-rc.2)
* Honour the (now legacy) enableLabs flag
[\#1473](https://github.com/matrix-org/matrix-react-sdk/pull/1473)
* Don't show labs features by default
[\#1472](https://github.com/matrix-org/matrix-react-sdk/pull/1472)
* Make features disabled by default
[\#1470](https://github.com/matrix-org/matrix-react-sdk/pull/1470)
Changes in [0.10.7-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7-rc.1) (2017-10-13)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.6...v0.10.7-rc.1)
* Add warm fuzzy dialog for inviting users to a group
[\#1459](https://github.com/matrix-org/matrix-react-sdk/pull/1459)
* enable/disable features in config.json
[\#1468](https://github.com/matrix-org/matrix-react-sdk/pull/1468)
* Update from Weblate.
[\#1469](https://github.com/matrix-org/matrix-react-sdk/pull/1469)
* Don't send RR or RM when peeking at a room
[\#1463](https://github.com/matrix-org/matrix-react-sdk/pull/1463)
* Fix bug that inserted emoji when typing
[\#1467](https://github.com/matrix-org/matrix-react-sdk/pull/1467)
* Ignore VS16 char in RTE
[\#1458](https://github.com/matrix-org/matrix-react-sdk/pull/1458)
* Show failures when sending messages
[\#1460](https://github.com/matrix-org/matrix-react-sdk/pull/1460)
* Run eslint --fix
[\#1461](https://github.com/matrix-org/matrix-react-sdk/pull/1461)
* Show who banned the user on hover
[\#1441](https://github.com/matrix-org/matrix-react-sdk/pull/1441)
* Enhancements to room power level settings
[\#1440](https://github.com/matrix-org/matrix-react-sdk/pull/1440)
* Added TextInputWithCheckbox dialog
[\#868](https://github.com/matrix-org/matrix-react-sdk/pull/868)
* Make it clearer which HS you're logging into
[\#1456](https://github.com/matrix-org/matrix-react-sdk/pull/1456)
* Remove redundant stale onKeyDown
[\#1451](https://github.com/matrix-org/matrix-react-sdk/pull/1451)
* Only allow event state event handlers on state events
[\#1453](https://github.com/matrix-org/matrix-react-sdk/pull/1453)
* Modify the group store to include group rooms
[\#1452](https://github.com/matrix-org/matrix-react-sdk/pull/1452)
* Factor-out GroupStore and create GroupStoreCache
[\#1449](https://github.com/matrix-org/matrix-react-sdk/pull/1449)
* Put related groups UI behind groups labs flag
[\#1448](https://github.com/matrix-org/matrix-react-sdk/pull/1448)
* Restrict Flair in the timeline to related groups of the room
[\#1447](https://github.com/matrix-org/matrix-react-sdk/pull/1447)
* Implement UI for editing related groups of a room
[\#1446](https://github.com/matrix-org/matrix-react-sdk/pull/1446)
* Fix a couple of bugs with EditableItemList
[\#1445](https://github.com/matrix-org/matrix-react-sdk/pull/1445)
* Factor out EditableItemList from AliasSettings
[\#1444](https://github.com/matrix-org/matrix-react-sdk/pull/1444)
* Add dummy translation function to mark translatable strings
[\#1421](https://github.com/matrix-org/matrix-react-sdk/pull/1421)
* Implement button to remove a room from a group
[\#1438](https://github.com/matrix-org/matrix-react-sdk/pull/1438)
* Fix showing 3pid invites in member list
[\#1443](https://github.com/matrix-org/matrix-react-sdk/pull/1443)
* Add button to get to MyGroups (view_my_groups or path #/groups)
[\#1435](https://github.com/matrix-org/matrix-react-sdk/pull/1435)
* Add eslint rule to disallow spaces inside of curly braces
[\#1436](https://github.com/matrix-org/matrix-react-sdk/pull/1436)
* Fix ability to invite existing mx users
[\#1437](https://github.com/matrix-org/matrix-react-sdk/pull/1437)
* Construct address picker message using provided `validAddressTypes`
[\#1434](https://github.com/matrix-org/matrix-react-sdk/pull/1434)
* Fix GroupView summary rooms displaying without avatars
[\#1433](https://github.com/matrix-org/matrix-react-sdk/pull/1433)
* Implement adding rooms to a group (or group summary) by room ID
[\#1432](https://github.com/matrix-org/matrix-react-sdk/pull/1432)
* Give flair avatars a tooltip = the group ID
[\#1431](https://github.com/matrix-org/matrix-react-sdk/pull/1431)
* Fix ability to feature self in a group summary
[\#1430](https://github.com/matrix-org/matrix-react-sdk/pull/1430)
* Implement "Add room to group" feature
[\#1429](https://github.com/matrix-org/matrix-react-sdk/pull/1429)
* Fix group membership publicity
[\#1428](https://github.com/matrix-org/matrix-react-sdk/pull/1428)
* Add support for Jitsi screensharing in electron app
[\#1355](https://github.com/matrix-org/matrix-react-sdk/pull/1355)
* Delint and DRY TextForEvent
[\#1424](https://github.com/matrix-org/matrix-react-sdk/pull/1424)
* Bust the flair caches after 30mins
[\#1427](https://github.com/matrix-org/matrix-react-sdk/pull/1427)
* Show displayname / avatar in group member info
[\#1426](https://github.com/matrix-org/matrix-react-sdk/pull/1426)
* Create GroupSummaryStore for storing group summary stuff
[\#1418](https://github.com/matrix-org/matrix-react-sdk/pull/1418)
* Add status & toggle for publicity
[\#1419](https://github.com/matrix-org/matrix-react-sdk/pull/1419)
* MemberList: show 100 more on overflow tile click
[\#1417](https://github.com/matrix-org/matrix-react-sdk/pull/1417)
* Fix NPE in MemberList
[\#1425](https://github.com/matrix-org/matrix-react-sdk/pull/1425)
* Fix incorrect variable in string
[\#1422](https://github.com/matrix-org/matrix-react-sdk/pull/1422)
* apply i18n _t to string which has already been translated
[\#1420](https://github.com/matrix-org/matrix-react-sdk/pull/1420)
* Make the invite section a truncatedlist too
[\#1416](https://github.com/matrix-org/matrix-react-sdk/pull/1416)
* Implement removal function of features users/rooms
[\#1415](https://github.com/matrix-org/matrix-react-sdk/pull/1415)
* Allow TruncatedList to get children via a callback
[\#1412](https://github.com/matrix-org/matrix-react-sdk/pull/1412)
* Experimental: Lazy load user autocomplete entries
[\#1413](https://github.com/matrix-org/matrix-react-sdk/pull/1413)
* Show displayname & avatar url in group member list
[\#1414](https://github.com/matrix-org/matrix-react-sdk/pull/1414)
* De-lint TruncatedList
[\#1411](https://github.com/matrix-org/matrix-react-sdk/pull/1411)
* Remove unneeded strings
[\#1409](https://github.com/matrix-org/matrix-react-sdk/pull/1409)
* Clean on prerelease
[\#1410](https://github.com/matrix-org/matrix-react-sdk/pull/1410)
* Redesign membership section in GroupView
[\#1408](https://github.com/matrix-org/matrix-react-sdk/pull/1408)
* Implement adding rooms to the group summary
[\#1406](https://github.com/matrix-org/matrix-react-sdk/pull/1406)
* Honour the is_privileged flag in GroupView
[\#1407](https://github.com/matrix-org/matrix-react-sdk/pull/1407)
* Update when a group arrives
[\#1405](https://github.com/matrix-org/matrix-react-sdk/pull/1405)
* Implement `view_group` dispatch when clicking flair
[\#1404](https://github.com/matrix-org/matrix-react-sdk/pull/1404)
* GroupView: Add a User
[\#1402](https://github.com/matrix-org/matrix-react-sdk/pull/1402)
* Track action button click event
[\#1403](https://github.com/matrix-org/matrix-react-sdk/pull/1403)
* Separate sender profile into elements with classes
[\#1401](https://github.com/matrix-org/matrix-react-sdk/pull/1401)
* Fix ugly integration button, use hover to show error
[\#1399](https://github.com/matrix-org/matrix-react-sdk/pull/1399)
* Fix promise error in flair
[\#1400](https://github.com/matrix-org/matrix-react-sdk/pull/1400)
* Flair!
[\#1351](https://github.com/matrix-org/matrix-react-sdk/pull/1351)
* Group Membership UI
[\#1328](https://github.com/matrix-org/matrix-react-sdk/pull/1328)
Changes in [0.10.6](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.6) (2017-09-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.5...v0.10.6)

View file

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "0.10.6",
"version": "0.10.7",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -67,7 +67,7 @@
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3",
"lodash": "^4.13.1",
"matrix-js-sdk": "0.8.4",
"matrix-js-sdk": "0.8.5",
"optimist": "^0.6.1",
"prop-types": "^15.5.8",
"react": "^15.4.0",

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

@ -259,6 +259,11 @@ function textForPowerEvent(event) {
});
}
function textForPinnedEvent(event) {
const senderName = event.getSender();
return _t("%(senderName)s changed the pinned messages for the room.", {senderName});
}
function textForWidgetEvent(event) {
const senderName = event.getSender();
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
@ -304,6 +309,7 @@ const stateHandlers = {
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
'm.room.power_levels': textForPowerEvent,
'm.room.pinned_events': textForPinnedEvent,
'im.vector.modular.widgets': textForWidgetEvent,
};

View file

@ -30,6 +30,10 @@ const FEATURES = [
id: 'feature_groups',
name: _td("Communities"),
},
{
id: 'feature_pinning',
name: _td("Message Pinning"),
},
];
export default {
@ -98,6 +102,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();
@ -1144,6 +1177,10 @@ module.exports = React.createClass({
return ret;
},
onPinnedClick: function() {
this.setState({showingPinned: !this.state.showingPinned, searching: false});
},
onSettingsClick: function() {
this.showSettings(true);
},
@ -1263,7 +1300,7 @@ module.exports = React.createClass({
},
onSearchClick: function() {
this.setState({ searching: true });
this.setState({ searching: true, showingPinned: false });
},
onCancelSearchClick: function() {
@ -1462,6 +1499,7 @@ module.exports = React.createClass({
const RoomSettings = sdk.getComponent("rooms.RoomSettings");
const AuxPanel = sdk.getComponent("rooms.AuxPanel");
const SearchBar = sdk.getComponent("rooms.SearchBar");
const PinnedEventsPanel = sdk.getComponent("rooms.PinnedEventsPanel");
const ScrollPanel = sdk.getComponent("structures.ScrollPanel");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const RoomPreviewBar = sdk.getComponent("rooms.RoomPreviewBar");
@ -1581,9 +1619,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}
@ -1603,6 +1644,9 @@ module.exports = React.createClass({
} else if (this.state.searching) {
hideCancel = true; // has own cancel
aux = <SearchBar ref="search_bar" searchInProgress={this.state.searchInProgress} onCancelClick={this.onCancelSearchClick} onSearch={this.onSearch} />;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
aux = <PinnedEventsPanel room={this.state.room} onCancelClick={this.onPinnedClick} />;
} else if (!myMember || myMember.membership !== "join") {
// We do have a room object for this room, but we're not currently in it.
// We may have a 3rd party invite to it.
@ -1776,6 +1820,7 @@ module.exports = React.createClass({
collapsedRhs={this.props.collapsedRhs}
onSearchClick={this.onSearchClick}
onSettingsClick={this.onSettingsClick}
onPinnedClick={this.onPinnedClick}
onSaveClick={this.onSettingsSaveClick}
onCancelClick={(aux && !hideCancel) ? this.onCancelClick : null}
onForgetClick={(myMember && myMember.membership === "leave") ? this.onForgetClick : null}

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

@ -35,7 +35,9 @@ export default withMatrixClient(React.createClass({
getInitialState: function() {
return {
fetching: false,
fetchingInvitedMembers: false,
members: null,
invitedMembers: null,
truncateAt: INITIAL_LOAD_NUM_MEMBERS,
};
},
@ -46,7 +48,10 @@ export default withMatrixClient(React.createClass({
},
_fetchMembers: function() {
this.setState({fetching: true});
this.setState({
fetching: true,
fetchingInvitedMembers: true,
});
this.props.matrixClient.getGroupUsers(this.props.groupId).then((result) => {
this.setState({
members: result.chunk.map((apiMember) => {
@ -58,6 +63,18 @@ export default withMatrixClient(React.createClass({
this.setState({fetching: false});
console.error("Failed to get group member list: " + e);
});
this.props.matrixClient.getGroupInvitedUsers(this.props.groupId).then((result) => {
this.setState({
invitedMembers: result.chunk.map((apiMember) => {
return groupMemberFromApiObject(apiMember);
}),
fetchingInvitedMembers: false,
});
}).catch((e) => {
this.setState({fetchingInvitedMembers: false});
console.error("Failed to get group invited member list: " + e);
});
},
_createOverflowTile: function(overflowCount, totalCount) {
@ -83,11 +100,10 @@ export default withMatrixClient(React.createClass({
this.setState({ searchQuery: ev.target.value });
},
makeGroupMemberTiles: function(query) {
makeGroupMemberTiles: function(query, memberList) {
const GroupMemberTile = sdk.getComponent("groups.GroupMemberTile");
const TruncatedList = sdk.getComponent("elements.TruncatedList");
query = (query || "").toLowerCase();
let memberList = this.state.members;
if (query) {
memberList = memberList.filter((m) => {
const matchesName = m.displayname.toLowerCase().indexOf(query) !== -1;
@ -118,17 +134,19 @@ export default withMatrixClient(React.createClass({
}
});
return memberList;
return <TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}
>
{ memberList }
</TruncatedList>;
},
render: function() {
if (this.state.fetching) {
if (this.state.fetching || this.state.fetchingInvitedMembers) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_MemberList">
<Spinner />
</div>);
} else if (this.state.members === null) {
return null;
}
const inputBox = (
@ -139,15 +157,21 @@ export default withMatrixClient(React.createClass({
</form>
);
const TruncatedList = sdk.getComponent("elements.TruncatedList");
const joined = this.state.members ? <div className="mx_MemberList_joined">
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.members) }
</div> : <div />;
const invited = this.state.invitedMembers ? <div className="mx_MemberList_invited">
<h2>{ _t("Invited") }</h2>
{ this.makeGroupMemberTiles(this.state.searchQuery, this.state.invitedMembers) }
</div> : <div />;
return (
<div className="mx_MemberList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_MemberList_joined mx_MemberList_outerWrapper">
<TruncatedList className="mx_MemberList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{ this.makeGroupMemberTiles(this.state.searchQuery) }
</TruncatedList>
<GeminiScrollbar autoshow={true} className="mx_MemberList_outerWrapper">
{ joined }
{ invited }
</GeminiScrollbar>
</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

@ -116,7 +116,9 @@ module.exports = React.createClass({
nameEl = (
<div className="mx_EntityTile_details">
<img className="mx_EntityTile_chevron" src="img/member_chevron.png" width="8" height="12" />
<EmojiText element="div" className="mx_EntityTile_name_hover" dir="auto">{ name }</EmojiText>
<EmojiText element="div" className="mx_EntityTile_name mx_EntityTile_name_hover" dir="auto">
{ name }
</EmojiText>
<PresenceLabel activeAgo={activeAgo}
currentlyActive={this.props.presenceCurrentlyActive}
presenceState={this.props.presenceState} />

View file

@ -44,6 +44,7 @@ const eventTileTypes = {
'm.room.history_visibility': 'messages.TextualEvent',
'm.room.encryption': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events' : 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};

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

@ -0,0 +1,90 @@
/*
Copyright 2017 Travis Ralston
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 MatrixClientPeg from "../../../MatrixClientPeg";
import dis from "../../../dispatcher";
import AccessibleButton from "../elements/AccessibleButton";
import MessageEvent from "../messages/MessageEvent";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PinnedEventTile',
propTypes: {
mxRoom: React.PropTypes.object.isRequired,
mxEvent: React.PropTypes.object.isRequired,
onUnpinned: React.PropTypes.func,
},
onTileClicked: function() {
dis.dispatch({
action: 'view_room',
event_id: this.props.mxEvent.getId(),
highlighted: true,
room_id: this.props.mxEvent.getRoomId(),
});
},
onUnpinClicked: function() {
const pinnedEvents = this.props.mxRoom.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
// Nothing to do: already unpinned
if (this.props.onUnpinned) this.props.onUnpinned();
} else {
const pinned = pinnedEvents.getContent().pinned;
const index = pinned.indexOf(this.props.mxEvent.getId());
if (index !== -1) {
pinned.splice(index, 1);
MatrixClientPeg.get().sendStateEvent(this.props.mxRoom.roomId, 'm.room.pinned_events', {pinned}, '')
.then(() => {
if (this.props.onUnpinned) this.props.onUnpinned();
});
} else if (this.props.onUnpinned) this.props.onUnpinned();
}
},
_canUnpin: function() {
return this.props.mxRoom.currentState.mayClientSendStateEvent('m.room.pinned_events', MatrixClientPeg.get());
},
render: function() {
const sender = this.props.mxRoom.getMember(this.props.mxEvent.getSender());
const avatarSize = 40;
let unpinButton = null;
if (this._canUnpin()) {
unpinButton = (
<AccessibleButton onClick={this.onUnpinClicked} className="mx_PinnedEventTile_unpinButton">
<img src="img/cancel-red.svg" width="8" height="8" alt={_t('Unpin Message')} title={_t('Unpin Message')} />
</AccessibleButton>
);
}
return (
<div className="mx_PinnedEventTile">
<div className="mx_PinnedEventTile_actions">
<AccessibleButton className="mx_PinnedEventTile_gotoButton mx_textButton" onClick={this.onTileClicked}>
{ _t("Jump to message") }
</AccessibleButton>
{ unpinButton }
</div>
<MemberAvatar member={sender} width={avatarSize} height={avatarSize} />
<span className="mx_PinnedEventTile_sender">
{ sender.name }
</span>
<MessageEvent mxEvent={this.props.mxEvent} className="mx_PinnedEventTile_body" />
</div>
);
},
});

View file

@ -0,0 +1,105 @@
/*
Copyright 2017 Travis Ralston
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 MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
propTypes: {
// The Room from the js-sdk we're going to show pinned events for
room: React.PropTypes.object.isRequired,
onCancelClick: React.PropTypes.func,
},
getInitialState: function() {
return {
loading: true,
};
},
componentDidMount: function() {
this._updatePinnedMessages();
},
_updatePinnedMessages: function() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents || !pinnedEvents.getContent().pinned) {
this.setState({ loading: false, pinned: [] });
} else {
const promises = [];
const cli = MatrixClientPeg.get();
pinnedEvents.getContent().pinned.map((eventId) => {
promises.push(cli.getEventTimeline(this.props.room.getUnfilteredTimelineSet(), eventId, 0).then(
(timeline) => {
const event = timeline.getEvents().find((e) => e.getId() === eventId);
return {eventId, timeline, event};
}).catch((err) => {
console.error("Error looking up pinned event " + eventId + " in room " + this.props.room.roomId);
console.error(err);
return null; // return lack of context to avoid unhandled errors
}));
});
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => {
if (!context) return false; // no context == not applicable for the room
if (context.event.getType() !== "m.room.message") return false;
if (context.event.isRedacted()) return false;
return true;
});
this.setState({ loading: false, pinned });
});
}
},
_getPinnedTiles: function() {
if (this.state.pinned.length == 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}
return this.state.pinned.map((context) => {
return (<PinnedEventTile key={context.event.getId()}
mxRoom={this.props.room}
mxEvent={context.event}
onUnpinned={this._updatePinnedMessages} />);
});
},
render: function() {
let tiles = <div>{ _t("Loading...") }</div>;
if (this.state && !this.state.loading) {
tiles = this._getPinnedTiles();
}
return (
<div className="mx_PinnedEventsPanel">
<div className="mx_PinnedEventsPanel_body">
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>
</div>
);
},
});

View file

@ -31,6 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader';
import UserSettingsStore from "../../../UserSettingsStore";
linkifyMatrix(linkify);
@ -45,6 +46,7 @@ module.exports = React.createClass({
inRoom: React.PropTypes.bool,
collapsedRhs: React.PropTypes.bool,
onSettingsClick: React.PropTypes.func,
onPinnedClick: React.PropTypes.func,
onSaveClick: React.PropTypes.func,
onSearchClick: React.PropTypes.func,
onLeaveClick: React.PropTypes.func,
@ -129,6 +131,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' });
},
@ -172,6 +178,7 @@ module.exports = React.createClass({
let spinner = null;
let saveButton = null;
let settingsButton = null;
let pinnedEventsButton = null;
let canSetRoomName;
let canSetRoomAvatar;
@ -268,11 +275,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)) {
@ -290,6 +301,13 @@ module.exports = React.createClass({
</AccessibleButton>;
}
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) {
pinnedEventsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
</AccessibleButton>;
}
// var leave_button;
// if (this.props.onLeaveClick) {
// leave_button =
@ -334,6 +352,7 @@ module.exports = React.createClass({
rightRow =
<div className="mx_RoomHeader_rightRow">
{ settingsButton }
{ pinnedEventsButton }
{ manageIntegsButton }
{ forgetButton }
{ searchButton }

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.",
@ -616,6 +619,7 @@
"(~%(count)s results)|other": "(~%(count)s results)",
"Cancel": "Cancel",
"or": "or",
"Message Pinning": "Message Pinning",
"Active call": "Active call",
"Monday": "Monday",
"Tuesday": "Tuesday",
@ -632,6 +636,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 +790,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",
@ -885,6 +890,8 @@
"Add rooms to the group summary": "Add rooms to the group summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Room name or alias": "Room name or alias",
"Pinned Messages": "Pinned Messages",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"You are an administrator of this group": "You are an administrator of this group",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"Failed to remove the room from the summary of %(groupId)s": "Failed to remove the room from the summary of %(groupId)s",

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)",
@ -844,6 +844,7 @@
"+example:%(domain)s": "+example:%(domain)s",
"Group IDs must be of the form +localpart:%(domain)s": "Group IDs must be of the form +localpart:%(domain)s",
"Room creation failed": "Room creation failed",
"Pinned Messages": "Pinned Messages",
"You are a member of these groups:": "You are a member of these groups:",
"Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.": "Create a group to represent your community! Define a set of rooms and your own custom homepage to mark out your space in the Matrix universe.",
"Join an existing group": "Join an existing group",
@ -859,6 +860,7 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Robot check is currently unavailable on desktop - please use a <a>web browser</a>": "Robot check is currently unavailable on desktop - please use a <a>web browser</a>",
"Verifies a user, device, and pubkey tuple": "Verifies a user, device, and pubkey tuple",
"%(senderName)s changed the pinned messages for the room.": "%(senderName)s changed the pinned messages for the room.",
"It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s": "It is currently only possible to create groups on your own home server: use a group ID ending with %(domain)s",
"To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.": "To join an existing group you'll have to know its group identifier; this will look something like <i>+example:matrix.org</i>.",
"%(weekDayName)s, %(monthName)s %(day)s": "%(weekDayName)s, %(monthName)s %(day)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).