diff --git a/src/GroupAddressPicker.js b/src/GroupAddressPicker.js index cfd2590780..ca495de16d 100644 --- a/src/GroupAddressPicker.js +++ b/src/GroupAddressPicker.js @@ -76,6 +76,13 @@ function _onGroupInviteFinished(groupId, addrs) { title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}), description: errorList.join(", "), }); + } else { + const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); + Modal.createTrackedDialog('Group invitations sent', '', QuestionDialog, { + title: _t("Invites sent"), + description: _t("Your group invitations have been sent."), + hasCancelButton: false, + }); } }).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 5620bc06df..b306eab23c 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -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); diff --git a/src/Notifier.js b/src/Notifier.js index a2e80353e1..93ef192fe0 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -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', diff --git a/src/TextForEvent.js b/src/TextForEvent.js index 6345403f09..51e3eb8dc9 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -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, }; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 050e28e5b7..b274e6a594 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -30,18 +30,31 @@ const FEATURES = [ id: 'feature_groups', name: _td("Groups"), }, + { + id: 'feature_pinning', + name: _td("Message Pinning"), + }, ]; export default { getLabsFeatures() { const featuresConfig = SdkConfig.get()['features'] || {}; - return FEATURES.filter((f) => { - const sdkConfigValue = featuresConfig[f.id]; - if (!['enable', 'disable'].includes(sdkConfigValue)) { - return true; - } - }).map((f) => { + // The old flag: honourned for backwards compat + const enableLabs = SdkConfig.get()['enableLabs']; + + let labsFeatures; + if (enableLabs) { + labsFeatures = FEATURES; + } else { + labsFeatures = FEATURES.filter((f) => { + const sdkConfigValue = featuresConfig[f.id]; + if (sdkConfigValue === 'labs') { + return true; + } + }); + } + return labsFeatures.map((f) => { return f.id; }); }, @@ -89,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(); }, @@ -193,7 +217,10 @@ export default { isFeatureEnabled: function(featureId: string): boolean { const featuresConfig = SdkConfig.get()['features']; - let sdkConfigValue = 'labs'; + // The old flag: honourned for backwards compat + const enableLabs = SdkConfig.get()['enableLabs']; + + let sdkConfigValue = enableLabs ? 'labs' : 'disable'; if (featuresConfig && featuresConfig[featureId] !== undefined) { sdkConfigValue = featuresConfig[featureId]; } diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js index 3a2ab33db8..cad55351d1 100644 --- a/src/components/structures/RoomStatusBar.js +++ b/src/components/structures/RoomStatusBar.js @@ -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 ( +
+ { _tJsx("There's no one else here! Would you like to invite others or stop warning about the empty room?", + [/(.*?)<\/a>/, /(.*?)<\/a>/], + [ + (sub) => { sub }, + (sub) => { sub }, + ], + ) } +
+ ); + } + return null; }, diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 3d3537e067..83ca987276 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -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 = ; + } else if (this.state.showingPinned) { + hideCancel = true; // has own cancel + aux = ; } 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} diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index d83434f5a8..b69bea9282 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -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({
-
+
+ {_t("Remove +
+
diff --git a/src/components/views/messages/RoomAvatarEvent.js b/src/components/views/messages/RoomAvatarEvent.js index 453394249f..995d5f8531 100644 --- a/src/components/views/messages/RoomAvatarEvent.js +++ b/src/components/views/messages/RoomAvatarEvent.js @@ -70,9 +70,9 @@ module.exports = React.createClass({ // it sucks that _tJsx doesn't support normal _t substitutions :(( return (
- { _tJsx('$senderDisplayName changed the room avatar to ', + { _tJsx('%(senderDisplayName)s changed the room avatar to ', [ - /\$senderDisplayName/, + /%\(senderDisplayName\)s/, //, ], [ diff --git a/src/components/views/messages/TextualBody.js b/src/components/views/messages/TextualBody.js index c0468c38c2..64b23238e5 100644 --- a/src/components/views/messages/TextualBody.js +++ b/src/components/views/messages/TextualBody.js @@ -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 = { body }; diff --git a/src/components/views/room_settings/ColorSettings.js b/src/components/views/room_settings/ColorSettings.js index d914390bd8..bfdaa49f65 100644 --- a/src/components/views/room_settings/ColorSettings.js +++ b/src/components/views/room_settings/ColorSettings.js @@ -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({ diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index f92dc0b97a..499d0ec09a 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -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', }; diff --git a/src/components/views/rooms/MemberInfo.js b/src/components/views/rooms/MemberInfo.js index 180db1d5dd..f768487509 100644 --- a/src/components/views/rooms/MemberInfo.js +++ b/src/components/views/rooms/MemberInfo.js @@ -626,22 +626,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 = ( { this.state.isIgnoring ? _t("Unignore") : _t("Ignore") } ); + + 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 = ( + + { _t('Jump to read receipt') } + + ); + } } - if (!ignoreButton) return null; + if (!ignoreButton && !readReceiptButton) return null; return (

{ _t("User Options") }

+ { readReceiptButton } { ignoreButton }
diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.js index 0713a4ae8a..04b6089559 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.js @@ -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; }); }, diff --git a/src/components/views/rooms/PinnedEventTile.js b/src/components/views/rooms/PinnedEventTile.js new file mode 100644 index 0000000000..65b41d946a --- /dev/null +++ b/src/components/views/rooms/PinnedEventTile.js @@ -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 = ( + + {_t('Unpin + + ); + } + + return ( +
+
+ + { _t("Jump to message") } + + { unpinButton } +
+ + + + { sender.name } + + +
+ ); + }, +}); diff --git a/src/components/views/rooms/PinnedEventsPanel.js b/src/components/views/rooms/PinnedEventsPanel.js new file mode 100644 index 0000000000..deea03f030 --- /dev/null +++ b/src/components/views/rooms/PinnedEventsPanel.js @@ -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 (
{ _t("No pinned messages.") }
); + } + + return this.state.pinned.map((context) => { + return (); + }); + }, + + render: function() { + let tiles =
{ _t("Loading...") }
; + if (this.state && !this.state.loading) { + tiles = this._getPinnedTiles(); + } + + return ( +
+
+ +

{ _t("Pinned Messages") }

+ { tiles } +
+
+ ); + }, +}); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 42cbb90cd9..4df0ff738c 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -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({
+
+ {_t("Remove +
); } else if (this.props.room || (this.props.oobData && this.props.oobData.name)) { @@ -290,6 +301,13 @@ module.exports = React.createClass({ ; } + if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { + pinnedEventsButton = + + + ; + } + // var leave_button; // if (this.props.onLeaveClick) { // leave_button = @@ -334,6 +352,7 @@ module.exports = React.createClass({ rightRow =
{ settingsButton } + { pinnedEventsButton } { manageIntegsButton } { forgetButton } { searchButton } diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.js index b3204ab86e..a363dc2c60 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.js @@ -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; diff --git a/src/i18n/strings/de_DE.json b/src/i18n/strings/de_DE.json index 63f21e80a1..c7b05de215 100644 --- a/src/i18n/strings/de_DE.json +++ b/src/i18n/strings/de_DE.json @@ -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 disabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig deaktiviert.", "You have enabled URL previews by default.": "Du hast die URL-Vorschau standardmäßig aktiviert.", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName hat das Raum-Bild geändert zu ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s hat das Raum-Bild geändert zu ", "%(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", diff --git a/src/i18n/strings/el.json b/src/i18n/strings/el.json index 99ae411d94..06009b0b3b 100644 --- a/src/i18n/strings/el.json +++ b/src/i18n/strings/el.json @@ -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 ": "Ο $senderDisplayName άλλαξε την εικόνα του δωματίου σε ", + "%(senderDisplayName)s changed the room avatar to ": "Ο %(senderDisplayName)s άλλαξε την εικόνα του δωματίου σε ", "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 homeserver's SSL certificate is trusted, and that a browser extension is not blocking requests.": "Δεν είναι δυνατή η σύνδεση στον διακομιστή - παρακαλούμε ελέγξτε την συνδεσιμότητα, βεβαιωθείτε ότι το πιστοποιητικό SSL του διακομιστή είναι έμπιστο και ότι κάποιο πρόσθετο περιηγητή δεν αποτρέπει τα αιτήματα.", diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e542253641..df236636a2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -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 invite others or stop warning about the empty room?": "There's no one else here! Would you like to invite others or stop warning about the empty room?", "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.", "Resend all or cancel all now. You can also select individual messages to resend or cancel.": "Resend all or cancel all 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 ": "$senderDisplayName changed the room avatar to ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", "%(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", @@ -917,5 +924,7 @@ "Related groups for this room:": "Related groups for this room:", "This room has no related groups": "This room has no related groups", "New group ID (e.g. +foo:%(localDomain)s)": "New group ID (e.g. +foo:%(localDomain)s)", + "Invites sent": "Invites sent", + "Your group invitations have been sent.": "Your group invitations have been sent.", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID" } diff --git a/src/i18n/strings/en_US.json b/src/i18n/strings/en_US.json index 5236e959d4..7326b54799 100644 --- a/src/i18n/strings/en_US.json +++ b/src/i18n/strings/en_US.json @@ -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 ": "$senderDisplayName changed the room avatar to ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s changed the room avatar to ", "%(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 web browser": "Robot check is currently unavailable on desktop - please use a web browser", "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 +example:matrix.org.": "To join an existing group you'll have to know its group identifier; this will look something like +example:matrix.org.", "%(weekDayName)s, %(monthName)s %(day)s": "%(weekDayName)s, %(monthName)s %(day)s", diff --git a/src/i18n/strings/eu.json b/src/i18n/strings/eu.json index fcad984ff5..825940fdc4 100644 --- a/src/i18n/strings/eu.json +++ b/src/i18n/strings/eu.json @@ -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 ": "$senderDisplayName erabiltzaileak gelaren abatarra aldatu du beste honetara: ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s erabiltzaileak gelaren abatarra aldatu du beste honetara: ", "%(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", diff --git a/src/i18n/strings/fr.json b/src/i18n/strings/fr.json index d07a162f8d..3a45793b7c 100644 --- a/src/i18n/strings/fr.json +++ b/src/i18n/strings/fr.json @@ -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 ": "$senderDisplayName a changé l’avatar du salon en ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s a changé l’avatar du salon en ", "%(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é l’avatar de %(roomName)s", "Device already verified!": "Appareil déjà vérifié !", diff --git a/src/i18n/strings/hu.json b/src/i18n/strings/hu.json index bcd3452a6c..4e2c39b27b 100644 --- a/src/i18n/strings/hu.json +++ b/src/i18n/strings/hu.json @@ -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 ": "$senderDisplayName megváltoztatta a szoba avatar képét: ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s megváltoztatta a szoba avatar képét: ", "%(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", diff --git a/src/i18n/strings/ko.json b/src/i18n/strings/ko.json index 307ee762ef..98a07e629d 100644 --- a/src/i18n/strings/ko.json +++ b/src/i18n/strings/ko.json @@ -743,7 +743,7 @@ "Start chatting": "이야기하기", "Start Chatting": "이야기하기", "Click on the button below to start chatting!": "이야기하려면 아래 버튼을 누르세요!", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName님이 방 아바타를 로 바꾸셨어요", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s님이 방 아바타를 로 바꾸셨어요", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s님이 방 아바타를 지우셨어요.", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s가 %(roomName)s 방의 아바타를 바꾸셨어요", "Username available": "쓸 수 있는 사용자 이름", diff --git a/src/i18n/strings/lv.json b/src/i18n/strings/lv.json index 0a5e999015..164b6226d9 100644 --- a/src/i18n/strings/lv.json +++ b/src/i18n/strings/lv.json @@ -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 ": "$senderDisplayName nomainīja istabas attēlu uz ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s nomainīja istabas attēlu uz ", "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.", diff --git a/src/i18n/strings/nl.json b/src/i18n/strings/nl.json index 38ca640fc8..c12a2ad81c 100644 --- a/src/i18n/strings/nl.json +++ b/src/i18n/strings/nl.json @@ -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 ": "$senderDisplayName heeft de ruimte avatar aangepast naar ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s heeft de ruimte avatar aangepast naar ", "%(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", diff --git a/src/i18n/strings/pl.json b/src/i18n/strings/pl.json index 0b11e26c44..66e7b2aa28 100644 --- a/src/i18n/strings/pl.json +++ b/src/i18n/strings/pl.json @@ -771,7 +771,7 @@ "for %(amount)sd": "%(amount)s dni", "Idle": "Bezczynny", "Check for update": "Sprawdź aktualizacje", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName zmienił awatar pokoju na ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s zmienił awatar pokoju na ", "%(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 homeserver, or you can pick a different server.": "To będzie twoja nazwa konta na serwerze domowym; możesz też wybrać inny serwer.", diff --git a/src/i18n/strings/pt.json b/src/i18n/strings/pt.json index 56beb43e33..73d7e6ca68 100644 --- a/src/i18n/strings/pt.json +++ b/src/i18n/strings/pt.json @@ -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 ": "$senderDisplayName alterou a imagem da sala para ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s alterou a imagem da sala para ", "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", diff --git a/src/i18n/strings/pt_BR.json b/src/i18n/strings/pt_BR.json index 109c0bb6c2..691cf8d9bf 100644 --- a/src/i18n/strings/pt_BR.json +++ b/src/i18n/strings/pt_BR.json @@ -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 ": "$senderDisplayName alterou a imagem da sala para ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s alterou a imagem da sala para ", "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", diff --git a/src/i18n/strings/ru.json b/src/i18n/strings/ru.json index ab09fd1700..fffce4f328 100644 --- a/src/i18n/strings/ru.json +++ b/src/i18n/strings/ru.json @@ -705,7 +705,7 @@ "Idle": "Неактивен", "Offline": "Не в сети", "Disable URL previews for this room (affects only you)": "Отключить предпросмотр URL-адресов для этой комнаты (влияет только на вас)", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName сменил аватар комнаты на ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s сменил аватар комнаты на ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s удалил аватар комнаты.", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s сменил аватар для %(roomName)s", "Create new room": "Создать новую комнату", diff --git a/src/i18n/strings/tr.json b/src/i18n/strings/tr.json index 98be1c9a64..fdc06f741c 100644 --- a/src/i18n/strings/tr.json +++ b/src/i18n/strings/tr.json @@ -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 ": "$senderDisplayName odanın avatarını olarak çevirdi", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s odanın avatarını 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", diff --git a/src/i18n/strings/zh_Hant.json b/src/i18n/strings/zh_Hant.json index 83aadd5178..35e18b9793 100644 --- a/src/i18n/strings/zh_Hant.json +++ b/src/i18n/strings/zh_Hant.json @@ -228,7 +228,7 @@ "Idle": "閒置", "Offline": "下線", "Disable URL previews for this room (affects only you)": "在這個房間禁止URL預覽(只影響你)", - "$senderDisplayName changed the room avatar to ": "$senderDisplayName 更改了聊天室的圖像為 ", + "%(senderDisplayName)s changed the room avatar to ": "%(senderDisplayName)s 更改了聊天室的圖像為 ", "%(senderDisplayName)s removed the room avatar.": "%(senderDisplayName)s 移除了聊天室圖片。", "%(senderDisplayName)s changed the avatar for %(roomName)s": "%(senderDisplayName)s 更改了聊天室 %(roomName)s 圖像", "Cancel": "取消", diff --git a/src/languageHandler.js b/src/languageHandler.js index b2fc65c46d..a90b78c40e 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -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).