Fix merge conflict

pull/21833/head
Stefan Parviainen 2017-09-27 08:41:37 +02:00
commit 35cb52febc
17 changed files with 713 additions and 165 deletions

View File

@ -123,7 +123,6 @@ src/Roles.js
src/Rooms.js
src/ScalarAuthClient.js
src/ScalarMessaging.js
src/TextForEvent.js
src/Tinter.js
src/UiEffects.js
src/Unread.js

View File

@ -107,6 +107,9 @@ export default class BasePlatform {
isElectron(): boolean { return false; }
setupScreenSharingForIframe() {
}
/**
* Restarts the application, without neccessarily reloading
* any application code

View File

@ -18,11 +18,12 @@ import Modal from './Modal';
import sdk from './';
import MultiInviter from './utils/MultiInviter';
import { _t } from './languageHandler';
import MatrixClientPeg from './MatrixClientPeg';
export function showGroupInviteDialog(groupId) {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Group Invite', '', AddressPickerDialog, {
title: _t('Invite new group members'),
title: _t("Invite new group members"),
description: _t("Who would you like to add to this group?"),
placeholder: _t("Name or matrix ID"),
button: _t("Invite to Group"),
@ -35,6 +36,25 @@ export function showGroupInviteDialog(groupId) {
});
}
export function showGroupAddRoomDialog(groupId) {
return new Promise((resolve, reject) => {
const AddressPickerDialog = sdk.getComponent("dialogs.AddressPickerDialog");
Modal.createTrackedDialog('Add Rooms to Group', '', AddressPickerDialog, {
title: _t("Add rooms to the group"),
description: _t("Which rooms would you like to add to this group?"),
placeholder: _t("Room name or alias"),
button: _t("Add to group"),
pickerType: 'room',
validAddressTypes: ['mx'],
onFinished: (success, addrs) => {
if (!success) return;
_onGroupAddRoomFinished(groupId, addrs).then(resolve, reject);
},
});
});
}
function _onGroupInviteFinished(groupId, addrs) {
const multiInviter = new MultiInviter(groupId);
@ -65,3 +85,27 @@ function _onGroupInviteFinished(groupId, addrs) {
});
}
function _onGroupAddRoomFinished(groupId, addrs) {
const errorList = [];
return Promise.all(addrs.map((addr) => {
return MatrixClientPeg.get()
.addRoomToGroup(groupId, addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
if (errorList.length === 0) {
return;
}
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog(
'Failed to add the following room to the group',
'', ErrorDialog,
{
title: _t(
"Failed to add the following rooms to %(groupId)s:",
{groupId},
),
description: errorList.join(", "),
});
});
}

View File

@ -13,56 +13,67 @@ 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 MatrixClientPeg from "./MatrixClientPeg";
import CallHandler from "./CallHandler";
import MatrixClientPeg from './MatrixClientPeg';
import CallHandler from './CallHandler';
import { _t } from './languageHandler';
import * as Roles from './Roles';
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
var senderName = ev.sender ? ev.sender.name : ev.getSender();
var targetName = ev.target ? ev.target.name : ev.getStateKey();
var ConferenceHandler = CallHandler.getConferenceHandler();
var reason = ev.getContent().reason ? (
_t('Reason') + ': ' + ev.getContent().reason
) : "";
switch (ev.getContent().membership) {
case 'invite':
var threePidContent = ev.getContent().third_party_invite;
const senderName = ev.sender ? ev.sender.name : ev.getSender();
const targetName = ev.target ? ev.target.name : ev.getStateKey();
const prevContent = ev.getPrevContent();
const content = ev.getContent();
const ConferenceHandler = CallHandler.getConferenceHandler();
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) {
case 'invite': {
const threePidContent = content.third_party_invite;
if (threePidContent) {
if (threePidContent.display_name) {
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {targetName: targetName, displayName: threePidContent.display_name});
return _t('%(targetName)s accepted the invitation for %(displayName)s.', {
targetName,
displayName: threePidContent.display_name,
});
} else {
return _t('%(targetName)s accepted an invitation.', {targetName: targetName});
return _t('%(targetName)s accepted an invitation.', {targetName});
}
}
else {
} else {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return _t('%(senderName)s requested a VoIP conference.', {senderName: senderName});
}
else {
return _t('%(senderName)s invited %(targetName)s.', {senderName: senderName, targetName: targetName});
return _t('%(senderName)s requested a VoIP conference.', {senderName});
} else {
return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
}
}
}
case 'ban':
return _t(
'%(senderName)s banned %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
return _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + reason;
case 'join':
if (ev.getPrevContent() && ev.getPrevContent().membership == 'join') {
if (ev.getPrevContent().displayname && ev.getContent().displayname && ev.getPrevContent().displayname != ev.getContent().displayname) {
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname, displayName: ev.getContent().displayname});
} else if (!ev.getPrevContent().displayname && ev.getContent().displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', {senderName: ev.getSender(), displayName: ev.getContent().displayname});
} else if (ev.getPrevContent().displayname && !ev.getContent().displayname) {
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {senderName: ev.getSender(), oldDisplayName: ev.getPrevContent().displayname});
} else if (ev.getPrevContent().avatar_url && !ev.getContent().avatar_url) {
return _t('%(senderName)s removed their profile picture.', {senderName: senderName});
} else if (ev.getPrevContent().avatar_url && ev.getContent().avatar_url && ev.getPrevContent().avatar_url != ev.getContent().avatar_url) {
return _t('%(senderName)s changed their profile picture.', {senderName: senderName});
} else if (!ev.getPrevContent().avatar_url && ev.getContent().avatar_url) {
return _t('%(senderName)s set a profile picture.', {senderName: senderName});
if (prevContent && prevContent.membership === 'join') {
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
return _t('%(senderName)s changed their display name from %(oldDisplayName)s to %(displayName)s.', {
senderName,
oldDisplayName: prevContent.displayname,
displayName: content.displayname,
});
} else if (!prevContent.displayname && content.displayname) {
return _t('%(senderName)s set their display name to %(displayName)s.', {
senderName,
displayName: content.displayname,
});
} else if (prevContent.displayname && !content.displayname) {
return _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
senderName,
oldDisplayName: prevContent.displayname,
});
} else if (prevContent.avatar_url && !content.avatar_url) {
return _t('%(senderName)s removed their profile picture.', {senderName});
} else if (prevContent.avatar_url && content.avatar_url &&
prevContent.avatar_url !== content.avatar_url) {
return _t('%(senderName)s changed their profile picture.', {senderName});
} else if (!prevContent.avatar_url && content.avatar_url) {
return _t('%(senderName)s set a profile picture.', {senderName});
} else {
// suppress null rejoins
return '';
@ -71,73 +82,69 @@ function textForMemberEvent(ev) {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return _t('VoIP conference started.');
}
else {
return _t('%(targetName)s joined the room.', {targetName: targetName});
} else {
return _t('%(targetName)s joined the room.', {targetName});
}
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
return _t('VoIP conference finished.');
} else if (prevContent.membership === "invite") {
return _t('%(targetName)s rejected the invitation.', {targetName});
} else {
return _t('%(targetName)s left the room.', {targetName});
}
else if (ev.getPrevContent().membership === "invite") {
return _t('%(targetName)s rejected the invitation.', {targetName: targetName});
}
else {
return _t('%(targetName)s left the room.', {targetName: targetName});
}
}
else if (ev.getPrevContent().membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName: senderName, targetName: targetName});
}
else if (ev.getPrevContent().membership === "join") {
return _t(
'%(senderName)s kicked %(targetName)s.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
}
else if (ev.getPrevContent().membership === "invite") {
return _t(
'%(senderName)s withdrew %(targetName)s\'s invitation.',
{senderName: senderName, targetName: targetName}
) + ' ' + reason;
}
else {
return _t('%(targetName)s left the room.', {targetName: targetName});
} else if (prevContent.membership === "ban") {
return _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
} else if (prevContent.membership === "join") {
return _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + reason;
} else if (prevContent.membership === "invite") {
return _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
senderName,
targetName,
}) + ' ' + reason;
} else {
return _t('%(targetName)s left the room.', {targetName});
}
}
}
function textForTopicEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {senderDisplayName: senderDisplayName, topic: ev.getContent().topic});
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
return _t('%(senderDisplayName)s changed the topic to "%(topic)s".', {
senderDisplayName,
topic: ev.getContent().topic,
});
}
function textForRoomNameEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName: senderDisplayName});
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
}
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {senderDisplayName: senderDisplayName, roomName: ev.getContent().name});
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
senderDisplayName,
roomName: ev.getContent().name,
});
}
function textForMessageEvent(ev) {
var senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
var message = senderDisplayName + ': ' + ev.getContent().body;
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
let message = senderDisplayName + ': ' + ev.getContent().body;
if (ev.getContent().msgtype === "m.emote") {
message = "* " + senderDisplayName + " " + message;
} else if (ev.getContent().msgtype === "m.image") {
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName: senderDisplayName});
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
}
return message;
}
function textForCallAnswerEvent(event) {
var senderName = event.sender ? event.sender.name : _t('Someone');
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName: senderName}) + ' ' + supported;
const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
}
function textForCallHangupEvent(event) {
@ -159,20 +166,23 @@ function textForCallHangupEvent(event) {
}
function textForCallInviteEvent(event) {
var senderName = event.sender ? event.sender.name : _t('Someone');
const senderName = event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
var type = "voice";
let callType = "voice";
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
type = "video";
callType = "video";
}
var supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName: senderName, callType: type}) + ' ' + supported;
const supported = MatrixClientPeg.get().supportsVoip() ? "" : _t('(not supported by this browser)');
return _t('%(senderName)s placed a %(callType)s call.', {senderName, callType}) + ' ' + supported;
}
function textForThreePidInviteEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender();
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {senderName: senderName, targetDisplayName: event.getContent().display_name});
const senderName = event.sender ? event.sender.name : event.getSender();
return _t('%(senderName)s sent an invitation to %(targetDisplayName)s to join the room.', {
senderName,
targetDisplayName: event.getContent().display_name,
});
}
function textForHistoryVisibilityEvent(event) {
@ -197,8 +207,11 @@ function textForHistoryVisibilityEvent(event) {
}
function textForEncryptionEvent(event) {
var senderName = event.sender ? event.sender.name : event.getSender();
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {senderName: senderName, algorithm: event.getContent().algorithm});
const senderName = event.sender ? event.sender.name : event.getSender();
return _t('%(senderName)s turned on end-to-end encryption (algorithm %(algorithm)s).', {
senderName,
algorithm: event.getContent().algorithm,
});
}
// Currently will only display a change if a user's power level is changed
@ -209,18 +222,18 @@ function textForPowerEvent(event) {
}
const userDefault = event.getContent().users_default || 0;
// Construct set of userIds
let users = [];
const users = [];
Object.keys(event.getContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
},
);
Object.keys(event.getPrevContent().users).forEach(
(userId) => {
if (users.indexOf(userId) === -1) users.push(userId);
}
},
);
let diff = [];
const diff = [];
// XXX: This is also surely broken for i18n
users.forEach((userId) => {
// Previous power level
@ -229,11 +242,11 @@ function textForPowerEvent(event) {
const to = event.getContent().users[userId];
if (to !== from) {
diff.push(
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
_t('%(userId)s from %(fromPowerLevel)s to %(toPowerLevel)s', {
userId: userId,
fromPowerLevel: Roles.textualPowerLevel(from, userDefault),
toPowerLevel: Roles.textualPowerLevel(to, userDefault)
})
toPowerLevel: Roles.textualPowerLevel(to, userDefault),
}),
);
}
});
@ -276,14 +289,14 @@ function textForWidgetEvent(event) {
}
}
var handlers = {
const handlers = {
'm.room.message': textForMessageEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent,
'm.room.name': textForRoomNameEvent,
'm.room.topic': textForTopicEvent,
'm.room.member': textForMemberEvent,
'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent,
'm.room.third_party_invite': textForThreePidInviteEvent,
'm.room.history_visibility': textForHistoryVisibilityEvent,
'm.room.encryption': textForEncryptionEvent,
@ -294,8 +307,8 @@ var handlers = {
module.exports = {
textForEvent: function(ev) {
var hdlr = handlers[ev.getType()];
if (!hdlr) return "";
const hdlr = handlers[ev.getType()];
if (!hdlr) return '';
return hdlr(ev);
}
},
};

View File

@ -27,6 +27,8 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
import GroupSummaryStore from '../../stores/GroupSummaryStore';
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
@ -76,8 +78,8 @@ const CategoryRoomList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return MatrixClientPeg.get()
.addRoomToGroupSummary(this.props.groupId, addr.address)
return this.context.groupSummaryStore
.addRoomToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
@ -153,8 +155,7 @@ const FeaturedRoom = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
MatrixClientPeg.get().removeRoomFromGroupSummary(
this.props.groupId,
this.context.groupSummaryStore.removeRoomFromGroupSummary(
this.props.summaryInfo.room_id,
).catch((err) => {
console.error('Error whilst removing room from group summary', err);
@ -242,8 +243,8 @@ const RoleUserList = React.createClass({
if (!success) return;
const errorList = [];
Promise.all(addrs.map((addr) => {
return MatrixClientPeg.get()
.addUserToGroupSummary(this.props.groupId, addr.address)
return this.context.groupSummaryStore
.addUserToGroupSummary(addr.address)
.catch(() => { errorList.push(addr.address); })
.reflect();
})).then(() => {
@ -317,8 +318,7 @@ const FeaturedUser = React.createClass({
onDeleteClicked: function(e) {
e.preventDefault();
e.stopPropagation();
MatrixClientPeg.get().removeUserFromGroupSummary(
this.props.groupId,
this.context.groupSummaryStore.removeUserFromGroupSummary(
this.props.summaryInfo.user_id,
).catch((err) => {
console.error('Error whilst removing user from group summary', err);
@ -364,6 +364,15 @@ const FeaturedUser = React.createClass({
},
});
const GroupSummaryContext = {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore).isRequired,
};
CategoryRoomList.contextTypes = GroupSummaryContext;
FeaturedRoom.contextTypes = GroupSummaryContext;
RoleUserList.contextTypes = GroupSummaryContext;
FeaturedUser.contextTypes = GroupSummaryContext;
export default React.createClass({
displayName: 'GroupView',
@ -371,6 +380,16 @@ export default React.createClass({
groupId: PropTypes.string.isRequired,
},
childContextTypes: {
groupSummaryStore: React.PropTypes.instanceOf(GroupSummaryStore),
},
getChildContext: function() {
return {
groupSummaryStore: this._groupSummaryStore,
};
},
getInitialState: function() {
return {
summary: null,
@ -379,18 +398,20 @@ export default React.createClass({
saving: false,
uploadingAvatar: false,
membershipBusy: false,
publicityBusy: false,
};
},
componentWillMount: function() {
this._changeAvatarComponent = null;
this._loadGroupFromServer(this.props.groupId);
this._initGroupSummaryStore(this.props.groupId);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
componentWillUnmount: function() {
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
this._groupSummaryStore.removeAllListeners();
},
componentWillReceiveProps: function(newProps) {
@ -399,7 +420,7 @@ export default React.createClass({
summary: null,
error: null,
}, () => {
this._loadGroupFromServer(newProps.groupId);
this._initGroupSummaryStore(newProps.groupId);
});
}
},
@ -410,13 +431,17 @@ export default React.createClass({
this.setState({membershipBusy: false});
},
_loadGroupFromServer: function(groupId) {
MatrixClientPeg.get().getGroupSummary(groupId).done((res) => {
_initGroupSummaryStore: function(groupId) {
this._groupSummaryStore = new GroupSummaryStore(
MatrixClientPeg.get(), this.props.groupId,
);
this._groupSummaryStore.on('update', () => {
this.setState({
summary: res,
summary: this._groupSummaryStore.getSummary(),
error: null,
});
}, (err) => {
});
this._groupSummaryStore.on('error', (err) => {
this.setState({
summary: null,
error: err,
@ -493,7 +518,7 @@ export default React.createClass({
editing: false,
summary: null,
});
this._loadGroupFromServer(this.props.groupId);
this._initGroupSummaryStore(this.props.groupId);
}).catch((e) => {
this.setState({
saving: false,
@ -560,7 +585,26 @@ export default React.createClass({
});
},
_getFeaturedRoomsNode() {
_onPubliciseOffClick: function() {
this._setPublicity(false);
},
_onPubliciseOnClick: function() {
this._setPublicity(true);
},
_setPublicity: function(publicity) {
this.setState({
publicityBusy: true,
});
this._groupSummaryStore.setGroupPublicity(publicity).then(() => {
this.setState({
publicityBusy: false,
});
});
},
_getFeaturedRoomsNode: function() {
const summary = this.state.summary;
const defaultCategoryRooms = [];
@ -601,7 +645,7 @@ export default React.createClass({
</div>;
},
_getFeaturedUsersNode() {
_getFeaturedUsersNode: function() {
const summary = this.state.summary;
const noRoleUsers = [];
@ -643,12 +687,12 @@ export default React.createClass({
},
_getMembershipSection: function() {
const Spinner = sdk.getComponent("elements.Spinner");
const group = MatrixClientPeg.get().getGroup(this.props.groupId);
if (!group) return null;
if (group.myMembership === 'invite') {
const Spinner = sdk.getComponent("elements.Spinner");
if (this.state.membershipBusy) {
return <div className="mx_GroupView_membershipSection">
<Spinner />
@ -677,17 +721,57 @@ export default React.createClass({
if (this.state.summary.user && this.state.summary.user.is_privileged) {
youAreAMemberText = _t("You are an administrator of this group");
}
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
<div className="mx_GroupView_membershipSection_description">
{youAreAMemberText}
</div>
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onLeaveClick}
let publicisedButton;
if (this.state.publicityBusy) {
publicisedButton = <Spinner />;
}
let publicisedSection;
if (this.state.summary.user && this.state.summary.user.is_publicised) {
if (!this.state.publicityBusy) {
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onPubliciseOffClick}
>
{_t("Unpublish")}
</AccessibleButton>;
}
publicisedSection = <div className="mx_GroupView_membershipSubSection">
{_t("This group is published on your profile")}
<div className="mx_GroupView_membership_buttonContainer">
{publicisedButton}
</div>
</div>;
} else {
if (!this.state.publicityBusy) {
publicisedButton = <AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onPubliciseOnClick}
>
{_t("Leave")}
</AccessibleButton>
{_t("Publish")}
</AccessibleButton>;
}
publicisedSection = <div className="mx_GroupView_membershipSubSection">
{_t("This group is not published on your profile")}
<div className="mx_GroupView_membership_buttonContainer">
{publicisedButton}
</div>
</div>;
}
return <div className="mx_GroupView_membershipSection mx_GroupView_membershipSection_joined">
<div className="mx_GroupView_membershipSubSection">
<div className="mx_GroupView_membershipSection_description">
{youAreAMemberText}
</div>
<div className="mx_GroupView_membership_buttonContainer">
<AccessibleButton className="mx_GroupView_textButton mx_RoomHeader_textButton"
onClick={this._onLeaveClick}
>
{_t("Leave")}
</AccessibleButton>
</div>
</div>
{publicisedSection}
</div>;
}

View File

@ -155,7 +155,7 @@ module.exports = React.createClass({
if (this.props.groupId) {
this._doNaiveGroupRoomSearch(query);
} else {
console.error('Room searching only implemented for groups');
this._doRoomSearch(query);
}
} else {
console.error('Unknown pickerType', this.props.pickerType);
@ -248,7 +248,7 @@ module.exports = React.createClass({
results.push({
room_id: r.room_id,
avatar_url: r.avatar_url,
name: r.name,
name: r.name || r.canonical_alias,
});
});
this._processResults(results, query);
@ -264,6 +264,37 @@ module.exports = React.createClass({
});
},
_doRoomSearch: function(query) {
const lowerCaseQuery = query.toLowerCase();
const rooms = MatrixClientPeg.get().getRooms();
const results = [];
rooms.forEach((room) => {
const nameEvent = room.currentState.getStateEvents('m.room.name', '');
const topicEvent = room.currentState.getStateEvents('m.room.topic', '');
const name = nameEvent ? nameEvent.getContent().name : '';
const canonicalAlias = room.getCanonicalAlias();
const topic = topicEvent ? topicEvent.getContent().topic : '';
const nameMatch = (name || '').toLowerCase().includes(lowerCaseQuery);
const aliasMatch = (canonicalAlias || '').toLowerCase().includes(lowerCaseQuery);
const topicMatch = (topic || '').toLowerCase().includes(lowerCaseQuery);
if (!(nameMatch || topicMatch || aliasMatch)) {
return;
}
const avatarEvent = room.currentState.getStateEvents('m.room.avatar', '');
const avatarUrl = avatarEvent ? avatarEvent.getContent().url : undefined;
results.push({
room_id: room.roomId,
avatar_url: avatarUrl,
name: name || canonicalAlias,
});
});
this._processResults(results, query);
this.setState({
busy: false,
});
},
_doUserDirectorySearch: function(query) {
this.setState({
busy: true,

View File

@ -19,6 +19,7 @@ limitations under the License.
import url from 'url';
import React from 'react';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import ScalarAuthClient from '../../../ScalarAuthClient';
import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal';
@ -127,6 +128,30 @@ export default React.createClass({
loading: false,
});
});
window.addEventListener('message', this._onMessage, false);
},
componentWillUnmount() {
window.removeEventListener('message', this._onMessage);
},
_onMessage(event) {
if (this.props.type !== 'jitsi') {
return;
}
if (!event.origin) {
event.origin = event.originalEvent.origin;
}
if (!this.state.widgetUrl.startsWith(event.origin)) {
return;
}
if (event.data.widgetAction === 'jitsi_iframe_loaded') {
const iframe = this.refs.appFrame.contentWindow
.document.querySelector('iframe[id^="jitsiConferenceFrame"]');
PlatformPeg.get().setupScreenSharingForIframe(iframe);
}
},
_canUserModify: function() {

View File

@ -29,6 +29,9 @@ const BULK_REQUEST_DEBOUNCE_MS = 200;
// If true, flair can function and we should keep sending requests for groups and avatars.
let groupSupport = true;
const USER_GROUPS_CACHE_BUST_MS = 1800000; // 30 mins
const GROUP_PROFILES_CACHE_BUST_MS = 1800000; // 30 mins
// TODO: Cache-busting based on time. (The server won't inform us of membership changes.)
// This applies to userGroups and groupProfiles. We can provide a slightly better UX by
// cache-busting when the current user joins/leaves a group.
@ -69,7 +72,9 @@ function getPublicisedGroupsCached(matrixClient, userId) {
usersPending[userId].reject = reject;
}).then((groups) => {
userGroups[userId] = groups;
// TODO: Reset cache at this point
setTimeout(() => {
delete userGroups[userId];
}, USER_GROUPS_CACHE_BUST_MS);
return userGroups[userId];
}).catch((err) => {
throw err;
@ -126,6 +131,9 @@ async function getGroupProfileCached(matrixClient, groupId) {
groupId,
avatarUrl: profile.avatar_url,
};
setTimeout(() => {
delete groupProfiles[groupId];
}, GROUP_PROFILES_CACHE_BUST_MS);
return groupProfiles[groupId];
}

View File

@ -152,12 +152,21 @@ module.exports = withMatrixClient(React.createClass({
</div>;
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatar = (
<BaseAvatar name={this.props.groupMember.userId} width={36} height={36} />
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
this.props.groupMember.avatarUrl,
36, 36, 'crop',
);
const groupMemberName = this.props.groupMember.userId;
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const avatar = (
<BaseAvatar name={this.props.groupMember.userId} width={36} height={36}
url={avatarUrl}
/>
);
const groupMemberName = (
this.props.groupMember.displayname || this.props.groupMember.userId
);
const EmojiText = sdk.getComponent('elements.EmojiText');
return (

View File

@ -131,7 +131,7 @@ export default withMatrixClient(React.createClass({
const inputBox = (
<form autoComplete="off">
<input className="mx_MemberList_query" id="mx_MemberList_query" type="text"
<input className="mx_GroupMemberList_query" id="mx_GroupMemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter group members') } />
</form>

View File

@ -48,11 +48,15 @@ export default withMatrixClient(React.createClass({
const EntityTile = sdk.getComponent('rooms.EntityTile');
const name = this.props.member.displayname || this.props.member.userId;
const avatarUrl = this.props.matrixClient.mxcUrlToHttp(
this.props.member.avatarUrl,
36, 36, 'crop',
);
const av = (
<BaseAvatar name={this.props.member.userId}
width={36} height={36}
url={this.props.matrixClient.mxcUrlToHttp(this.props.member.avatarUrl)}
url={avatarUrl}
/>
);

View File

@ -0,0 +1,143 @@
/*
Copyright 2017 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 { _t } from '../../../languageHandler';
import sdk from '../../../index';
import { groupRoomFromApiObject } from '../../../groups';
import GeminiScrollbar from 'react-gemini-scrollbar';
import PropTypes from 'prop-types';
import {MatrixClient} from 'matrix-js-sdk';
const INITIAL_LOAD_NUM_ROOMS = 30;
export default React.createClass({
contextTypes: {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
},
propTypes: {
groupId: PropTypes.string.isRequired,
},
getInitialState: function() {
return {
fetching: false,
rooms: null,
truncateAt: INITIAL_LOAD_NUM_ROOMS,
searchQuery: "",
};
},
componentWillMount: function() {
this._unmounted = false;
this._fetchRooms();
},
_fetchRooms: function() {
this.setState({fetching: true});
this.context.matrixClient.getGroupRooms(this.props.groupId).then((result) => {
this.setState({
rooms: result.chunk.map((apiRoom) => {
return groupRoomFromApiObject(apiRoom);
}),
fetching: false,
});
}).catch((e) => {
this.setState({fetching: false});
console.error("Failed to get group room list: ", e);
});
},
_createOverflowTile: function(overflowCount, totalCount) {
// For now we'll pretend this is any entity. It should probably be a separate tile.
const EntityTile = sdk.getComponent("rooms.EntityTile");
const BaseAvatar = sdk.getComponent("avatars.BaseAvatar");
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url="img/ellipsis.svg" name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={this._showFullRoomList} />
);
},
_showFullRoomList: function() {
this.setState({
truncateAt: -1,
});
},
onSearchQueryChanged: function(ev) {
this.setState({ searchQuery: ev.target.value });
},
makeGroupRoomTiles: function(query) {
const GroupRoomTile = sdk.getComponent("groups.GroupRoomTile");
query = (query || "").toLowerCase();
let roomList = this.state.rooms;
if (query) {
roomList = roomList.filter((room) => {
const matchesName = (room.name || "").toLowerCase().include(query);
const matchesAlias = (room.canonicalAlias || "").toLowerCase().includes(query);
return matchesName || matchesAlias;
});
}
roomList = roomList.map((groupRoom, index) => {
return (
<GroupRoomTile
key={index}
groupId={this.props.groupId}
groupRoom={groupRoom} />
);
});
return roomList;
},
render: function() {
if (this.state.fetching) {
const Spinner = sdk.getComponent("elements.Spinner");
return (<div className="mx_GroupRoomList">
<Spinner />
</div>);
} else if (this.state.rooms === null) {
return null;
}
const inputBox = (
<form autoComplete="off">
<input className="mx_GroupRoomList_query" id="mx_GroupRoomList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery}
placeholder={ _t('Filter group rooms') } />
</form>
);
const TruncatedList = sdk.getComponent("elements.TruncatedList");
return (
<div className="mx_GroupRoomList">
{ inputBox }
<GeminiScrollbar autoshow={true} className="mx_GroupRoomList_joined mx_GroupRoomList_outerWrapper">
<TruncatedList className="mx_GroupRoomList_wrapper" truncateAt={this.state.truncateAt}
createOverflowElement={this._createOverflowTile}>
{this.makeGroupRoomTiles(this.state.searchQuery)}
</TruncatedList>
</GeminiScrollbar>
</div>
);
},
});

View File

@ -0,0 +1,89 @@
/*
Copyright 2017 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 {MatrixClient} from 'matrix-js-sdk';
import { _t } from '../../../languageHandler';
import PropTypes from 'prop-types';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { GroupRoomType } from '../../../groups';
const GroupRoomTile = React.createClass({
displayName: 'GroupRoomTile',
propTypes: {
groupId: PropTypes.string.isRequired,
groupRoom: GroupRoomType.isRequired,
},
getInitialState: function() {
return {};
},
onClick: function(e) {
let roomId;
let roomAlias;
if (this.props.groupRoom.canonicalAlias) {
roomAlias = this.props.groupRoom.canonicalAlias;
} else {
roomId = this.props.groupRoom.roomId;
}
dis.dispatch({
action: 'view_room',
room_id: roomId,
room_alias: roomAlias,
});
},
render: function() {
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const name = this.props.groupRoom.name ||
this.props.groupRoom.canonicalAlias ||
_t("Unnamed Room");
const avatarUrl = this.context.matrixClient.mxcUrlToHttp(
this.props.groupRoom.avatarUrl,
36, 36, 'crop',
);
const av = (
<BaseAvatar name={name}
width={36} height={36}
url={avatarUrl}
/>
);
return (
<AccessibleButton className="mx_GroupRoomTile" onClick={this.onClick}>
<div className="mx_GroupRoomTile_avatar">
{av}
</div>
<div className="mx_GroupRoomTile_name">
{name}
</div>
</AccessibleButton>
);
},
});
GroupRoomTile.contextTypes = {
matrixClient: React.PropTypes.instanceOf(MatrixClient).isRequired,
};
export default GroupRoomTile;

View File

@ -18,12 +18,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import classNames from 'classnames';
import Matrix from 'matrix-js-sdk';
import Promise from 'bluebird';
var MatrixClientPeg = require("../../../MatrixClientPeg");
var Modal = require("../../../Modal");
var Entities = require("../../../Entities");
var sdk = require('../../../index');
var GeminiScrollbar = require('react-gemini-scrollbar');
var rate_limited_func = require('../../../ratelimitedfunc');
@ -31,30 +26,26 @@ var CallHandler = require("../../../CallHandler");
const INITIAL_LOAD_NUM_MEMBERS = 30;
const INITIAL_LOAD_NUM_INVITED = 5;
const SHOW_MORE_INCREMENT = 100;
module.exports = React.createClass({
displayName: 'MemberList',
getInitialState: function() {
const state = {
members: [],
this.memberDict = this.getMemberDict();
const members = this.roomMembers();
return {
members: members,
filteredJoinedMembers: this._filterMembers(members, 'join'),
filteredInvitedMembers: this._filterMembers(members, 'invite'),
// ideally we'd size this to the page height, but
// in practice I find that a little constraining
truncateAtJoined: INITIAL_LOAD_NUM_MEMBERS,
truncateAtInvited: INITIAL_LOAD_NUM_INVITED,
searchQuery: "",
};
if (!this.props.roomId) return state;
var cli = MatrixClientPeg.get();
var room = cli.getRoom(this.props.roomId);
if (!room) return state;
this.memberDict = this.getMemberDict();
state.members = this.roomMembers();
state.filteredJoinedMembers = this._filterMembers(state.members, 'join');
state.filteredInvitedMembers = this._filterMembers(state.members, 'invite');
return state;
},
componentWillMount: function() {
@ -207,11 +198,11 @@ module.exports = React.createClass({
},
_createOverflowTileJoined: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showFullJoinedMemberList);
return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList);
},
_createOverflowTileInvited: function(overflowCount, totalCount) {
return this._createOverflowTile(overflowCount, totalCount, this._showFullInvitedMemberList);
return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList);
},
_createOverflowTile: function(overflowCount, totalCount, onClick) {
@ -227,15 +218,15 @@ module.exports = React.createClass({
);
},
_showFullJoinedMemberList: function() {
_showMoreJoinedMemberList: function() {
this.setState({
truncateAtJoined: -1
truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT,
});
},
_showFullInvitedMemberList: function() {
_showMoreInvitedMemberList: function() {
this.setState({
truncateAtInvited: -1
truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT,
});
},

View File

@ -22,6 +22,14 @@ export const GroupMemberType = PropTypes.shape({
avatarUrl: PropTypes.string,
});
export const GroupRoomType = PropTypes.shape({
name: PropTypes.string,
// TODO: API doesn't return this yet
// roomId: PropTypes.string.isRequired,
canonicalAlias: PropTypes.string,
avatarUrl: PropTypes.string,
});
export function groupMemberFromApiObject(apiObject) {
return {
userId: apiObject.user_id,
@ -29,3 +37,11 @@ export function groupMemberFromApiObject(apiObject) {
avatarUrl: apiObject.avatar_url,
};
}
export function groupRoomFromApiObject(apiObject) {
return {
name: apiObject.name,
canonicalAlias: apiObject.canonical_alias,
avatarUrl: apiObject.avatar_url,
};
}

View File

@ -846,6 +846,7 @@
"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>",
"Description": "Description",
"Filter group members": "Filter group members",
"Filter group rooms": "Filter group rooms",
"Remove from group": "Remove from group",
"Invite new group members": "Invite new group members",
"Who would you like to add to this group?": "Who would you like to add to this group?",
@ -876,11 +877,22 @@
"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",
"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",
"The room '%(roomName)' could not be removed from the summary.": "The room '%(roomName)' could not be removed from the summary.",
"The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.",
"Failed to remove a user from the summary of %(groupId)s": "Failed to remove a user from the summary of %(groupId)s",
"The user '%(displayName)s' could not be removed from the summary.": "The user '%(displayName)s' could not be removed from the summary.",
"Light theme": "Light theme",
"Dark theme": "Dark theme",
"Unknown": "Unknown"
"Unknown": "Unknown",
"Failed to add the following rooms to the summary of %(groupId)s:": "Failed to add the following rooms to the summary of %(groupId)s:",
"The room '%(roomName)s' could not be removed from the summary.": "The room '%(roomName)s' could not be removed from the summary.",
"Add rooms to the group": "Add rooms to the group",
"Which rooms would you like to add to this group?": "Which rooms would you like to add to this group?",
"Add to group": "Add to group",
"Failed to add the following rooms to %(groupId)s:": "Failed to add the following rooms to %(groupId)s:",
"Unpublish": "Unpublish",
"This group is published on your profile": "This group is published on your profile",
"Publish": "Publish",
"This group is not published on your profile": "This group is not published on your profile"
}

View File

@ -0,0 +1,77 @@
/*
Copyright 2017 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 EventEmitter from 'events';
/**
* Stores the group summary for a room and provides an API to change it
*/
export default class GroupSummaryStore extends EventEmitter {
constructor(matrixClient, groupId) {
super();
this._groupId = groupId;
this._matrixClient = matrixClient;
this._summary = {};
this._fetchSummary();
}
_fetchSummary() {
this._matrixClient.getGroupSummary(this._groupId).then((resp) => {
this._summary = resp;
this._notifyListeners();
}).catch((err) => {
this.emit('error', err);
});
}
_notifyListeners() {
this.emit('update');
}
getSummary() {
return this._summary;
}
addRoomToGroupSummary(roomId, categoryId) {
return this._matrixClient
.addRoomToGroupSummary(this._groupId, roomId, categoryId)
.then(this._fetchSummary.bind(this));
}
addUserToGroupSummary(userId, roleId) {
return this._matrixClient
.addUserToGroupSummary(this._groupId, userId, roleId)
.then(this._fetchSummary.bind(this));
}
removeRoomFromGroupSummary(roomId) {
return this._matrixClient
.removeRoomFromGroupSummary(this._groupId, roomId)
.then(this._fetchSummary.bind(this));
}
removeUserFromGroupSummary(userId) {
return this._matrixClient
.removeUserFromGroupSummary(this._groupId, userId)
.then(this._fetchSummary.bind(this));
}
setGroupPublicity(isPublished) {
return this._matrixClient
.setGroupPublicity(this._groupId, isPublished)
.then(this._fetchSummary.bind(this));
}
}