Merge remote-tracking branch 'upstream/develop' into hs/upload-limits

This commit is contained in:
Will Hunt 2018-10-15 19:20:55 +01:00
commit 5a72a5863f
232 changed files with 13386 additions and 11189 deletions

View file

@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -27,8 +28,9 @@ import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import ScalarMessaging from '../../../ScalarMessaging';
import { _t } from '../../../languageHandler';
import WidgetUtils from '../../../WidgetUtils';
import SettingsStore from "../../../settings/SettingsStore";
import WidgetUtils from '../../../utils/WidgetUtils';
import WidgetEchoStore from "../../../stores/WidgetEchoStore";
import AccessibleButton from '../elements/AccessibleButton';
// The maximum number of widgets that can be added in a room
const MAX_WIDGETS = 2;
@ -57,6 +59,7 @@ module.exports = React.createClass({
componentWillMount: function() {
ScalarMessaging.startListening();
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
WidgetEchoStore.on('update', this._updateApps);
},
componentDidMount: function() {
@ -82,6 +85,7 @@ module.exports = React.createClass({
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
WidgetEchoStore.removeListener('update', this._updateApps);
dis.unregister(this.dispatcherRef);
},
@ -106,55 +110,6 @@ module.exports = React.createClass({
}
},
/**
* Encodes a URI according to a set of template variables. Variables will be
* passed through encodeURIComponent.
* @param {string} pathTemplate The path with template variables e.g. '/foo/$bar'.
* @param {Object} variables The key/value pairs to replace the template
* variables with. E.g. { '$bar': 'baz' }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
encodeUri: function(pathTemplate, variables) {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
}
pathTemplate = pathTemplate.replace(
key, encodeURIComponent(variables[key]),
);
}
return pathTemplate;
},
_initAppConfig: function(appId, app, sender) {
const user = MatrixClientPeg.get().getUser(this.props.userId);
const params = {
'$matrix_user_id': this.props.userId,
'$matrix_room_id': this.props.room.roomId,
'$matrix_display_name': user ? user.displayName : this.props.userId,
'$matrix_avatar_url': user ? MatrixClientPeg.get().mxcUrlToHttp(user.avatarUrl) : '',
// TODO: Namespace themes through some standard
'$theme': SettingsStore.getValue("theme"),
};
app.id = appId;
app.name = app.name || app.type;
if (app.data) {
Object.keys(app.data).forEach((key) => {
params['$' + key] = app.data[key];
});
app.waitForIframeLoad = (app.data.waitForIframeLoad === 'false' ? false : true);
}
app.url = this.encodeUri(app.url, params);
app.creatorUserId = (sender && sender.userId) ? sender.userId : null;
return app;
},
onRoomStateEvents: function(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId || ev.getType() !== 'im.vector.modular.widgets') {
return;
@ -163,15 +118,11 @@ module.exports = React.createClass({
},
_getApps: function() {
const appsStateEvents = this.props.room.currentState.getStateEvents('im.vector.modular.widgets');
if (!appsStateEvents) {
return [];
}
return appsStateEvents.filter((ev) => {
return ev.getContent().type && ev.getContent().url;
}).map((ev) => {
return this._initAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
const widgets = WidgetEchoStore.getEchoedRoomWidgets(
this.props.room.roomId, WidgetUtils.getRoomWidgets(this.props.room),
);
return widgets.map((ev) => {
return WidgetUtils.makeAppConfig(ev.getStateKey(), ev.getContent(), ev.sender);
});
},
@ -219,48 +170,57 @@ module.exports = React.createClass({
},
render: function() {
const enableScreenshots = SettingsStore.getValue("enableWidgetScreenshots", this.props.room.room_id);
const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
const apps = this.state.apps.map(
(app, index, arr) => {
return (<AppTile
key={app.id}
id={app.id}
url={app.url}
name={app.name}
type={app.type}
fullWidth={arr.length<2 ? true : false}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={enableScreenshots ? ["m.capability.screenshot"] : []}
/>);
});
return (<AppTile
key={app.id}
id={app.id}
url={app.url}
name={app.name}
type={app.type}
fullWidth={arr.length<2 ? true : false}
room={this.props.room}
userId={this.props.userId}
show={this.props.showApps}
creatorUserId={app.creatorUserId}
widgetPageTitle={(app.data && app.data.title) ? app.data.title : ''}
waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
/>);
});
let addWidget;
if (this.props.showApps &&
this._canUserModify()
) {
addWidget = <div
addWidget = <AccessibleButton
onClick={this.onClickAddWidget}
role='button'
tabIndex='0'
className={this.state.apps.length<2 ?
'mx_AddWidget_button mx_AddWidget_button_full_width' :
'mx_AddWidget_button'
}
title={_t('Add a widget')}>
[+] { _t('Add a widget') }
</div>;
</AccessibleButton>;
}
let spinner;
if (
apps.length === 0 && WidgetEchoStore.roomHasPendingWidgets(
this.props.room.roomId,
WidgetUtils.getRoomWidgets(this.props.room),
)
) {
const Loader = sdk.getComponent("elements.Spinner");
spinner = <Loader />;
}
return (
<div className={'mx_AppsDrawer' + (this.props.hide ? ' mx_AppsDrawer_hidden' : '')}>
<div id='apps' className='mx_AppsContainer'>
{ apps }
{ spinner }
</div>
{ this._canUserModify() && addWidget }
</div>

View file

@ -216,12 +216,12 @@ export default class Autocomplete extends React.Component {
return done.promise;
}
onCompletionClicked(): boolean {
if (this.countCompletions() === 0 || this.state.selectionOffset === COMPOSER_SELECTED) {
onCompletionClicked(selectionOffset: number): boolean {
if (this.countCompletions() === 0 || selectionOffset === COMPOSER_SELECTED) {
return false;
}
this.props.onConfirm(this.state.completionList[this.state.selectionOffset - 1]);
this.props.onConfirm(this.state.completionList[selectionOffset - 1]);
this.hide();
return true;
@ -263,17 +263,14 @@ export default class Autocomplete extends React.Component {
const componentPosition = position;
position++;
const onMouseMove = () => this.setSelection(componentPosition);
const onClick = () => {
this.setSelection(componentPosition);
this.onCompletionClicked();
this.onCompletionClicked(componentPosition);
};
return React.cloneElement(completion.component, {
key: i,
ref: `completion${position - 1}`,
className,
onMouseMove,
onClick,
});
});

View file

@ -47,6 +47,10 @@ const eventTileTypes = {
};
const stateEventTileTypes = {
'm.room.aliases': 'messages.TextualEvent',
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
'm.room.canonical_alias': 'messages.TextualEvent',
'm.room.create': 'messages.RoomCreate',
'm.room.member': 'messages.TextualEvent',
'm.room.name': 'messages.TextualEvent',
'm.room.avatar': 'messages.RoomAvatarEvent',
@ -56,7 +60,7 @@ const stateEventTileTypes = {
'm.room.topic': 'messages.TextualEvent',
'm.room.power_levels': 'messages.TextualEvent',
'm.room.pinned_events': 'messages.TextualEvent',
'm.room.server_acl': 'messages.TextualEvent',
'im.vector.modular.widgets': 'messages.TextualEvent',
};
@ -273,7 +277,11 @@ module.exports = withMatrixClient(React.createClass({
return false;
}
for (let j = 0; j < rA.length; j++) {
if (rA[j].roomMember.userId !== rB[j].roomMember.userId) {
if (rA[j].userId !== rB[j].userId) {
return false;
}
// one has a member set and the other doesn't?
if (rA[j].roomMember !== rB[j].roomMember) {
return false;
}
}
@ -355,7 +363,7 @@ module.exports = withMatrixClient(React.createClass({
// else set it proportional to index
left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset;
const userId = receipt.roomMember.userId;
const userId = receipt.userId;
let readReceiptInfo;
if (this.props.readReceiptMap) {
@ -369,6 +377,7 @@ module.exports = withMatrixClient(React.createClass({
// add to the start so the most recent is on the end (ie. ends up rightmost)
avatars.unshift(
<ReadReceiptMarker key={userId} member={receipt.roomMember}
fallbackUserId={userId}
leftOffset={left} hidden={hidden}
readReceiptInfo={readReceiptInfo}
checkUnmounting={this.props.checkUnmounting}
@ -439,17 +448,6 @@ module.exports = withMatrixClient(React.createClass({
});
},
onPermalinkShareClicked: function(e) {
// These permalinks are like above, can be opened in new tab/window to matrix.to
// but otherwise fire the ShareDialog as it makes little sense to click permalink
// whilst it is in the current room
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room event dialog', '', ShareDialog, {
target: this.props.mxEvent,
});
},
_renderE2EPadlock: function() {
const ev = this.props.mxEvent;
const props = {onClick: this.onCryptoClicked};
@ -493,14 +491,23 @@ module.exports = withMatrixClient(React.createClass({
const eventType = this.props.mxEvent.getType();
// Info messages are basically information about commands processed on a room
const isInfoMessage = (eventType !== 'm.room.message' && eventType !== 'm.sticker');
const isInfoMessage = (
eventType !== 'm.room.message' && eventType !== 'm.sticker' && eventType != 'm.room.create'
);
const EventTileType = sdk.getComponent(getHandlerTile(this.props.mxEvent));
const tileHandler = getHandlerTile(this.props.mxEvent);
// This shouldn't happen: the caller should check we support this type
// before trying to instantiate us
if (!EventTileType) {
throw new Error("Event type not supported");
if (!tileHandler) {
const {mxEvent} = this.props;
console.warn(`Event type not supported: type:${mxEvent.getType()} isState:${mxEvent.isState()}`);
return <div className="mx_EventTile mx_EventTile_info mx_MNoticeBody">
<div className="mx_EventTile_line">
{ _t('This event could not be displayed') }
</div>
</div>;
}
const EventTileType = sdk.getComponent(tileHandler);
const isSending = (['sending', 'queued', 'encrypting'].indexOf(this.props.eventSendStatus) !== -1);
const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted;
@ -538,6 +545,9 @@ module.exports = withMatrixClient(React.createClass({
if (this.props.tileShape === "notif") {
avatarSize = 24;
needsSenderProfile = true;
} else if (tileHandler === 'messages.RoomCreate') {
avatarSize = 0;
needsSenderProfile = false;
} else if (isInfoMessage) {
// a small avatar, with no sender profile, for
// joins/parts/etc
@ -621,13 +631,14 @@ module.exports = withMatrixClient(React.createClass({
switch (this.props.tileShape) {
case 'notif': {
const EmojiText = sdk.getComponent('elements.EmojiText');
const room = this.props.matrixClient.getRoom(this.props.mxEvent.getRoomId());
return (
<div className={classes}>
<div className="mx_EventTile_roomName">
<a href={permalink} onClick={this.onPermalinkClicked}>
<EmojiText element="a" href={permalink} onClick={this.onPermalinkClicked}>
{ room ? room.name : '' }
</a>
</EmojiText>
</div>
<div className="mx_EventTile_senderDetails">
{ avatar }
@ -680,7 +691,7 @@ module.exports = withMatrixClient(React.createClass({
{ avatar }
{ sender }
<div className="mx_EventTile_reply">
<a href={permalink} onClick={this.onPermalinkShareClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
@ -704,10 +715,9 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_EventTile_msgOption">
{ readAvatars }
</div>
{ avatar }
{ sender }
<div className="mx_EventTile_line">
<a href={permalink} onClick={this.onPermalinkShareClicked}>
<a href={permalink} onClick={this.onPermalinkClicked}>
{ timestamp }
</a>
{ this._renderE2EPadlock() }
@ -721,6 +731,12 @@ module.exports = withMatrixClient(React.createClass({
{ keyRequestInfo }
{ editButton }
</div>
{
// The avatar goes after the event tile as it's absolutly positioned to be over the
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
// the need for further z-indexing chaos)
}
{ avatar }
</div>
);
}
@ -742,6 +758,8 @@ module.exports.haveTileForEvent = function(e) {
if (handler === undefined) return false;
if (handler === 'messages.TextualEvent') {
return TextForEvent.textForEvent(e) !== '';
} else if (handler === 'messages.RoomCreate') {
return Boolean(e.getContent()['predecessor']);
} else {
return true;
}

View file

@ -332,13 +332,40 @@ module.exports = withMatrixClient(React.createClass({
});
},
onMuteToggle: function() {
_warnSelfDemote: function() {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
return new Promise((resolve) => {
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Demote yourself?"),
description:
<div>
{ _t("You will not be able to undo this change as you are demoting yourself, " +
"if you are the last privileged user in the room it will be impossible " +
"to regain privileges.") }
</div>,
button: _t("Demote"),
onFinished: resolve,
});
});
},
onMuteToggle: async function() {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
if (!room) return;
// if muting self, warn as it may be irreversible
if (target === this.props.matrixClient.getUserId()) {
try {
if (!(await this._warnSelfDemote())) return;
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
return;
}
}
const powerLevelEvent = room.currentState.getStateEvents("m.room.power_levels", "");
if (!powerLevelEvent) return;
@ -402,7 +429,7 @@ module.exports = withMatrixClient(React.createClass({
console.log("Mod toggle success");
}, function(err) {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'});
dis.dispatch({action: 'require_registration'});
} else {
console.error("Toggle moderator error:" + err);
Modal.createTrackedDialog('Failed to toggle moderator status', '', ErrorDialog, {
@ -436,7 +463,7 @@ module.exports = withMatrixClient(React.createClass({
}).done();
},
onPowerChange: function(powerLevel) {
onPowerChange: async function(powerLevel) {
const roomId = this.props.member.roomId;
const target = this.props.member.userId;
const room = this.props.matrixClient.getRoom(roomId);
@ -455,20 +482,12 @@ module.exports = withMatrixClient(React.createClass({
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
if (myUserId === target) {
Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are demoting yourself, if you are the last privileged user in the room it will be impossible to regain privileges.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
onFinished: (confirmed) => {
if (confirmed) {
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
}
},
});
try {
if (!(await this._warnSelfDemote())) return;
this._applyPowerChange(roomId, target, powerLevel, powerLevelEvent);
} catch (e) {
console.error("Failed to warn about self demotion: ", e);
}
return;
}
@ -478,7 +497,8 @@ module.exports = withMatrixClient(React.createClass({
title: _t("Warning!"),
description:
<div>
{ _t("You will not be able to undo this change as you are promoting the user to have the same power level as yourself.") }<br />
{ _t("You will not be able to undo this change as you are promoting the user " +
"to have the same power level as yourself.") }<br />
{ _t("Are you sure?") }
</div>,
button: _t("Continue"),
@ -578,7 +598,7 @@ module.exports = withMatrixClient(React.createClass({
onMemberAvatarClick: function() {
const member = this.props.member;
const avatarUrl = member.user ? member.user.avatarUrl : member.events.member.getContent().avatar_url;
const avatarUrl = member.getMxcAvatarUrl();
if (!avatarUrl) return;
const httpUrl = this.props.matrixClient.mxcUrlToHttp(avatarUrl);
@ -754,15 +774,15 @@ module.exports = withMatrixClient(React.createClass({
for (const roomId of dmRooms) {
const room = this.props.matrixClient.getRoom(roomId);
if (room) {
const me = room.getMember(this.props.matrixClient.credentials.userId);
const myMembership = room.getMyMembership();
// not a DM room if we have are not joined
if (!me.membership || me.membership !== 'join') continue;
// not a DM room if they are not joined
if (myMembership !== 'join') continue;
const them = this.props.member;
// not a DM room if they are not joined
if (!them.membership || them.membership !== 'join') continue;
const highlight = room.getUnreadNotificationCount('highlight') > 0 || me.membership === 'invite';
const highlight = room.getUnreadNotificationCount('highlight') > 0;
tiles.push(
<RoomTile key={room.roomId} room={room}
@ -771,7 +791,7 @@ module.exports = withMatrixClient(React.createClass({
selected={false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={highlight}
isInvite={me.membership === "invite"}
isInvite={false}
onClick={this.onRoomTileClick}
/>,
);
@ -915,7 +935,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo">
<GeminiScrollbarWrapper autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel" onClick={this.onCancel}>
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" alt={_t('Close')} />
</AccessibleButton>
<div className="mx_MemberInfo_avatar">
<MemberAvatar onClick={this.onMemberAvatarClick} member={this.props.member} width={48} height={48} />

View file

@ -32,10 +32,93 @@ module.exports = React.createClass({
displayName: 'MemberList',
getInitialState: function() {
this.memberDict = this.getMemberDict();
const members = this.roomMembers();
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
// show an empty list
return this._getMembersState([]);
} else {
return this._getMembersState(this.roomMembers());
}
},
componentWillMount: function() {
this._mounted = true;
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
this._showMembersAccordingToMembershipWithLL();
cli.on("Room.myMembership", this.onMyMembership);
} else {
this._listenForMembersChanges();
}
cli.on("Room", this.onRoom); // invites & joining after peek
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl;
this._showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
},
_listenForMembersChanges: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent);
// We listen for changes to the lastPresenceTs which is essentially
// listening for all presence events (we display most of not all of
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
},
componentWillUnmount: function() {
this._mounted = false;
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("RoomMember.name", this.onRoomMemberName);
cli.removeListener("Room.myMembership", this.onMyMembership);
cli.removeListener("RoomState.events", this.onRoomStateEvent);
cli.removeListener("Room", this.onRoom);
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
}
// cancel any pending calls to the rate_limited_funcs
this._updateList.cancelPendingCall();
},
/**
* If lazy loading is enabled, either:
* show a spinner and load the members if the user is joined,
* or show the members available so far if the user is invited
*/
_showMembersAccordingToMembershipWithLL: async function() {
const cli = MatrixClientPeg.get();
if (cli.hasLazyLoadMembersEnabled()) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
const membership = room && room.getMyMembership();
if (membership === "join") {
this.setState({loading: true});
try {
await room.loadMembersIfNeeded();
} catch (ex) {/* already logged in RoomView */}
if (this._mounted) {
this.setState(this._getMembersState(this.roomMembers()));
this._listenForMembersChanges();
}
} else if (membership === "invite") {
// show the members we've got when invited
this.setState(this._getMembersState(this.roomMembers()));
}
}
},
_getMembersState: function(members) {
// set the state after determining _showPresence to make sure it's
// taken into account while rerendering
return {
loading: false,
members: members,
filteredJoinedMembers: this._filterMembers(members, 'join'),
filteredInvitedMembers: this._filterMembers(members, 'invite'),
@ -48,70 +131,6 @@ module.exports = React.createClass({
};
},
componentWillMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.members", this.onRoomStateMember);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomState.events", this.onRoomStateEvent);
cli.on("Room", this.onRoom); // invites
// We listen for changes to the lastPresenceTs which is essentially
// listening for all presence events (we display most of not all of
// the information contained in presence events).
cli.on("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.on("Room.timeline", this.onRoomTimeline);
const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"];
const hsUrl = MatrixClientPeg.get().baseUrl;
this._showPresence = true;
if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) {
this._showPresence = enablePresenceByHsUrl[hsUrl];
}
},
componentWillUnmount: function() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.members", this.onRoomStateMember);
cli.removeListener("RoomMember.name", this.onRoomMemberName);
cli.removeListener("RoomState.events", this.onRoomStateEvent);
cli.removeListener("Room", this.onRoom);
cli.removeListener("User.lastPresenceTs", this.onUserLastPresenceTs);
// cli.removeListener("Room.timeline", this.onRoomTimeline);
}
// cancel any pending calls to the rate_limited_funcs
this._updateList.cancelPendingCall();
},
/*
onRoomTimeline: function(ev, room, toStartOfTimeline, removed, data) {
// ignore anything but real-time updates at the end of the room:
// updates from pagination will happen when the paginate completes.
if (toStartOfTimeline || !data || !data.liveEvent) return;
// treat any activity from a user as implicit presence to update the
// ordering of the list whenever someone says something.
// Except right now we're not tiebreaking "active now" users in this way
// so don't bother for now.
if (ev.getSender()) {
// console.log("implicit presence from " + ev.getSender());
var tile = this.refs[ev.getSender()];
if (tile) {
// work around a race where you might have a room member object
// before the user object exists. XXX: why does this ever happen?
var all_members = room.currentState.members;
var userId = ev.getSender();
if (all_members[userId].user === null) {
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
}
this._updateList(); // reorder the membership list
}
}
},
*/
onUserLastPresenceTs(event, user) {
// Attach a SINGLE listener for global presence changes then locate the
// member tile and re-render it. This is more efficient than every tile
@ -130,28 +149,40 @@ module.exports = React.createClass({
// We listen for room events because when we accept an invite
// we need to wait till the room is fully populated with state
// before refreshing the member list else we get a stale list.
this._updateList();
this._showMembersAccordingToMembershipWithLL();
},
onMyMembership: function(room, membership, oldMembership) {
if (room.roomId === this.props.roomId && membership === "join") {
this._showMembersAccordingToMembershipWithLL();
}
},
onRoomStateMember: function(ev, state, member) {
if (member.roomId !== this.props.roomId) {
return;
}
this._updateList();
},
onRoomMemberName: function(ev, member) {
if (member.roomId !== this.props.roomId) {
return;
}
this._updateList();
},
onRoomStateEvent: function(event, state) {
if (event.getType() === "m.room.third_party_invite") {
if (event.getRoomId() === this.props.roomId &&
event.getType() === "m.room.third_party_invite") {
this._updateList();
}
},
_updateList: new rate_limited_func(function() {
// console.log("Updating memberlist");
this.memberDict = this.getMemberDict();
const newState = {
loading: false,
members: this.roomMembers(),
};
newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery);
@ -159,50 +190,43 @@ module.exports = React.createClass({
this.setState(newState);
}, 500),
getMemberDict: function() {
if (!this.props.roomId) return {};
getMembersWithUser: function() {
if (!this.props.roomId) return [];
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
if (!room) return {};
if (!room) return [];
const all_members = room.currentState.members;
const allMembers = Object.values(room.currentState.members);
Object.keys(all_members).map(function(userId) {
allMembers.forEach(function(member) {
// work around a race where you might have a room member object
// before the user object exists. This may or may not cause
// https://github.com/vector-im/vector-web/issues/186
if (all_members[userId].user === null) {
all_members[userId].user = MatrixClientPeg.get().getUser(userId);
if (member.user === null) {
member.user = cli.getUser(member.userId);
}
// XXX: this user may have no lastPresenceTs value!
// the right solution here is to fix the race rather than leave it as 0
});
return all_members;
return allMembers;
},
roomMembers: function() {
const all_members = this.memberDict || {};
const all_user_ids = Object.keys(all_members);
const ConferenceHandler = CallHandler.getConferenceHandler();
all_user_ids.sort(this.memberSort);
const to_display = [];
let count = 0;
for (let i = 0; i < all_user_ids.length; ++i) {
const user_id = all_user_ids[i];
const m = all_members[user_id];
if (m.membership === 'join' || m.membership === 'invite') {
if ((ConferenceHandler && !ConferenceHandler.isConferenceUser(user_id)) || !ConferenceHandler) {
to_display.push(user_id);
++count;
}
}
}
return to_display;
const allMembers = this.getMembersWithUser();
const filteredAndSortedMembers = allMembers.filter((m) => {
return (
m.membership === 'join' || m.membership === 'invite'
) && (
!ConferenceHandler ||
(ConferenceHandler && !ConferenceHandler.isConferenceUser(m.userId))
);
});
filteredAndSortedMembers.sort(this.memberSort);
return filteredAndSortedMembers;
},
_createOverflowTileJoined: function(overflowCount, totalCount) {
@ -249,14 +273,12 @@ module.exports = React.createClass({
// returns negative if a comes before b,
// returns 0 if a and b are equivalent in ordering
// returns positive if a comes after b.
memberSort: function(userIdA, userIdB) {
memberSort: function(memberA, memberB) {
// order by last active, with "active now" first.
// ...and then by power
// ...and then alphabetically.
// We could tiebreak instead by "last recently spoken in this room" if we wanted to.
const memberA = this.memberDict[userIdA];
const memberB = this.memberDict[userIdB];
const userA = memberA.user;
const userB = memberB.user;
@ -306,9 +328,7 @@ module.exports = React.createClass({
},
_filterMembers: function(members, membership, query) {
return members.filter((userId) => {
const m = this.memberDict[userId];
return members.filter((m) => {
if (query) {
query = query.toLowerCase();
const matchesName = m.name.toLowerCase().indexOf(query) !== -1;
@ -350,10 +370,9 @@ module.exports = React.createClass({
_makeMemberTiles: function(members, membership) {
const MemberTile = sdk.getComponent("rooms.MemberTile");
const memberList = members.map((userId) => {
const m = this.memberDict[userId];
const memberList = members.map((m) => {
return (
<MemberTile key={userId} member={m} ref={userId} showPresence={this._showPresence} />
<MemberTile key={m.userId} member={m} ref={m.userId} showPresence={this._showPresence} />
);
});
@ -393,6 +412,11 @@ module.exports = React.createClass({
},
render: function() {
if (this.state.loading) {
const Spinner = sdk.getComponent("elements.Spinner");
return <div className="mx_MemberList"><Spinner /></div>;
}
const TruncatedList = sdk.getComponent("elements.TruncatedList");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Copyright 2017, 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,7 +16,7 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { _t, _td } from '../../../languageHandler';
import CallHandler from '../../../CallHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
@ -25,6 +25,18 @@ import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to';
const formatButtonList = [
_td("bold"),
_td("italic"),
_td("deleted"),
_td("underlined"),
_td("inline-code"),
_td("block-quote"),
_td("bulleted-list"),
_td("numbered-list"),
];
export default class MessageComposer extends React.Component {
constructor(props, context) {
@ -35,25 +47,24 @@ export default class MessageComposer extends React.Component {
this.onUploadFileSelected = this.onUploadFileSelected.bind(this);
this.uploadFiles = this.uploadFiles.bind(this);
this.onVoiceCallClick = this.onVoiceCallClick.bind(this);
this.onInputContentChanged = this.onInputContentChanged.bind(this);
this._onAutocompleteConfirm = this._onAutocompleteConfirm.bind(this);
this.onToggleFormattingClicked = this.onToggleFormattingClicked.bind(this);
this.onToggleMarkdownClicked = this.onToggleMarkdownClicked.bind(this);
this.onInputStateChanged = this.onInputStateChanged.bind(this);
this.onEvent = this.onEvent.bind(this);
this._onRoomStateEvents = this._onRoomStateEvents.bind(this);
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._onTombstoneClick = this._onTombstoneClick.bind(this);
this.state = {
autocompleteQuery: '',
selection: null,
inputState: {
style: [],
marks: [],
blockType: null,
isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
wordCount: 0,
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
},
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
tombstone: this._getRoomTombstone(),
};
}
@ -63,12 +74,31 @@ export default class MessageComposer extends React.Component {
// marked as encrypted.
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
MatrixClientPeg.get().on("event", this.onEvent);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember();
}
_waitForOwnMember() {
// if we have the member already, do that
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
if (me) {
this.setState({me});
return;
}
// Otherwise, wait for member loading to finish and then update the member for the avatar.
// The members should already be loading, and loadMembersIfNeeded
// will return the promise for the existing operation
this.props.room.loadMembersIfNeeded().then(() => {
const me = this.props.room.getMember(MatrixClientPeg.get().getUserId());
this.setState({me});
});
}
componentWillUnmount() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("event", this.onEvent);
MatrixClientPeg.get().removeListener("RoomState.events", this._onRoomStateEvents);
}
if (this._roomStoreToken) {
this._roomStoreToken.remove();
@ -81,6 +111,18 @@ export default class MessageComposer extends React.Component {
this.forceUpdate();
}
_onRoomStateEvents(ev, state) {
if (ev.getRoomId() !== this.props.room.roomId) return;
if (ev.getType() === 'm.room.tombstone') {
this.setState({tombstone: this._getRoomTombstone()});
}
}
_getRoomTombstone() {
return this.props.room.currentState.getStateEvents('m.room.tombstone', '');
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
@ -89,7 +131,7 @@ export default class MessageComposer extends React.Component {
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_set_mxid'});
dis.dispatch({action: 'require_registration'});
return;
}
@ -211,13 +253,6 @@ export default class MessageComposer extends React.Component {
});
}
onInputContentChanged(content: string, selection: {start: number, end: number}) {
this.setState({
autocompleteQuery: content,
selection,
});
}
onInputStateChanged(inputState) {
this.setState({inputState});
}
@ -228,7 +263,7 @@ export default class MessageComposer extends React.Component {
}
}
onFormatButtonClicked(name: "bold" | "italic" | "strike" | "code" | "underline" | "quote" | "bullet" | "numbullet", event) {
onFormatButtonClicked(name, event) {
event.preventDefault();
this.messageComposerInput.onFormatButtonClicked(name, event);
}
@ -240,11 +275,21 @@ export default class MessageComposer extends React.Component {
onToggleMarkdownClicked(e) {
e.preventDefault(); // don't steal focus from the editor!
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichtextEnabled);
this.messageComposerInput.enableRichtext(!this.state.inputState.isRichTextEnabled);
}
_onTombstoneClick(ev) {
ev.preventDefault();
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
dis.dispatch({
action: 'view_room',
highlighted: true,
room_id: replacementRoomId,
});
}
render() {
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -252,11 +297,13 @@ export default class MessageComposer extends React.Component {
const controls = [];
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} />
</div>,
);
if (this.state.me) {
controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={this.state.me} width={24} height={24} />
</div>,
);
}
let e2eImg, e2eTitle, e2eClass;
const roomIsEncrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.room.roomId);
@ -281,49 +328,51 @@ export default class MessageComposer extends React.Component {
let videoCallButton;
let hangupButton;
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
// Call buttons
if (this.props.callState && this.props.callState !== 'ended') {
hangupButton =
<div key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<AccessibleButton key="controls_hangup" className="mx_MessageComposer_hangup" onClick={this.onHangupClick}>
<img src="img/hangup.svg" alt={_t('Hangup')} title={_t('Hangup')} width="25" height="26" />
</div>;
</AccessibleButton>;
} else {
callButton =
<div key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
<AccessibleButton key="controls_call" className="mx_MessageComposer_voicecall" onClick={this.onVoiceCallClick} title={_t('Voice call')}>
<TintableSvg src="img/icon-call.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
videoCallButton =
<div key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
<AccessibleButton key="controls_videocall" className="mx_MessageComposer_videocall" onClick={this.onCallClick} title={_t('Video call')}>
<TintableSvg src="img/icons-video.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
}
const canSendMessages = this.props.room.currentState.maySendMessage(
MatrixClientPeg.get().credentials.userId);
const canSendMessages = !this.state.tombstone &&
this.props.room.maySendMessage();
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
const uploadButton = (
<div key="controls_upload" className="mx_MessageComposer_upload"
<AccessibleButton key="controls_upload" className="mx_MessageComposer_upload"
onClick={this.onUploadClick} title={_t('Upload file')}>
<TintableSvg src="img/icons-upload.svg" width="35" height="35" />
<input ref="uploadInput" type="file"
style={uploadInputStyle}
multiple
onChange={this.onUploadFileSelected} />
</div>
</AccessibleButton>
);
const formattingButton = (
<img className="mx_MessageComposer_formatting"
const formattingButton = this.state.inputState.isRichTextEnabled ? (
<AccessibleButton element="img" className="mx_MessageComposer_formatting"
alt={_t("Show Text Formatting Toolbar")}
title={_t("Show Text Formatting Toolbar")}
src="img/button-text-formatting.svg"
onClick={this.onToggleFormattingClicked}
style={{visibility: this.state.showFormatting ? 'hidden' : 'visible'}}
key="controls_formatting" />
);
) : null;
let placeholderText;
if (this.state.isQuoting) {
@ -350,7 +399,6 @@ export default class MessageComposer extends React.Component {
room={this.props.room}
placeholder={placeholderText}
onFilesPasted={this.uploadFiles}
onContentChanged={this.onInputContentChanged}
onInputStateChanged={this.onInputStateChanged} />,
formattingButton,
stickerpickerButton,
@ -359,6 +407,23 @@ export default class MessageComposer extends React.Component {
callButton,
videoCallButton,
);
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src="img/room_replaced.svg" />
<span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")}
</span><br />
<a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this._onTombstoneClick}
>
{_t("The conversation continues here.")}
</a>
</div>
</div>);
} else {
controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error">
@ -367,11 +432,14 @@ export default class MessageComposer extends React.Component {
);
}
const {style, blockType} = this.state.inputState;
const formatButtons = ["bold", "italic", "strike", "underline", "code", "quote", "bullet", "numbullet"].map(
(name) => {
const active = style.includes(name) || blockType === name;
const suffix = active ? '-o-n' : '';
let formatBar;
if (this.state.showFormatting && this.state.inputState.isRichTextEnabled) {
const {marks, blockType} = this.state.inputState;
const formatButtons = formatButtonList.map((name) => {
// special-case to match the md serializer and the special-case in MessageComposerInput.js
const markName = name === 'inline-code' ? 'code' : name;
const active = marks.some(mark => mark.type === markName) || blockType === name;
const suffix = active ? '-on' : '';
const onFormatButtonClicked = this.onFormatButtonClicked.bind(this, name);
const className = 'mx_MessageComposer_format_button mx_filterFlipColor';
return <img className={className}
@ -380,8 +448,25 @@ export default class MessageComposer extends React.Component {
key={name}
src={`img/button-text-${name}${suffix}.svg`}
height="17" />;
},
);
},
);
formatBar =
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar">
{ formatButtons }
<div style={{flex: 1}}></div>
<img title={this.state.inputState.isRichTextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichTextEnabled}.png`} />
<AccessibleButton element="img" title={_t("Hide Text Formatting Toolbar")}
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>
}
return (
<div className="mx_MessageComposer">
@ -390,20 +475,7 @@ export default class MessageComposer extends React.Component {
{ controls }
</div>
</div>
<div className="mx_MessageComposer_formatbar_wrapper">
<div className="mx_MessageComposer_formatbar" style={this.state.showFormatting ? {} : {display: 'none'}}>
{ formatButtons }
<div style={{flex: 1}}></div>
<img title={this.state.inputState.isRichtextEnabled ? _t("Turn Markdown on") : _t("Turn Markdown off")}
onMouseDown={this.onToggleMarkdownClicked}
className="mx_MessageComposer_formatbar_markdown mx_filterFlipColor"
src={`img/button-md-${!this.state.inputState.isRichtextEnabled}.png`} />
<img title={_t("Hide Text Formatting Toolbar")}
onClick={this.onToggleFormattingClicked}
className="mx_MessageComposer_formatbar_cancel mx_filterFlipColor"
src="img/icon-text-cancel.svg" />
</div>
</div>
{ formatBar }
</div>
);
}

File diff suppressed because it is too large Load diff

View file

@ -41,7 +41,10 @@ module.exports = React.createClass({
propTypes: {
// the RoomMember to show the RR for
member: PropTypes.object.isRequired,
member: PropTypes.object,
// userId to fallback the avatar to
// if the member hasn't been loaded yet
fallbackUserId: PropTypes.string.isRequired,
// number of pixels to offset the avatar from the right of its parent;
// typically a negative value.
@ -130,8 +133,7 @@ module.exports = React.createClass({
// the docs for `offsetParent` say it may be null if `display` is
// `none`, but I can't see why that would happen.
console.warn(
`ReadReceiptMarker for ${this.props.member.userId} in ` +
`${this.props.member.roomId} has no offsetParent`,
`ReadReceiptMarker for ${this.props.fallbackUserId} in has no offsetParent`,
);
startTopOffset = 0;
} else {
@ -186,17 +188,17 @@ module.exports = React.createClass({
let title;
if (this.props.timestamp) {
const dateString = formatDate(new Date(this.props.timestamp), this.props.showTwelveHour);
if (this.props.member.userId === this.props.member.rawDisplayName) {
if (!this.props.member || this.props.fallbackUserId === this.props.member.rawDisplayName) {
title = _t(
"Seen by %(userName)s at %(dateTime)s",
{userName: this.props.member.userId,
{userName: this.props.fallbackUserId,
dateTime: dateString},
);
} else {
title = _t(
"Seen by %(displayName)s (%(userName)s) at %(dateTime)s",
{displayName: this.props.member.rawDisplayName,
userName: this.props.member.userId,
userName: this.props.fallbackUserId,
dateTime: dateString},
);
}
@ -208,6 +210,7 @@ module.exports = React.createClass({
enterTransitionOpts={this.state.enterTransitionOpts} >
<MemberAvatar
member={this.props.member}
fallbackUserId={this.props.fallbackUserId}
aria-hidden="true"
width={14} height={14} resizeMethod="crop"
style={style}

View file

@ -16,7 +16,7 @@ limitations under the License.
'use strict';
var React = require('react');
const React = require('react');
module.exports = React.createClass({
displayName: 'RoomDropTarget',
@ -31,5 +31,5 @@ module.exports = React.createClass({
</div>
</div>
);
}
},
});

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2017, 2018 Vector Creations Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -16,6 +16,8 @@ limitations under the License.
*/
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require("react");
const ReactDOM = require("react-dom");
import PropTypes from 'prop-types';
@ -33,7 +35,12 @@ import RoomListStore from '../../../stores/RoomListStore';
import GroupStore from '../../../stores/GroupStore';
const HIDE_CONFERENCE_CHANS = true;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
function labelForTagName(tagName) {
if (tagName.startsWith('u.')) return tagName.slice(2);
return tagName;
}
function phraseForSection(section) {
switch (section) {
@ -90,7 +97,7 @@ module.exports = React.createClass({
};
// All rooms that should be kept in the room list when filtering.
// By default, show all rooms.
this._visibleRooms = MatrixClientPeg.get().getRooms();
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
// Listen to updates to group data. RoomList cares about members and rooms in order
// to filter the room list when group tags are selected.
@ -295,7 +302,7 @@ module.exports = React.createClass({
this._visibleRooms = Array.from(roomSet);
} else {
// Show all rooms
this._visibleRooms = MatrixClientPeg.get().getRooms();
this._visibleRooms = MatrixClientPeg.get().getVisibleRooms();
}
this._delayedRefreshRoomList();
},
@ -340,8 +347,8 @@ module.exports = React.createClass({
if (!taggedRoom) {
return;
}
const me = taggedRoom.getMember(MatrixClientPeg.get().credentials.userId);
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, me, this.props.ConferenceHandler)) {
const myUserId = MatrixClientPeg.get().getUserId();
if (HIDE_CONFERENCE_CHANS && Rooms.isConfCallRoom(taggedRoom, myUserId, this.props.ConferenceHandler)) {
return;
}
@ -444,6 +451,8 @@ module.exports = React.createClass({
}
}
if (!this.stickies) return;
const self = this;
let scrollStuckOffset = 0;
// Scroll to the passed in position, i.e. a header was clicked and in a scroll to state
@ -608,10 +617,14 @@ module.exports = React.createClass({
const RoomSubList = sdk.getComponent('structures.RoomSubList');
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
// XXX: we can't detect device-level (localStorage) settings onChange as the SettingsStore does not notify
// so checking on every render is the sanest thing at this time.
const showEmpty = SettingsStore.getValue('RoomSubList.showEmpty');
const self = this;
return (
<GeminiScrollbarWrapper className="mx_RoomList_scrollbar"
autoshow={true} onScroll={self._whenScrolling} wrappedRef={this._collectGemini}>
autoshow={true} onScroll={self._whenScrolling} onResize={self._whenScrolling} wrappedRef={this._collectGemini}>
<div className="mx_RoomList">
<RoomSubList list={[]}
extraTiles={this._makeGroupInviteTiles(self.props.searchFilter)}
@ -623,6 +636,7 @@ module.exports = React.createClass({
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty}
/>
<RoomSubList list={self.state.lists['im.vector.fake.invite']}
@ -635,6 +649,7 @@ module.exports = React.createClass({
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty}
/>
<RoomSubList list={self.state.lists['m.favourite']}
@ -647,7 +662,8 @@ module.exports = React.createClass({
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />
<RoomSubList list={self.state.lists['im.vector.fake.direct']}
label={_t('People')}
@ -661,7 +677,8 @@ module.exports = React.createClass({
alwaysShowHeader={true}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />
<RoomSubList list={self.state.lists['im.vector.fake.recent']}
label={_t('Rooms')}
@ -673,13 +690,14 @@ module.exports = React.createClass({
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />
{ Object.keys(self.state.lists).map((tagName) => {
if (!tagName.match(STANDARD_TAGS_REGEX)) {
return <RoomSubList list={self.state.lists[tagName]}
key={tagName}
label={tagName}
label={labelForTagName(tagName)}
tagName={tagName}
emptyContent={this._getEmptyContent(tagName)}
editable={true}
@ -688,7 +706,8 @@ module.exports = React.createClass({
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />;
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />;
}
}) }
@ -702,9 +721,17 @@ module.exports = React.createClass({
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms} />
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />
<RoomSubList list={self.state.lists['im.vector.fake.archived']}
emptyContent={self.props.collapsed ? null :
<div className="mx_RoomList_emptySubListTip_container">
<div className="mx_RoomList_emptySubListTip">
{ _t('You have no historical rooms') }
</div>
</div>
}
label={_t('Historical')}
editable={false}
order="recent"
@ -712,10 +739,23 @@ module.exports = React.createClass({
alwaysShowHeader={true}
startAsHidden={true}
showSpinner={self.state.isLoadingLeftRooms}
onHeaderClick= {self.onArchivedHeaderClick}
onHeaderClick={self.onArchivedHeaderClick}
incomingCall={self.state.incomingCall}
searchFilter={self.props.searchFilter}
onShowMoreRooms={self.onShowMoreRooms} />
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={showEmpty} />
<RoomSubList list={self.state.lists['m.server_notice']}
label={_t('System Alerts')}
tagName="m.lowpriority"
editable={false}
order="recent"
incomingCall={self.state.incomingCall}
collapsed={self.props.collapsed}
searchFilter={self.props.searchFilter}
onHeaderClick={self.onSubListHeaderClick}
onShowMoreRooms={self.onShowMoreRooms}
showEmpty={false} />
</div>
</GeminiScrollbarWrapper>
);

View file

@ -98,15 +98,11 @@ module.exports = React.createClass({
</div>);
}
const myMember = this.props.room ? this.props.room.currentState.members[
MatrixClientPeg.get().credentials.userId
] : null;
const kicked = (
myMember &&
myMember.membership == 'leave' &&
myMember.events.member.getSender() != MatrixClientPeg.get().credentials.userId
);
const banned = myMember && myMember.membership == 'ban';
const myMember = this.props.room ?
this.props.room.getMember(MatrixClientPeg.get().getUserId()) :
null;
const kicked = myMember && myMember.isKicked();
const banned = myMember && myMember && myMember.membership == 'ban';
if (this.props.inviterName) {
let emailMatchBlock;

View file

@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -571,6 +572,11 @@ module.exports = React.createClass({
});
},
_onRoomUpgradeClick: function() {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
},
_onRoomMemberMembership: function() {
// Update, since our banned user list may have changed
this.forceUpdate();
@ -793,15 +799,15 @@ module.exports = React.createClass({
}
let leaveButton = null;
const myMember = this.props.room.getMember(myUserId);
if (myMember) {
if (myMember.membership === "join") {
const myMemberShip = this.props.room.getMyMembership();
if (myMemberShip) {
if (myMemberShip === "join") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onLeaveClick}>
{ _t('Leave room') }
</AccessibleButton>
);
} else if (myMember.membership === "leave") {
} else if (myMemberShip === "leave") {
leaveButton = (
<AccessibleButton className="mx_RoomSettings_leaveButton" onClick={this.onForgetClick}>
{ _t('Forget room') }
@ -929,6 +935,13 @@ module.exports = React.createClass({
);
});
let roomUpgradeButton = null;
if (this.props.room.shouldUpgradeToVersion() && this.props.room.userMayUpgradeRoom(myUserId)) {
roomUpgradeButton = <AccessibleButton className="mx_RoomSettings_upgradeButton danger" onClick={this._onRoomUpgradeClick}>
{ _t("Upgrade room to version %(ver)s", {ver: this.props.room.shouldUpgradeToVersion()}) }
</AccessibleButton>;
}
return (
<div className="mx_RoomSettings">
@ -1039,7 +1052,9 @@ module.exports = React.createClass({
<h3>{ _t('Advanced') }</h3>
<div className="mx_RoomSettings_settings">
{ _t('This room\'s internal ID is') } <code>{ this.props.room.roomId }</code>
{ _t('Internal room ID: ') } <code>{ this.props.room.roomId }</code><br />
{ _t('Room version number: ') } <code>{ this.props.room.getVersion() }</code><br />
{ roomUpgradeButton }
</div>
</div>
);

View file

@ -243,9 +243,7 @@ module.exports = React.createClass({
},
render: function() {
const myUserId = MatrixClientPeg.get().credentials.userId;
const me = this.props.room.currentState.members[myUserId];
const isInvite = this.props.room.getMyMembership() === "invite";
const notificationCount = this.state.notificationCount;
// var highlightCount = this.props.room.getUnreadNotificationCount("highlight");
@ -259,7 +257,7 @@ module.exports = React.createClass({
'mx_RoomTile_unread': this.props.unread,
'mx_RoomTile_unreadNotify': notifBadges,
'mx_RoomTile_highlight': mentionBadges,
'mx_RoomTile_invited': (me && me.membership === 'invite'),
'mx_RoomTile_invited': isInvite,
'mx_RoomTile_menuDisplayed': this.state.menuDisplayed,
'mx_RoomTile_noBadges': !badges,
'mx_RoomTile_transparent': this.props.transparent,
@ -275,6 +273,7 @@ module.exports = React.createClass({
});
let name = this.state.roomName;
if (name == undefined || name == null) name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let badgeContent;

View file

@ -0,0 +1,57 @@
/*
Copyright 2018 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import Modal from '../../../Modal';
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'RoomUpgradeWarningBar',
propTypes: {
room: PropTypes.object.isRequired,
},
onUpgradeClick: function() {
const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog');
Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, {room: this.props.room});
},
render: function() {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
return (
<div className="mx_RoomUpgradeWarningBar">
<div className="mx_RoomUpgradeWarningBar_header">
{_t("There is a known vulnerability affecting this room.")}
</div>
<div className="mx_RoomUpgradeWarningBar_body">
{_t("This room version is vulnerable to malicious modification of room state.")}
</div>
<p className="mx_RoomUpgradeWarningBar_upgradelink">
<AccessibleButton onClick={this.onUpgradeClick}>
{_t("Click here to upgrade to the latest room version and ensure room integrity is protected.")}
</AccessibleButton>
</p>
<div className="mx_RoomUpgradeWarningBar_small">
{_t("Only room administrators will see this warning")}
</div>
</div>
);
},
});

View file

@ -16,11 +16,11 @@ limitations under the License.
'use strict';
var React = require('react');
var MatrixClientPeg = require('../../../MatrixClientPeg');
var sdk = require('../../../index');
var classNames = require('classnames');
var AccessibleButton = require('../../../components/views/elements/AccessibleButton');
const React = require('react');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require('../../../index');
const classNames = require('classnames');
const AccessibleButton = require('../../../components/views/elements/AccessibleButton');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
@ -28,7 +28,7 @@ module.exports = React.createClass({
getInitialState: function() {
return ({
scope: 'Room'
scope: 'Room',
});
},
@ -54,18 +54,18 @@ module.exports = React.createClass({
},
render: function() {
var searchButtonClasses = classNames({ mx_SearchBar_searchButton : true, mx_SearchBar_searching: this.props.searchInProgress });
var thisRoomClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'Room' });
var allRoomsClasses = classNames({ mx_SearchBar_button : true, mx_SearchBar_unselected : this.state.scope !== 'All' });
const searchButtonClasses = classNames({ mx_SearchBar_searchButton: true, mx_SearchBar_searching: this.props.searchInProgress });
const thisRoomClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'Room' });
const allRoomsClasses = classNames({ mx_SearchBar_button: true, mx_SearchBar_unselected: this.state.scope !== 'All' });
return (
<div className="mx_SearchBar">
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange}/>
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")}/></AccessibleButton>
<div className="mx_SearchBar">
<input ref="search_term" className="mx_SearchBar_input" type="text" autoFocus={true} placeholder={_t("Search…")} onKeyDown={this.onSearchChange} />
<AccessibleButton className={ searchButtonClasses } onClick={this.onSearch}><img src="img/search-button.svg" width="37" height="37" alt={_t("Search")} /></AccessibleButton>
<AccessibleButton className={ thisRoomClasses } onClick={this.onThisRoomClick}>{_t("This Room")}</AccessibleButton>
<AccessibleButton className={ allRoomsClasses } onClick={this.onAllRoomsClick}>{_t("All Rooms")}</AccessibleButton>
<AccessibleButton className="mx_SearchBar_cancel" onClick={this.props.onCancelClick}><img src="img/cancel.svg" width="18" height="18" /></AccessibleButton>
</div>
);
}
},
});

View file

@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
import { _t } from '../../../languageHandler';
import Widgets from '../../../utils/widgets';
import AppTile from '../elements/AppTile';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
@ -24,9 +23,15 @@ import SdkConfig from '../../../SdkConfig';
import ScalarAuthClient from '../../../ScalarAuthClient';
import dis from '../../../dispatcher';
import AccessibleButton from '../elements/AccessibleButton';
import WidgetUtils from '../../../utils/WidgetUtils';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore';
const widgetType = 'm.stickerpicker';
// We sit in a context menu, so the persisted element container needs to float
// above it, so it needs a greater z-index than the ContextMenu
const STICKERPICKER_Z_INDEX = 5000;
export default class Stickerpicker extends React.Component {
constructor(props) {
super(props);
@ -39,8 +44,6 @@ export default class Stickerpicker extends React.Component {
this._onResize = this._onResize.bind(this);
this._onFinished = this._onFinished.bind(this);
this._collectWidgetMessaging = this._collectWidgetMessaging.bind(this);
this.popoverWidth = 300;
this.popoverHeight = 300;
@ -67,7 +70,7 @@ export default class Stickerpicker extends React.Component {
}
this.setState({showStickers: false});
Widgets.removeStickerpickerWidgets().then(() => {
WidgetUtils.removeStickerpickerWidgets().then(() => {
this.forceUpdate();
}).catch((e) => {
console.error('Failed to remove sticker picker widget', e);
@ -119,7 +122,7 @@ export default class Stickerpicker extends React.Component {
}
_updateWidget() {
const stickerpickerWidget = Widgets.getStickerpickerWidgets()[0];
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
this.setState({
stickerpickerWidget,
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,
@ -148,8 +151,8 @@ export default class Stickerpicker extends React.Component {
<AccessibleButton onClick={this._launchManageIntegrations}
className='mx_Stickers_contentPlaceholder'>
<p>{ _t("You don't currently have any stickerpacks enabled") }</p>
<p className='mx_Stickers_addLink'>Add some now</p>
<img src='img/stickerpack-placeholder.png' alt={_t('Add a stickerpack')} />
<p className='mx_Stickers_addLink'>{ _t("Add some now") }</p>
<img src='img/stickerpack-placeholder.png' alt="" />
</AccessibleButton>
);
}
@ -162,17 +165,11 @@ export default class Stickerpicker extends React.Component {
);
}
_collectWidgetMessaging(widgetMessaging) {
this._appWidgetMessaging = widgetMessaging;
// Do this now instead of in componentDidMount because we might not have had the
// reference to widgetMessaging when mounting
this._sendVisibilityToWidget(true);
}
_sendVisibilityToWidget(visible) {
if (this._appWidgetMessaging && visible !== this._prevSentVisibility) {
this._appWidgetMessaging.sendVisibility(visible);
if (!this.state.stickerpickerWidget) return;
const widgetMessaging = ActiveWidgetStore.getWidgetMessaging(this.state.stickerpickerWidget.id);
if (widgetMessaging && visible !== this._prevSentVisibility) {
widgetMessaging.sendVisibility(visible);
this._prevSentVisibility = visible;
}
}
@ -211,9 +208,8 @@ export default class Stickerpicker extends React.Component {
width: this.popoverWidth,
}}
>
<PersistedElement>
<PersistedElement persistKey="stickerPicker" style={{zIndex: STICKERPICKER_Z_INDEX}}>
<AppTile
collectWidgetMessaging={this._collectWidgetMessaging}
id={stickerpickerWidget.id}
url={stickerpickerWidget.content.url}
name={stickerpickerWidget.content.name}
@ -348,7 +344,7 @@ export default class Stickerpicker extends React.Component {
if (this.state.showStickers) {
// Show hide-stickers button
stickersButton =
<div
<AccessibleButton
id='stickersButton'
key="controls_hide_stickers"
className="mx_MessageComposer_stickers mx_Stickers_hideStickers"
@ -356,18 +352,18 @@ export default class Stickerpicker extends React.Component {
ref='target'
title={_t("Hide Stickers")}>
<TintableSvg src="img/icons-hide-stickers.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
} else {
// Show show-stickers button
stickersButton =
<div
<AccessibleButton
id='stickersButton'
key="constrols_show_stickers"
key="controls_show_stickers"
className="mx_MessageComposer_stickers"
onClick={this._onShowStickersClick}
title={_t("Show Stickers")}>
<TintableSvg src="img/icons-show-stickers.svg" width="35" height="35" />
</div>;
</AccessibleButton>;
}
return <div>
{stickersButton}