Merge branch 'develop' into travis/granular-settings

This commit is contained in:
Travis Ralston 2017-11-07 14:55:12 -07:00 committed by GitHub
commit 289b0c2b6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 419 additions and 57 deletions

View File

@ -208,7 +208,7 @@ const sanitizeHtmlParams = {
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s.
if (!attribs.src.startsWith('mxc://')) {
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
return { tagName, attribs: {}};
}
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(

View File

@ -25,7 +25,6 @@ const onAction = function(payload) {
const UnknownDeviceDialog = sdk.getComponent('dialogs.UnknownDeviceDialog');
isDialogOpen = true;
Modal.createTrackedDialog('Unknown Device Error', '', UnknownDeviceDialog, {
devices: payload.err.devices,
room: payload.room,
onFinished: (r) => {
isDialogOpen = false;

View File

@ -23,6 +23,7 @@ import DuckDuckGoProvider from './DuckDuckGoProvider';
import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider';
import Promise from 'bluebird';
export type SelectionRange = {
@ -44,6 +45,7 @@ const PROVIDERS = [
UserProvider,
RoomProvider,
EmojiProvider,
NotifProvider,
CommandProvider,
DuckDuckGoProvider,
];

View File

@ -0,0 +1,62 @@
/*
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 AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler';
import MatrixClientPeg from '../MatrixClientPeg';
import {PillCompletion} from './Components';
import sdk from '../index';
const AT_ROOM_REGEX = /@\S*/g;
export default class NotifProvider extends AutocompleteProvider {
constructor(room) {
super(AT_ROOM_REGEX);
this.room = room;
}
async getCompletions(query: string, selection: {start: number, end: number}, force = false) {
const RoomAvatar = sdk.getComponent('views.avatars.RoomAvatar');
const client = MatrixClientPeg.get();
if (!this.room.currentState.mayTriggerNotifOfType('room', client.credentials.userId)) return [];
const {command, range} = this.getCurrentCommand(query, selection, force);
if (command && command[0] && '@room'.startsWith(command[0]) && command[0].length > 1) {
return [{
completion: '@room',
suffix: ' ',
component: (
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={this.room} />} title="@room" description={_t("Notify the whole room")} />
),
range,
}];
}
return [];
}
getName() {
return '❗️ ' + _t('Room Notification');
}
renderCompletions(completions: [React.Component]): ?React.Component {
return <div className="mx_Autocomplete_Completion_container_pill mx_Autocomplete_Completion_container_truncate">
{ completions }
</div>;
}
}

View File

@ -22,7 +22,7 @@ import MatrixClientPeg from '../../MatrixClientPeg';
import sdk from '../../index';
import dis from '../../dispatcher';
import { sanitizedHtmlNode } from '../../HtmlUtils';
import { _t } from '../../languageHandler';
import { _t, _td, _tJsx } from '../../languageHandler';
import AccessibleButton from '../views/elements/AccessibleButton';
import Modal from '../../Modal';
import classnames from 'classnames';
@ -32,6 +32,17 @@ import GroupStore from '../../stores/GroupStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import GeminiScrollbar from 'react-gemini-scrollbar';
const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1>
<p>
Use the long description to introduce new members to the community, or distribute
some important <a href="foo">links</a>
</p>
<p>
You can even use 'img' tags
</p>
`);
const RoomSummaryType = PropTypes.shape({
room_id: PropTypes.string.isRequired,
profile: PropTypes.shape({
@ -392,6 +403,8 @@ export default React.createClass({
propTypes: {
groupId: PropTypes.string.isRequired,
// Whether this is the first time the group admin is viewing the group
groupIsNew: PropTypes.bool,
},
childContextTypes: {
@ -422,7 +435,7 @@ export default React.createClass({
componentWillMount: function() {
this._changeAvatarComponent = null;
this._initGroupStore(this.props.groupId);
this._initGroupStore(this.props.groupId, true);
MatrixClientPeg.get().on("Group.myMembership", this._onGroupMyMembership);
},
@ -449,7 +462,7 @@ export default React.createClass({
this.setState({membershipBusy: false});
},
_initGroupStore: function(groupId) {
_initGroupStore: function(groupId, firstInit) {
this._groupStore = GroupStoreCache.getGroupStore(MatrixClientPeg.get(), groupId);
this._groupStore.registerListener(() => {
const summary = this._groupStore.getSummary();
@ -472,6 +485,9 @@ export default React.createClass({
),
error: null,
});
if (this.props.groupIsNew && firstInit) {
this._onEditClick();
}
});
this._groupStore.on('error', (err) => {
this.setState({
@ -661,6 +677,14 @@ export default React.createClass({
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const Spinner = sdk.getComponent('elements.Spinner');
const ToolTipButton = sdk.getComponent('elements.ToolTipButton');
const roomsHelpNode = this.state.editing ? <ToolTipButton helpText={
_t(
'These rooms are displayed to community members on the community page. '+
'Community members can join the rooms by clicking on them.',
)
} /> : <div />;
const addRoomRow = this.state.editing ?
(<AccessibleButton className="mx_GroupView_rooms_header_addRow"
@ -673,14 +697,23 @@ export default React.createClass({
{ _t('Add rooms to this community') }
</div>
</AccessibleButton>) : <div />;
const roomDetailListClassName = classnames({
"mx_fadable": true,
"mx_fadable_faded": this.state.editing,
});
return <div className="mx_GroupView_rooms">
<div className="mx_GroupView_rooms_header">
<h3>{ _t('Rooms') }</h3>
<h3>
{ _t('Rooms') }
{ roomsHelpNode }
</h3>
{ addRoomRow }
</div>
{ this.state.groupRoomsLoading ?
<Spinner /> :
<RoomDetailList rooms={this.state.groupRooms} />
<RoomDetailList
rooms={this.state.groupRooms}
className={roomDetailListClassName} />
}
</div>;
},
@ -851,6 +884,18 @@ export default React.createClass({
let description = null;
if (summary.profile && summary.profile.long_description) {
description = sanitizedHtmlNode(summary.profile.long_description);
} else if (this.state.isUserPrivileged) {
description = <div
className="mx_GroupView_groupDesc_placeholder"
onClick={this._onEditClick}
>
{ _tJsx(
'Your community hasn\'t got a Long Description, a HTML page to show to community members.<br />' +
'Click here to open settings and give it one!',
[/<br \/>/],
[(sub) => <br />])
}
</div>;
}
const groupDescEditingClasses = classnames({
"mx_GroupView_groupDesc": true,
@ -862,6 +907,7 @@ export default React.createClass({
<h3> { _t("Long Description (HTML)") } </h3>
<textarea
value={this.state.profileForm.long_description}
placeholder={_t(LONG_DESC_PLACEHOLDER)}
onChange={this._onLongDescChange}
tabIndex="4"
key="editLongDesc"

View File

@ -301,6 +301,7 @@ export default React.createClass({
case PageTypes.GroupView:
page_element = <GroupView
groupId={this.props.currentGroupId}
isNew={this.props.currentGroupIsNew}
collapsedRhs={this.props.collapseRhs}
/>;
if (!this.props.collapseRhs) right_panel = <RightPanel groupId={this.props.currentGroupId} disabled={this.props.rightDisabled} />;

View File

@ -490,7 +490,10 @@ module.exports = React.createClass({
case 'view_group':
{
const groupId = payload.group_id;
this.setState({currentGroupId: groupId});
this.setState({
currentGroupId: groupId,
currentGroupIsNew: payload.group_is_new,
});
this._setPage(PageTypes.GroupView);
this.notifyNewScreen('group/' + groupId);
}

View File

@ -301,6 +301,15 @@ module.exports = React.createClass({
_shouldShowApps: function(room) {
if (!BROWSER_SUPPORTS_SANDBOX) return false;
// Check if user has previously chosen to hide the app drawer for this
// room. If so, do not show apps
let hideWidgetDrawer = localStorage.getItem(
room.roomId + "_hide_widget_drawer");
if (hideWidgetDrawer === "true") {
return false;
}
const appsStateEvents = room.currentState.getStateEvents('im.vector.modular.widgets');
// any valid widget = show apps
for (let i = 0; i < appsStateEvents.length; i++) {

View File

@ -81,6 +81,7 @@ export default React.createClass({
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
this.props.onFinished(true);
}).catch((e) => {

View File

@ -48,8 +48,9 @@ function UserUnknownDeviceList(props) {
const {userId, userDevices} = props;
const deviceListEntries = Object.keys(userDevices).map((deviceId) =>
<DeviceListEntry key={deviceId} userId={userId}
device={userDevices[deviceId]} />,
<DeviceListEntry key={deviceId} userId={userId}
device={userDevices[deviceId]}
/>,
);
return (
@ -92,26 +93,60 @@ export default React.createClass({
propTypes: {
room: React.PropTypes.object.isRequired,
// map from userid -> deviceid -> deviceinfo
devices: React.PropTypes.object.isRequired,
onFinished: React.PropTypes.func.isRequired,
},
componentDidMount: function() {
// Given we've now shown the user the unknown device, it is no longer
// unknown to them. Therefore mark it as 'known'.
Object.keys(this.props.devices).forEach((userId) => {
Object.keys(this.props.devices[userId]).map((deviceId) => {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
});
componentWillMount: function() {
this._unmounted = false;
const roomMembers = this.props.room.getJoinedMembers().map((m) => {
return m.userId;
});
// XXX: temporary logging to try to diagnose
// https://github.com/vector-im/riot-web/issues/3148
console.log('Opening UnknownDeviceDialog');
this.setState({
// map from userid -> deviceid -> deviceinfo
devices: null,
});
MatrixClientPeg.get().downloadKeys(roomMembers, false).then((devices) => {
if (this._unmounted) return;
const unknownDevices = {};
// This is all devices in this room, so find the unknown ones.
Object.keys(devices).forEach((userId) => {
Object.keys(devices[userId]).map((deviceId) => {
const device = devices[userId][deviceId];
if (device.isUnverified() && !device.isKnown()) {
if (unknownDevices[userId] === undefined) {
unknownDevices[userId] = {};
}
unknownDevices[userId][deviceId] = device;
}
// Given we've now shown the user the unknown device, it is no longer
// unknown to them. Therefore mark it as 'known'.
if (!device.isKnown()) {
MatrixClientPeg.get().setDeviceKnown(userId, deviceId, true);
}
});
});
this.setState({
devices: unknownDevices,
});
});
},
componentWillUnmount: function() {
this._unmounted = true;
},
render: function() {
if (this.state.devices === null) {
const Spinner = sdk.getComponent("elements.Spinner");
return <Spinner />;
}
const client = MatrixClientPeg.get();
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() ||
this.props.room.getBlacklistUnverifiedDevices();
@ -154,7 +189,7 @@ export default React.createClass({
{ warning }
{ _t("Unknown devices") }:
<UnknownDeviceList devices={this.props.devices} />
<UnknownDeviceList devices={this.state.devices} />
</GeminiScrollbar>
<div className="mx_Dialog_buttons">
<button className="mx_Dialog_primary" autoFocus={true}

View File

@ -26,11 +26,9 @@ class MenuOption extends React.Component {
this._onClick = this._onClick.bind(this);
}
getDefaultProps() {
return {
disabled: false,
};
}
static defaultProps = {
disabled: false,
};
_onMouseEnter() {
this.props.onMouseEnter(this.props.dropdownKey);

View File

@ -0,0 +1,55 @@
/*
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 sdk from '../../../index';
module.exports = React.createClass({
displayName: 'ToolTipButton',
getInitialState: function() {
return {
hover: false,
};
},
onMouseOver: function() {
this.setState({
hover: true,
});
},
onMouseOut: function() {
this.setState({
hover: false,
});
},
render: function() {
const RoomTooltip = sdk.getComponent("rooms.RoomTooltip");
const tip = this.state.hover ? <RoomTooltip
className="mx_ToolTipButton_container"
tooltipClassName="mx_ToolTipButton_helpText"
label={this.props.helpText}
/> : <div />;
return (
<div className="mx_ToolTipButton" onMouseOver={this.onMouseOver} onMouseOut={this.onMouseOut} >
?
{ tip }
</div>
);
},
});

View File

@ -61,9 +61,9 @@ export default withMatrixClient(React.createClass({
);
return (
<EntityTile presenceState="online"
avatarJsx={av} onClick={this.onClick}
name={name} powerLevel={0} suppressOnHover={true}
<EntityTile name={name} avatarJsx={av} onClick={this.onClick}
suppressOnHover={true} presenceState="online"
powerStatus={this.props.member.isAdmin ? EntityTile.POWER_STATUS_ADMIN : null}
/>
);
},

View File

@ -81,16 +81,25 @@ module.exports = React.createClass({
},
onAction: function(action) {
const hideWidgetKey = this.props.room.roomId + "_hide_widget_drawer";
switch (action.action) {
case 'appsDrawer':
// When opening the app draw when there aren't any apps, auto-launch the
// integrations manager to skip the awkward click on "Add widget"
// When opening the app drawer when there aren't any apps,
// auto-launch the integrations manager to skip the awkward
// click on "Add widget"
if (action.show) {
const apps = this._getApps();
if (apps.length === 0) {
this._launchManageIntegrations();
}
localStorage.removeItem(hideWidgetKey);
} else {
// Store hidden state of widget
// Don't show if previously hidden
localStorage.setItem(hideWidgetKey, true);
}
break;
}
},

View File

@ -47,7 +47,7 @@ function presenceClassForMember(presenceState, lastActiveAgo) {
}
}
module.exports = React.createClass({
const EntityTile = React.createClass({
displayName: 'EntityTile',
propTypes: {
@ -140,16 +140,19 @@ module.exports = React.createClass({
}
let power;
const powerLevel = this.props.powerLevel;
if (powerLevel >= 50 && powerLevel < 99) {
power = <img src="img/mod.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Moderator")} />;
}
if (powerLevel >= 99) {
power = <img src="img/admin.svg" className="mx_EntityTile_power" width="16" height="17" alt={_t("Admin")} />;
const powerStatus = this.props.powerStatus;
if (powerStatus) {
const src = {
[EntityTile.POWER_STATUS_MODERATOR]: "img/mod.svg",
[EntityTile.POWER_STATUS_ADMIN]: "img/admin.svg",
}[powerStatus];
const alt = {
[EntityTile.POWER_STATUS_MODERATOR]: _t("Moderator"),
[EntityTile.POWER_STATUS_ADMIN]: _t("Admin"),
}[powerStatus];
power = <img src={src} className="mx_EntityTile_power" width="16" height="17" alt={alt} />;
}
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar');
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
@ -168,3 +171,9 @@ module.exports = React.createClass({
);
},
});
EntityTile.POWER_STATUS_MODERATOR = "moderator";
EntityTile.POWER_STATUS_ADMIN = "admin";
export default EntityTile;

View File

@ -86,13 +86,19 @@ module.exports = React.createClass({
}
this.member_last_modified_time = member.getLastModifiedTime();
// We deliberately leave power levels that are not 100 or 50 undefined
const powerStatus = {
100: EntityTile.POWER_STATUS_ADMIN,
50: EntityTile.POWER_STATUS_MODERATOR,
}[this.props.member.powerLevel];
return (
<EntityTile {...this.props} presenceState={presenceState}
presenceLastActiveAgo={member.user ? member.user.lastActiveAgo : 0}
presenceLastTs={member.user ? member.user.lastPresenceTs : 0}
presenceCurrentlyActive={member.user ? member.user.currentlyActive : false}
avatarJsx={av} title={this.getPowerLabel()} onClick={this.onClick}
name={name} powerLevel={this.props.member.powerLevel} />
name={name} powerStatus={powerStatus} />
);
},
});

View File

@ -58,6 +58,11 @@ const TYPING_USER_TIMEOUT = 10000, TYPING_SERVER_TIMEOUT = 30000;
const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
const ENTITY_TYPES = {
AT_ROOM_PILL: 'ATROOMPILL',
};
function stateToMarkdown(state) {
return __stateToMarkdown(state)
.replace(
@ -188,13 +193,16 @@ export default class MessageComposerInput extends React.Component {
this.client = MatrixClientPeg.get();
}
findLinkEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
findPillEntities(contentState: ContentState, contentBlock: ContentBlock, callback) {
contentBlock.findEntityRanges(
(character) => {
const entityKey = character.getEntity();
return (
entityKey !== null &&
contentState.getEntity(entityKey).getType() === 'LINK'
(
contentState.getEntity(entityKey).getType() === 'LINK' ||
contentState.getEntity(entityKey).getType() === ENTITY_TYPES.AT_ROOM_PILL
)
);
}, callback,
);
@ -210,11 +218,19 @@ export default class MessageComposerInput extends React.Component {
RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
decorators.push({
strategy: this.findLinkEntities.bind(this),
strategy: this.findPillEntities.bind(this),
component: (entityProps) => {
const Pill = sdk.getComponent('elements.Pill');
const type = entityProps.contentState.getEntity(entityProps.entityKey).getType();
const {url} = entityProps.contentState.getEntity(entityProps.entityKey).getData();
if (Pill.isPillUrl(url)) {
if (type === ENTITY_TYPES.AT_ROOM_PILL) {
return <Pill
type={Pill.TYPE_AT_ROOM_MENTION}
room={this.props.room}
offsetKey={entityProps.offsetKey}
shouldShowPillAvatar={shouldShowPillAvatar}
/>;
} else if (Pill.isPillUrl(url)) {
return <Pill
url={url}
room={this.props.room}
@ -784,7 +800,7 @@ export default class MessageComposerInput extends React.Component {
const pt = contentState.getBlocksAsArray().map((block) => {
let blockText = block.getText();
let offset = 0;
this.findLinkEntities(contentState, block, (start, end) => {
this.findPillEntities(contentState, block, (start, end) => {
const entity = contentState.getEntity(block.getEntityAt(start));
if (entity.getType() !== 'LINK') {
return;
@ -989,6 +1005,11 @@ export default class MessageComposerInput extends React.Component {
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
} else if (completion === '@room') {
contentState = contentState.createEntity(ENTITY_TYPES.AT_ROOM_PILL, 'IMMUTABLE', {
isCompletion: true,
});
entityKey = contentState.getLastCreatedEntityKey();
}
let selection;

View File

@ -19,6 +19,7 @@ import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from "../elements/AccessibleButton";
import PinnedEventTile from "./PinnedEventTile";
import { _t } from '../../../languageHandler';
import PinningUtils from "../../../utils/PinningUtils";
module.exports = React.createClass({
displayName: 'PinnedEventsPanel',
@ -61,20 +62,39 @@ module.exports = React.createClass({
Promise.all(promises).then((contexts) => {
// Filter out the messages before we try to render them
const pinned = contexts.filter((context) => {
if (!context) return false; // no context == not applicable for the room
if (context.event.getType() !== "m.room.message") return false;
if (context.event.isRedacted()) return false;
return true;
});
const pinned = contexts.filter((context) => PinningUtils.isPinnable(context.event));
this.setState({ loading: false, pinned });
});
}
this._updateReadState();
},
_updateReadState: function() {
const pinnedEvents = this.props.room.currentState.getStateEvents("m.room.pinned_events", "");
if (!pinnedEvents) return; // nothing to read
let readStateEvents = [];
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
readStateEvents = readPinsEvent.getContent().event_ids || [];
}
if (!readStateEvents.includes(pinnedEvents.getId())) {
readStateEvents.push(pinnedEvents.getId());
// Only keep the last 10 event IDs to avoid infinite growth
readStateEvents = readStateEvents.reverse().splice(0, 10).reverse();
MatrixClientPeg.get().setRoomAccountData(this.props.room.roomId, "im.vector.room.read_pins", {
event_ids: readStateEvents,
});
}
},
_getPinnedTiles: function() {
if (this.state.pinned.length == 0) {
if (this.state.pinned.length === 0) {
return (<div>{ _t("No pinned messages.") }</div>);
}

View File

@ -23,6 +23,7 @@ import sanitizeHtml from 'sanitize-html';
import { ContentRepo } from 'matrix-js-sdk';
import MatrixClientPeg from '../../../MatrixClientPeg';
import PropTypes from 'prop-types';
import classNames from 'classnames';
function getDisplayAliasForRoom(room) {
return room.canonicalAlias || (room.aliases ? room.aliases[0] : "");
@ -117,6 +118,8 @@ export default React.createClass({
worldReadable: PropTypes.bool,
guestCanJoin: PropTypes.bool,
})),
className: PropTypes.string,
},
getRows: function() {
@ -138,7 +141,7 @@ export default React.createClass({
</tbody>
</table>;
}
return <div className="mx_RoomDetailList">
return <div className={classNames("mx_RoomDetailList", this.props.className)}>
{ rooms }
</div>;
},

View File

@ -65,6 +65,7 @@ module.exports = React.createClass({
componentDidMount: function() {
const cli = MatrixClientPeg.get();
cli.on("RoomState.events", this._onRoomStateEvents);
cli.on("Room.accountData", this._onRoomAccountData);
// When a room name occurs, RoomState.events is fired *before*
// room.name is updated. So we have to listen to Room.name as well as
@ -87,6 +88,7 @@ module.exports = React.createClass({
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener("RoomState.events", this._onRoomStateEvents);
cli.removeListener("Room.accountData", this._onRoomAccountData);
}
},
@ -99,6 +101,13 @@ module.exports = React.createClass({
this._rateLimitedUpdate();
},
_onRoomAccountData: function(event, room) {
if (!this.props.room || room.roomId !== this.props.room.roomId) return;
if (event.getType() !== "im.vector.room.read_pins") return;
this._rateLimitedUpdate();
},
_rateLimitedUpdate: new RateLimitedFunc(function() {
/* eslint-disable babel/no-invalid-this */
this.forceUpdate();
@ -139,6 +148,32 @@ module.exports = React.createClass({
dis.dispatch({ action: 'show_right_panel' });
},
_hasUnreadPins: function() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
if (currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0) {
return false; // no pins == nothing to read
}
const readPinsEvent = this.props.room.getAccountData("im.vector.room.read_pins");
if (readPinsEvent && readPinsEvent.getContent()) {
const readStateEvents = readPinsEvent.getContent().event_ids || [];
if (readStateEvents) {
return !readStateEvents.includes(currentPinEvent.getId());
}
}
// There's pins, and we haven't read any of them
return true;
},
_hasPins: function() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
return !(currentPinEvent.getContent().pinned && currentPinEvent.getContent().pinned.length <= 0);
},
/**
* After editing the settings, get the new name for the room
*
@ -305,8 +340,17 @@ module.exports = React.createClass({
}
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);
} else if (this._hasPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator" />);
}
pinnedEventsButton =
<AccessibleButton className="mx_RoomHeader_button" onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
<AccessibleButton className="mx_RoomHeader_button mx_RoomHeader_pinnedButton"
onClick={this.props.onPinnedClick} title={_t("Pinned Messages")}>
{ pinsIndicator }
<TintableSvg src="img/icons-pin.svg" width="16" height="16" />
</AccessibleButton>;
}

View File

@ -36,6 +36,7 @@ export function groupMemberFromApiObject(apiObject) {
userId: apiObject.user_id,
displayname: apiObject.displayname,
avatarUrl: apiObject.avatar_url,
isAdmin: apiObject.is_admin,
};
}

View File

@ -669,6 +669,7 @@
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
"You must join the room to see its files": "You must join the room to see its files",
"There are no visible files in this room": "There are no visible files in this room",
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n",
"Add rooms to the community summary": "Add rooms to the community summary",
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
"Add to summary": "Add to summary",
@ -691,6 +692,7 @@
"Leave": "Leave",
"Unable to leave room": "Unable to leave room",
"Community Settings": "Community Settings",
"These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.": "These rooms are displayed to community members on the community page. Community members can join the rooms by clicking on them.",
"Add rooms to this community": "Add rooms to this community",
"Featured Rooms:": "Featured Rooms:",
"Featured Users:": "Featured Users:",
@ -699,6 +701,7 @@
"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",
"Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!": "Your community hasn't got a Long Description, a HTML page to show to community members.<br />Click here to open settings and give it one!",
"Long Description (HTML)": "Long Description (HTML)",
"Description": "Description",
"Community %(groupId)s not found": "Community %(groupId)s not found",
@ -889,6 +892,8 @@
"Commands": "Commands",
"Results from DuckDuckGo": "Results from DuckDuckGo",
"Emoji": "Emoji",
"Notify the whole room": "Notify the whole room",
"Room Notification": "Room Notification",
"Users": "Users",
"unknown device": "unknown device",
"NOT verified": "NOT verified",

View File

@ -33,6 +33,9 @@ export default class GroupStore extends EventEmitter {
constructor(matrixClient, groupId) {
super();
if (!groupId) {
throw new Error('GroupStore needs a valid groupId to be created');
}
this.groupId = groupId;
this._matrixClient = matrixClient;
this._summary = {};

30
src/utils/PinningUtils.js Normal file
View File

@ -0,0 +1,30 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export default class PinningUtils {
/**
* Determines if the given event may be pinned.
* @param {MatrixEvent} event The event to check.
* @return {boolean} True if the event may be pinned, false otherwise.
*/
static isPinnable(event) {
if (!event) return false;
if (event.getType() !== "m.room.message") return false;
if (event.isRedacted()) return false;
return true;
}
}