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

This commit is contained in:
Stefan Parviainen 2017-10-15 16:49:27 +02:00
commit 9a5cffb5eb
35 changed files with 470 additions and 48 deletions

View File

@ -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");

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,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];
}

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

@ -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

@ -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

@ -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 = (
<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",
@ -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"
}

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).