Merge branch 'develop' into departify

This commit is contained in:
Stefan Parviainen 2017-10-27 19:23:13 +02:00
commit 95b2392104
17 changed files with 202 additions and 60 deletions

View file

@ -21,6 +21,8 @@ import Modal from './Modal';
import { getAddressType } from './UserAddress';
import createRoom from './createRoom';
import sdk from './';
import dis from './dispatcher';
import DMRoomMap from './utils/DMRoomMap';
import { _t } from './languageHandler';
export function inviteToRoom(roomId, addr) {
@ -79,15 +81,40 @@ function _onStartChatFinished(shouldInvite, addrs) {
const addrTexts = addrs.map((addr) => addr.address);
if (_isDmChat(addrTexts)) {
// Start a new DM chat
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
console.error(err.stack);
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
const rooms = _getDirectMessageRooms(addrTexts[0]);
if (rooms.length > 0) {
// A Direct Message room already exists for this user, so select a
// room from a list that is similar to the one in MemberInfo panel
const ChatCreateOrReuseDialog = sdk.getComponent(
"views.dialogs.ChatCreateOrReuseDialog",
);
const close = Modal.createTrackedDialog('Create or Reuse', '', ChatCreateOrReuseDialog, {
userId: addrTexts[0],
onNewDMClick: () => {
dis.dispatch({
action: 'start_chat',
user_id: addrTexts[0],
});
close(true);
},
onExistingRoomSelected: (roomId) => {
dis.dispatch({
action: 'view_room',
room_id: roomId,
});
close(true);
},
}).close;
} else {
// Start a new DM chat
createRoom({dmUserId: addrTexts[0]}).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to invite user', '', ErrorDialog, {
title: _t("Failed to invite user"),
description: ((err && err.message) ? err.message : _t("Operation failed")),
});
});
});
}
} else {
// Start multi user chat
let room;
@ -153,3 +180,19 @@ function _showAnyInviteErrors(addrs, room) {
return addrs;
}
function _getDirectMessageRooms(addr) {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
const dmRooms = dmRoomMap.getDMRoomsForUserId(addr);
const rooms = [];
dmRooms.forEach((dmRoom) => {
const room = MatrixClientPeg.get().getRoom(dmRoom);
if (room) {
const me = room.getMember(MatrixClientPeg.get().credentials.userId);
if (me.membership == 'join') {
rooms.push(room);
}
}
});
return rooms;
}

View file

@ -447,7 +447,7 @@ export default React.createClass({
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.on('update', () => {
this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary();
if (summary.profile) {
// Default profile fields should be "" for later sending to the server (which
@ -464,7 +464,6 @@ export default React.createClass({
});
});
this._groupStore.on('error', (err) => {
console.error(err);
this.setState({
summary: null,
error: err,
@ -964,13 +963,15 @@ export default React.createClass({
</AccessibleButton>,
);
} else {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Community Settings")} key="_editButton"
>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
</AccessibleButton>,
);
if (summary.user && summary.user.membership === 'join') {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"
onClick={this._onEditClick} title={_t("Community Settings")} key="_editButton"
>
<TintableSvg src="img/icons-settings-room.svg" width="16" height="16" />
</AccessibleButton>,
);
}
if (this.props.collapsedRhs) {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button"

View file

@ -118,6 +118,10 @@ const SETTINGS_LABELS = [
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
},
{
id: 'VideoView.flipVideoHorizontally',
label: _td('Mirror local video feed'),
},
/*
{
id: 'useFixedWidthFont',
@ -1328,8 +1332,11 @@ module.exports = React.createClass({
<div className="mx_UserSettings_avatarPicker">
<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")} />
<img src="img/cancel.svg"
width="15" height="15"
className="mx_filterFlipColor"
alt={_t("Remove avatar")}
title={_t("Remove avatar")} />
</div>
<div onClick={this.onAvatarPickerClick} className="mx_UserSettings_avatarPicker_imgContainer">
<ChangeAvatar ref="changeAvatar" initialAvatarUrl={avatarUrl}

View file

@ -302,7 +302,7 @@ module.exports = React.createClass({
} : {};
return this._matrixClient.register(
this.state.formVals.username,
this.state.formVals.username.toLowerCase(),
this.state.formVals.password,
undefined, // session id: included in the auth dict already
auth,

View file

@ -174,7 +174,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo">
<GeminiScrollbar autoshow={true}>
<AccessibleButton className="mx_MemberInfo_cancel"onClick={this._onCancel}>
<img src="img/cancel.svg" width="18" height="18" />
<img src="img/cancel.svg" width="18" height="18" className="mx_filterFlipColor" />
</AccessibleButton>
<div className="mx_MemberInfo_avatar">
{ avatar }

View file

@ -50,12 +50,9 @@ export default withMatrixClient(React.createClass({
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => {
this._groupStore.registerListener(() => {
this._fetchMembers();
});
this._groupStore.on('error', (err) => {
console.error(err);
});
},
_fetchMembers: function() {

View file

@ -47,16 +47,14 @@ export default React.createClass({
_initGroupStore: function(groupId) {
this._groupStore = GroupStoreCache.getGroupStore(this.context.matrixClient, groupId);
this._groupStore.on('update', () => {
this._groupStore.registerListener(() => {
this._fetchRooms();
});
this._groupStore.on('error', (err) => {
console.error('Error in group store (listened to by GroupRoomList)', err);
this.setState({
rooms: null,
});
});
this._fetchRooms();
},
_fetchRooms: function() {

View file

@ -120,8 +120,11 @@ const GroupRoomTile = React.createClass({
<div className="mx_GroupRoomTile_name">
{ this.state.name }
</div>
<AccessibleButton className="mx_GroupRoomTile_delete" onClick={this.onDeleteClick}>
<img src="img/cancel-small.svg" />
<AccessibleButton className="mx_GroupRoomTile_delete"
onClick={this.onDeleteClick}
tooltip={_t("Remove this room from the community")}
>
<img src="img/cancel.svg" width="15" height="15" className="mx_filterFlipColor" />
</AccessibleButton>
</AccessibleButton>
);

View file

@ -25,7 +25,10 @@ module.exports = React.createClass({
render: function() {
let tooltip = _t("Removed or unknown message type");
if (this.props.mxEvent.isRedacted()) {
tooltip = _t("Message removed by %(userId)s", {userId: this.props.mxEvent.getSender()});
const redactedBecauseUserId = this.props.mxEvent.getUnsigned().redacted_because.sender;
tooltip = redactedBecauseUserId ?
_t("Message removed by %(userId)s", { userId: redactedBecauseUserId }) :
_t("Message removed");
}
const text = this.props.mxEvent.getContent().body;

View file

@ -133,8 +133,9 @@ module.exports = React.createClass({
{ p["og:description"] }
</div>
</div>
<img className="mx_LinkPreviewWidget_cancel" src="img/cancel.svg" width="18" height="18"
onClick={this.props.onCancelClick} />
<img className="mx_LinkPreviewWidget_cancel mx_filterFlipColor"
src="img/cancel.svg" width="18" height="18"
onClick={this.props.onCancelClick} />
</div>
);
},

View file

@ -39,6 +39,7 @@ import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import GeminiScrollbar from 'react-gemini-scrollbar';
import RoomViewStore from '../../../stores/RoomViewStore';
module.exports = withMatrixClient(React.createClass({
@ -81,6 +82,7 @@ module.exports = withMatrixClient(React.createClass({
cli.on("Room.receipt", this.onRoomReceipt);
cli.on("RoomState.events", this.onRoomStateEvents);
cli.on("RoomMember.name", this.onRoomMemberName);
cli.on("RoomMember.membership", this.onRoomMemberMembership);
cli.on("accountData", this.onAccountData);
this._checkIgnoreState();
@ -107,6 +109,7 @@ module.exports = withMatrixClient(React.createClass({
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("RoomState.events", this.onRoomStateEvents);
client.removeListener("RoomMember.name", this.onRoomMemberName);
client.removeListener("RoomMember.membership", this.onRoomMemberMembership);
client.removeListener("accountData", this.onAccountData);
}
if (this._cancelDeviceList) {
@ -186,6 +189,10 @@ module.exports = withMatrixClient(React.createClass({
this.forceUpdate();
},
onRoomMemberMembership: function(ev, member) {
if (this.props.member.userId === member.userId) this.forceUpdate();
},
onAccountData: function(ev) {
if (ev.getType() === 'm.direct') {
this.forceUpdate();
@ -615,6 +622,8 @@ module.exports = withMatrixClient(React.createClass({
const member = this.props.member;
let ignoreButton = null;
let insertPillButton = null;
let inviteUserButton = null;
let readReceiptButton = null;
// Only allow the user to ignore the user if its not ourselves
@ -639,22 +648,58 @@ module.exports = withMatrixClient(React.createClass({
});
};
const onInsertPillButton = function() {
dis.dispatch({
action: 'insert_mention',
user_id: member.userId,
});
};
readReceiptButton = (
<AccessibleButton onClick={onReadReceiptButton} className="mx_MemberInfo_field">
{ _t('Jump to read receipt') }
</AccessibleButton>
);
insertPillButton = (
<AccessibleButton onClick={onInsertPillButton} className={"mx_MemberInfo_field"}>
{ _t('Mention') }
</AccessibleButton>
);
}
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async () => {
try {
await cli.invite(roomId, member.userId);
} catch (err) {
const ErrorDialog = sdk.getComponent('dialogs.ErrorDialog');
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t('Failed to invite'),
description: ((err && err.message) ? err.message : "Operation failed"),
});
}
};
inviteUserButton = (
<AccessibleButton onClick={onInviteUserButton} className="mx_MemberInfo_field">
{ _t('Invite') }
</AccessibleButton>
);
}
}
if (!ignoreButton && !readReceiptButton) return null;
if (!ignoreButton && !readReceiptButton && !insertPillButton && !inviteUserButton) return null;
return (
<div>
<h3>{ _t("User Options") }</h3>
<div className="mx_MemberInfo_buttons">
{ readReceiptButton }
{ insertPillButton }
{ ignoreButton }
{ inviteUserButton }
</div>
</div>
);
@ -760,9 +805,6 @@ module.exports = withMatrixClient(React.createClass({
</AccessibleButton>;
}
// TODO: we should have an invite button if this MemberInfo is showing a user who isn't actually in the current room yet
// e.g. clicking on a linkified userid in a room
let adminTools;
if (kickButton || banButton || muteButton || giveModButton) {
adminTools =
@ -790,9 +832,29 @@ module.exports = withMatrixClient(React.createClass({
presenceCurrentlyActive = this.props.member.user.currentlyActive;
}
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
roomMemberDetails = <div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b>
<PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)}
disabled={!this.state.can.modifyLevel}
onChange={this.onPowerChange} />
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
</div>
</div>;
}
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
const EmojiText = sdk.getComponent('elements.EmojiText');
return (
<div className="mx_MemberInfo">
@ -808,16 +870,7 @@ module.exports = withMatrixClient(React.createClass({
<div className="mx_MemberInfo_profileField">
{ this.props.member.userId }
</div>
<div className="mx_MemberInfo_profileField">
{ _t("Level:") } <b>
<PowerSelector controlled={true} value={parseInt(this.props.member.powerLevel)} disabled={!this.state.can.modifyLevel} onChange={this.onPowerChange} />
</b>
</div>
<div className="mx_MemberInfo_profileField">
<PresenceLabel activeAgo={presenceLastActiveAgo}
currentlyActive={presenceCurrentlyActive}
presenceState={presenceState} />
</div>
{ roomMemberDetails }
</div>
{ this._renderUserOptions() }

View file

@ -95,7 +95,9 @@ module.exports = React.createClass({
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>
<AccessibleButton className="mx_PinnedEventsPanel_cancel" onClick={this.props.onCancelClick}>
<img className="mx_filterFlipColor" src="img/cancel.svg" width="18" height="18" />
</AccessibleButton>
<h3 className="mx_PinnedEventsPanel_header">{ _t("Pinned Messages") }</h3>
{ tiles }
</div>

View file

@ -281,8 +281,11 @@ module.exports = React.createClass({
<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")} />
<img src="img/cancel.svg"
className="mx_filterFlipColor"
width="10"
alt={_t("Remove avatar")}
title={_t("Remove avatar")} />
</div>
</div>
);

View file

@ -18,10 +18,13 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({
displayName: 'VideoView',
@ -108,14 +111,18 @@ module.exports = React.createClass({
document.mozFullScreenElement ||
document.webkitFullscreenElement);
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
{ "mx_VideoView_localVideoFeed_flipped":
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false),
},
);
return (
<div className="mx_VideoView" ref={this.setContainer} onClick={this.props.onClick}>
<div className="mx_VideoView_remoteVideoFeed">
<VideoFeed ref="remote" onResize={this.props.onResize}
maxHeight={maxVideoHeight} />
</div>
<div className="mx_VideoView_localVideoFeed">
<div className={localVideoFeedClasses}>
<VideoFeed ref="local" />
</div>
</div>

View file

@ -152,6 +152,7 @@
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Communities": "Communities",
"Message Pinning": "Message Pinning",
"Mention": "Mention",
"%(displayName)s is typing": "%(displayName)s is typing",
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
@ -200,8 +201,6 @@
"Authentication": "Authentication",
"Failed to delete device": "Failed to delete device",
"Delete": "Delete",
"Delete Widget": "Delete Widget",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications",
"Cannot add any more widgets": "Cannot add any more widgets",
@ -245,6 +244,7 @@
"Unignore": "Unignore",
"Ignore": "Ignore",
"Jump to read receipt": "Jump to read receipt",
"Invite": "Invite",
"User Options": "User Options",
"Direct chats": "Direct chats",
"Unmute": "Unmute",
@ -456,6 +456,7 @@
"You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?": "You are about to be taken to a third-party site so you can authenticate your account for use with %(integrationsUrl)s. Do you wish to continue?",
"Removed or unknown message type": "Removed or unknown message type",
"Message removed by %(userId)s": "Message removed by %(userId)s",
"Message removed": "Message removed",
"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>",
"This Home Server would like to make sure you are not a robot": "This Home Server would like to make sure you are not a robot",
"Sign in with CAS": "Sign in with CAS",
@ -500,10 +501,13 @@
"Are you sure you want to remove '%(roomName)s' from %(groupId)s?": "Are you sure you want to remove '%(roomName)s' from %(groupId)s?",
"Removing a room from the community will also remove it from the community page.": "Removing a room from the community will also remove it from the community page.",
"Remove": "Remove",
"Remove this room from the community": "Remove this room from the community",
"Unknown Address": "Unknown Address",
"NOTE: Apps are not end-to-end encrypted": "NOTE: Apps are not end-to-end encrypted",
"Do you want to load widget from URL:": "Do you want to load widget from URL:",
"Allow": "Allow",
"Delete Widget": "Delete Widget",
"Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?": "Deleting a widget removes it for all users in this room. Are you sure you want to delete this widget?",
"Delete widget": "Delete widget",
"Revoke widget access": "Revoke widget access",
"Edit": "Edit",
@ -688,8 +692,8 @@
"Featured Rooms:": "Featured Rooms:",
"Featured Users:": "Featured Users:",
"%(inviter)s has invited you to join this community": "%(inviter)s has invited you to join this community",
"You are a member of this community": "You are a member of this community",
"You are an administrator of this community": "You are an administrator of this community",
"You are a member of this community": "You are a member of this community",
"Community Member Settings": "Community Member Settings",
"Publish this community on your profile": "Publish this community on your profile",
"Long Description (HTML)": "Long Description (HTML)",
@ -759,6 +763,7 @@
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
"Hide avatars in user and room mentions": "Hide avatars in user and room mentions",
"Disable big emoji in chat": "Disable big emoji in chat",
"Mirror local video feed": "Mirror local video feed",
"Opt out of analytics": "Opt out of analytics",
"Disable Peer-to-Peer for 1:1 calls": "Disable Peer-to-Peer for 1:1 calls",
"Never send encrypted messages to unverified devices from this device": "Never send encrypted messages to unverified devices from this device",

View file

@ -29,9 +29,10 @@ export default class GroupStore extends EventEmitter {
this._matrixClient = matrixClient;
this._summary = {};
this._rooms = [];
this._fetchSummary();
this._fetchRooms();
this._fetchMembers();
this.on('error', (err) => {
console.error(`GroupStore for ${this.groupId} encountered error`, err);
});
}
_fetchMembers() {
@ -51,6 +52,10 @@ export default class GroupStore extends EventEmitter {
});
this._notifyListeners();
}).catch((err) => {
// Invited users not visible to non-members
if (err.httpStatus === 403) {
return;
}
console.error("Failed to get group invited member list: " + err);
this.emit('error', err);
});
@ -80,6 +85,17 @@ export default class GroupStore extends EventEmitter {
this.emit('update');
}
registerListener(fn) {
this.on('update', fn);
this._fetchSummary();
this._fetchRooms();
this._fetchMembers();
}
unregisterListener(fn) {
this.removeListener('update', fn);
}
getSummary() {
return this._summary;
}

View file

@ -88,6 +88,9 @@ describe('MemberEventListSummary', function() {
sandbox = testUtils.stubClient();
languageHandler.setLanguage('en').done(done);
languageHandler.setMissingEntryGenerator(function(key) {
return key.split('|', 2)[1];
});
});
afterEach(function() {