1
0
mirror of https://github.com/matrix-org/matrix-react-sdk.git synced 2025-11-20 16:22:28 +03:00

Merge branch 'develop' into matthew/retina

This commit is contained in:
Bruno Windels
2019-04-09 10:55:05 +02:00
64 changed files with 1283 additions and 546 deletions

View File

@@ -124,6 +124,7 @@ export default class AutoHideScrollbar extends React.Component {
style={this.props.style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
onScroll={this.props.onScroll}
onWheel={this.props.onWheel}
>
<div className="mx_AutoHideScrollbar_offset">
{ this.props.children }

View File

@@ -24,6 +24,11 @@ export default class IndicatorScrollbar extends React.Component {
// and mx_IndicatorScrollbar_rightOverflowIndicator elements to the list for positioning
// by the parent element.
trackHorizontalOverflow: PropTypes.bool,
// If true, when the user tries to use their mouse wheel in the component it will
// scroll horizontally rather than vertically. This should only be used on components
// with no vertical scroll opportunity.
verticalScrollsHorizontally: PropTypes.bool,
};
constructor(props) {
@@ -40,6 +45,13 @@ export default class IndicatorScrollbar extends React.Component {
};
}
moveToOrigin() {
if (!this._scrollElement) return;
this._scrollElement.scrollLeft = 0;
this._scrollElement.scrollTop = 0;
}
_collectScroller(scroller) {
if (scroller && !this._scrollElement) {
this._scrollElement = scroller;
@@ -106,6 +118,13 @@ export default class IndicatorScrollbar extends React.Component {
}
}
onMouseWheel = (e) => {
if (this.props.verticalScrollsHorizontally && this._scrollElement) {
// noinspection JSSuspiciousNameCombination
this._scrollElement.scrollLeft += e.deltaY / 2; // divide by 2 to reduce harshness
}
};
render() {
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
@@ -117,6 +136,7 @@ export default class IndicatorScrollbar extends React.Component {
return (<AutoHideScrollbar
ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller}
onWheel={this.onMouseWheel}
{... this.props}
>
{ leftOverflowIndicator }

View File

@@ -27,6 +27,7 @@ import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
import Analytics from "../../Analytics";
const LeftPanel = React.createClass({
@@ -45,11 +46,23 @@ const LeftPanel = React.createClass({
getInitialState: function() {
return {
searchFilter: '',
breadcrumbs: false,
};
},
componentWillMount: function() {
this.focusedElement = null;
this._settingWatchRef = SettingsStore.watchSetting(
"feature_room_breadcrumbs", null, this._onBreadcrumbsChanged);
const useBreadcrumbs = SettingsStore.isFeatureEnabled("feature_room_breadcrumbs");
Analytics.setBreadcrumbs(useBreadcrumbs);
this.setState({breadcrumbs: useBreadcrumbs});
},
componentWillUnmount: function() {
SettingsStore.unwatchSetting(this._settingWatchRef);
},
shouldComponentUpdate: function(nextProps, nextState) {
@@ -73,6 +86,22 @@ const LeftPanel = React.createClass({
return false;
},
componentDidUpdate(prevProps, prevState) {
if (prevState.breadcrumbs !== this.state.breadcrumbs) {
Analytics.setBreadcrumbs(this.state.breadcrumbs);
}
},
_onBreadcrumbsChanged: function(settingName, roomId, level, valueAtLevel, value) {
// Features are only possible at a single level, so we can get away with using valueAtLevel.
// The SettingsStore runs on the same tick as the update, so `value` will be wrong.
this.setState({breadcrumbs: valueAtLevel});
// For some reason the setState doesn't trigger a render of the component, so force one.
// Probably has to do with the change happening outside of a change detector cycle.
this.forceUpdate();
},
_onFocus: function(ev) {
this.focusedElement = ev.target;
},
@@ -220,7 +249,7 @@ const LeftPanel = React.createClass({
collapsed={this.props.collapsed} />);
let breadcrumbs;
if (SettingsStore.isFeatureEnabled("feature_room_breadcrumbs")) {
if (this.state.breadcrumbs) {
breadcrumbs = (<RoomBreadcrumbs collapsed={this.props.collapsed} />);
}

View File

@@ -597,8 +597,8 @@ export default React.createClass({
break;
case 'view_user_settings': {
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {}, 'mx_SettingsDialog',
/*isPriority=*/false, /*isStatic=*/true);
Modal.createTrackedDialog('User settings', '', UserSettingsDialog, {},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true);
// View the welcome or home page if we need something to look at
this._viewSomethingBehindModal();
@@ -2032,7 +2032,6 @@ export default React.createClass({
fallbackHsUrl={this.getFallbackHsUrl()}
defaultDeviceDisplayName={this.props.defaultDeviceDisplayName}
onForgotPasswordClick={this.onForgotPasswordClick}
enableGuest={this.props.enableGuest}
onServerConfigChange={this.onServerConfigChange}
/>
);

View File

@@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 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.
@@ -29,7 +29,6 @@ import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
import RoomTile from "../views/rooms/RoomTile";
import LazyRenderList from "../views/elements/LazyRenderList";
import MatrixClientPeg from "../../MatrixClientPeg";
// turn this on for drop & drag console debugging galore
const debug = false;
@@ -139,28 +138,6 @@ const RoomSubList = React.createClass({
this.setState(this.state);
},
getUnreadNotificationCount: function(room, type=null) {
let notificationCount = room.getUnreadNotificationCount(type);
// Check notification counts in the old room just in case there's some lost
// there. We only go one level down to avoid performance issues, and theory
// is that 1st generation rooms will have already been read by the 3rd generation.
const createEvent = room.currentState.getStateEvents("m.room.create", "");
if (createEvent && createEvent.getContent()['predecessor']) {
const oldRoomId = createEvent.getContent()['predecessor']['room_id'];
const oldRoom = MatrixClientPeg.get().getRoom(oldRoomId);
if (oldRoom) {
// We only ever care if there's highlights in the old room. No point in
// notifying the user for unread messages because they would have extreme
// difficulty changing their notification preferences away from "All Messages"
// and "Noisy".
notificationCount += oldRoom.getUnreadNotificationCount("highlight");
}
}
return notificationCount;
},
makeRoomTile: function(room) {
return <RoomTile
room={room}
@@ -169,8 +146,8 @@ const RoomSubList = React.createClass({
key={room.roomId}
collapsed={this.props.collapsed || false}
unread={Unread.doesRoomHaveUnreadMessages(room)}
highlight={this.props.isInvite || this.getUnreadNotificationCount(room, 'highlight') > 0}
notificationCount={this.getUnreadNotificationCount(room)}
highlight={this.props.isInvite || RoomNotifs.getUnreadNotificationCount(room, 'highlight') > 0}
notificationCount={RoomNotifs.getUnreadNotificationCount(room)}
isInvite={this.props.isInvite}
refreshSubList={this._updateSubListCount}
incomingCall={null}

View File

@@ -536,11 +536,14 @@ module.exports = React.createClass({
payload.data.description || payload.data.name);
break;
case 'picture_snapshot':
this.uploadFile(payload.file);
return ContentMessages.sharedInstance().sendContentListToRoom(
[payload.file], this.state.room.roomId, MatrixClientPeg.get(),
);
break;
case 'notifier_enabled':
case 'upload_started':
case 'upload_finished':
case 'upload_canceled':
this.forceUpdate();
break;
case 'call_state':
@@ -728,8 +731,19 @@ module.exports = React.createClass({
if (!MatrixClientPeg.get().isRoomEncrypted(room.roomId)) {
return;
}
if (!MatrixClientPeg.get().isCryptoEnabled()) {
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
this.setState({
e2eStatus: "warning",
});
return;
}
room.hasUnverifiedDevices().then((hasUnverifiedDevices) => {
this.setState({e2eStatus: hasUnverifiedDevices ? "warning" : "verified"});
this.setState({
e2eStatus: hasUnverifiedDevices ? "warning" : "verified",
});
});
},

View File

@@ -39,7 +39,7 @@ const TagPanelButtons = React.createClass({
if (payload.action === "show_redesign_feedback_dialog") {
const RedesignFeedbackDialog =
sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createDialog(RedesignFeedbackDialog);
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
}
},

View File

@@ -16,7 +16,7 @@ limitations under the License.
const React = require('react');
import PropTypes from 'prop-types';
const ContentMessages = require('../../ContentMessages');
import ContentMessages from '../../ContentMessages';
const dis = require('../../dispatcher');
const filesize = require('filesize');
import { _t } from '../../languageHandler';
@@ -40,6 +40,7 @@ module.exports = React.createClass({displayName: 'UploadBar',
switch (payload.action) {
case 'upload_progress':
case 'upload_finished':
case 'upload_canceled':
case 'upload_failed':
if (this.mounted) this.forceUpdate();
break;

View File

@@ -54,8 +54,6 @@ module.exports = React.createClass({
propTypes: {
onLoggedIn: PropTypes.func.isRequired,
enableGuest: PropTypes.bool,
// The default server name to use when the user hasn't specified
// one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this
// via `.well-known` discovery. The server name is used instead of the
@@ -225,37 +223,6 @@ module.exports = React.createClass({
}).done();
},
_onLoginAsGuestClick: function(ev) {
ev.preventDefault();
ev.stopPropagation();
const self = this;
self.setState({
busy: true,
errorText: null,
loginIncorrect: false,
});
this._loginLogic.loginAsGuest().then(function(data) {
self.props.onLoggedIn(data);
}, function(error) {
let errorText;
if (error.httpStatus === 403) {
errorText = _t("Guest access is disabled on this homeserver.");
} else {
errorText = self._errorTextFromError(error);
}
self.setState({
errorText: errorText,
loginIncorrect: false,
});
}).finally(function() {
self.setState({
busy: false,
});
}).done();
},
onUsernameChanged: function(username) {
this.setState({ username: username });
},
@@ -627,14 +594,6 @@ module.exports = React.createClass({
const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText;
let loginAsGuestJsx;
if (this.props.enableGuest) {
loginAsGuestJsx =
<a className="mx_AuthBody_changeFlow" onClick={this._onLoginAsGuestClick} href="#">
{ _t('Try the app first') }
</a>;
}
let errorTextSection;
if (errorText) {
errorTextSection = (
@@ -658,7 +617,6 @@ module.exports = React.createClass({
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
{ _t('Create account') }
</a>
{ loginAsGuestJsx }
</AuthBody>
</AuthPage>
);

View File

@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
Copyright 2017, 2018, 2019 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.
@@ -566,7 +566,7 @@ module.exports = React.createClass({
rows="1"
id="textinput"
ref="textinput"
className="mx_ChatInviteDialog_input"
className="mx_AddressPickerDialog_input"
onChange={this.onQueryChanged}
placeholder={this.props.placeholder}
defaultValue={this.props.value}
@@ -578,7 +578,7 @@ module.exports = React.createClass({
let addressSelector;
if (this.state.error) {
const validTypeDescriptions = this.props.validAddressTypes.map((t) => _t(addressTypeName[t]));
error = <div className="mx_ChatInviteDialog_error">
error = <div className="mx_AddressPickerDialog_error">
{ _t("You have entered an invalid address.") }
<br />
{ _t("Try using one of the following valid address types: %(validTypesList)s.", {
@@ -586,9 +586,9 @@ module.exports = React.createClass({
}) }
</div>;
} else if (this.state.searchError) {
error = <div className="mx_ChatInviteDialog_error">{ this.state.searchError }</div>;
error = <div className="mx_AddressPickerDialog_error">{ this.state.searchError }</div>;
} else if (this.state.query.length > 0 && filteredSuggestedList.length === 0 && !this.state.busy) {
error = <div className="mx_ChatInviteDialog_error">{ _t("No results") }</div>;
error = <div className="mx_AddressPickerDialog_error">{ _t("No results") }</div>;
} else {
addressSelector = (
<AddressSelector ref={(ref) => {this.addressSelector = ref;}}
@@ -601,13 +601,13 @@ module.exports = React.createClass({
}
return (
<BaseDialog className="mx_ChatInviteDialog" onKeyDown={this.onKeyDown}
<BaseDialog className="mx_AddressPickerDialog" onKeyDown={this.onKeyDown}
onFinished={this.props.onFinished} title={this.props.title}>
<div className="mx_ChatInviteDialog_label">
<div className="mx_AddressPickerDialog_label">
<label htmlFor="textinput">{ this.props.description }</label>
</div>
<div className="mx_Dialog_content">
<div className="mx_ChatInviteDialog_inputContainer">{ query }</div>
<div className="mx_AddressPickerDialog_inputContainer">{ query }</div>
{ error }
{ addressSelector }
{ this.props.extraNode }

View File

@@ -1,6 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2018, 2019 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.
@@ -55,6 +55,11 @@ export default React.createClass({
// CSS class to apply to dialog div
className: PropTypes.string,
// if true, dialog container is 60% of the viewport width. Otherwise,
// the container will have no fixed size, allowing its contents to
// determine its size. Default: true.
fixedWidth: PropTypes.bool,
// Title for the dialog.
title: PropTypes.node.isRequired,
@@ -72,6 +77,7 @@ export default React.createClass({
getDefaultProps: function() {
return {
hasCancel: true,
fixedWidth: true,
};
},
@@ -113,7 +119,10 @@ export default React.createClass({
return (
<FocusTrap onKeyDown={this._onKeyDown}
className={this.props.className}
className={classNames({
[this.props.className]: true,
'mx_Dialog_fixedWidth': this.props.fixedWidth,
})}
role="dialog"
aria-labelledby='mx_BaseDialog_title'
// This should point to a node describing the dialog.
@@ -131,8 +140,8 @@ export default React.createClass({
{ this.props.title }
</div>
{ this.props.headerButton }
{ cancelButton }
</div>
{ cancelButton }
{ this.props.children }
</FocusTrap>
);

View File

@@ -87,6 +87,7 @@ export default class UploadConfirmDialog extends React.Component {
return (
<BaseDialog className='mx_UploadConfirmDialog'
fixedWidth={false}
onFinished={this._onCancelClick}
title={title}
contentId='mx_Dialog_content'

View File

@@ -42,7 +42,7 @@ export default class NetworkDropdown extends React.Component {
expanded: false,
selectedServer: server,
selectedInstanceId: null,
includeAllNetworks: true,
includeAllNetworks: false,
};
}
@@ -109,7 +109,7 @@ export default class NetworkDropdown extends React.Component {
expanded: false,
selectedServer: e.target.value,
selectedNetwork: null,
includeAllNetworks: true,
includeAllNetworks: false,
});
this.props.onOptionChange(e.target.value, null);
}

View File

@@ -339,9 +339,7 @@ export default class AppTile extends React.Component {
// Destroy the old widget messaging before starting it back up again. Some widgets
// have startup routines that run when they are loaded, so we just need to reinitialize
// the messaging for them.
if (ActiveWidgetStore.getWidgetMessaging(this.props.id)) {
ActiveWidgetStore.delWidgetMessaging(this.props.id);
}
ActiveWidgetStore.delWidgetMessaging(this.props.id);
this._setupWidgetMessaging();
ActiveWidgetStore.setRoomId(this.props.id, this.props.room.roomId);

View File

@@ -27,10 +27,8 @@ const Modal = require('../../../Modal');
const sdk = require('../../../index');
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'ImageView',
propTypes: {
export default class ImageView extends React.Component {
static propTypes = {
src: React.PropTypes.string.isRequired, // the source of the image being displayed
name: React.PropTypes.string, // the main title ('name') for the image
link: React.PropTypes.string, // the link (if any) applied to the name of the image
@@ -44,27 +42,32 @@ module.exports = React.createClass({
// properties above, which let us use lightboxes to display images which aren't associated
// with events.
mxEvent: React.PropTypes.object,
},
};
constructor(props) {
super(props);
this.state = { rotationDegrees: 0 };
}
// XXX: keyboard shortcuts for managing dialogs should be done by the modal
// dialog base class somehow, surely...
componentDidMount: function() {
componentDidMount() {
document.addEventListener("keydown", this.onKeyDown);
},
}
componentWillUnmount: function() {
componentWillUnmount() {
document.removeEventListener("keydown", this.onKeyDown);
},
}
onKeyDown: function(ev) {
onKeyDown = (ev) => {
if (ev.keyCode == 27) { // escape
ev.stopPropagation();
ev.preventDefault();
this.props.onFinished();
}
},
};
onRedactClick: function() {
onRedactClick = () => {
const ConfirmRedactDialog = sdk.getComponent("dialogs.ConfirmRedactDialog");
Modal.createTrackedDialog('Confirm Redact Dialog', 'Image View', ConfirmRedactDialog, {
onFinished: (proceed) => {
@@ -83,17 +86,29 @@ module.exports = React.createClass({
}).done();
},
});
},
};
getName: function() {
getName() {
let name = this.props.name;
if (name && this.props.link) {
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
}
return name;
},
}
render: function() {
rotateCounterClockwise = () => {
const cur = this.state.rotationDegrees;
const rotationDegrees = (cur - 90) % 360;
this.setState({ rotationDegrees });
};
rotateClockwise = () => {
const cur = this.state.rotationDegrees;
const rotationDegrees = (cur + 90) % 360;
this.setState({ rotationDegrees });
};
render() {
/*
// In theory max-width: 80%, max-height: 80% on the CSS should work
// but in practice, it doesn't, so do it manually:
@@ -122,7 +137,8 @@ module.exports = React.createClass({
height: displayHeight
};
*/
let style; let res;
let style = {};
let res;
if (this.props.width && this.props.height) {
style = {
@@ -168,15 +184,26 @@ module.exports = React.createClass({
</div>);
}
const rotationDegrees = this.state.rotationDegrees;
const effectiveStyle = {transform: `rotate(${rotationDegrees}deg)`, ...style};
return (
<div className="mx_ImageView">
<div className="mx_ImageView_lhs">
</div>
<div className="mx_ImageView_content">
<img src={this.props.src} title={this.props.name} style={style} />
<img src={this.props.src} title={this.props.name} style={effectiveStyle} className="mainImage" />
<div className="mx_ImageView_labelWrapper">
<div className="mx_ImageView_label">
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }><img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } /></AccessibleButton>
<AccessibleButton className="mx_ImageView_rotateCounterClockwise" onClick={ this.rotateCounterClockwise }>
<img src={require("../../../../res/img/rotate-ccw.svg")} alt={ _t('Rotate counter-clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_rotateClockwise" onClick={ this.rotateClockwise }>
<img src={require("../../../../res/img/rotate-cw.svg")} alt={ _t('Rotate clockwise') } width="18" height="18" />
</AccessibleButton>
<AccessibleButton className="mx_ImageView_cancel" onClick={ this.props.onFinished }>
<img src={require("../../../../res/img/cancel-white.svg")} width="18" height="18" alt={ _t('Close') } />
</AccessibleButton>
<div className="mx_ImageView_shim">
</div>
<div className="mx_ImageView_name">
@@ -199,5 +226,5 @@ module.exports = React.createClass({
</div>
</div>
);
},
});
}
}

View File

@@ -73,8 +73,12 @@ module.exports = React.createClass({
_initStateFromProps: function(newProps) {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter((l) => {
return l === undefined || l <= newProps.maxValue;
const options = Object.keys(levelRoleMap).filter(level => {
return (
level === undefined ||
level <= newProps.maxValue ||
level == newProps.value
);
});
const isCustom = levelRoleMap[newProps.value] === undefined;
@@ -130,7 +134,7 @@ module.exports = React.createClass({
<Field id={`powerSelector_custom_${this.props.powerLevelKey}`} type="number"
label={this.props.label || _t("Power level")} max={this.props.maxValue}
onBlur={this.onCustomBlur} onKeyPress={this.onCustomKeyPress} onChange={this.onCustomChange}
value={this.state.customValue} disabled={this.props.disabled} />
value={String(this.state.customValue)} disabled={this.props.disabled} />
);
} else {
// Each level must have a definition in this.state.levelRoleMap
@@ -148,7 +152,7 @@ module.exports = React.createClass({
picker = (
<Field id={`powerSelector_notCustom_${this.props.powerLevelKey}`} element="select"
label={this.props.label || _t("Power level")} onChange={this.onSelectChange}
value={this.state.selectValue} disabled={this.props.disabled}>
value={String(this.state.selectValue)} disabled={this.props.disabled}>
{options}
</Field>
);

View File

@@ -362,18 +362,22 @@ export default class MessageComposer extends React.Component {
} else if (this.state.tombstone) {
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
const continuesLink = replacementRoomId ? (
<a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this._onTombstoneClick}
>
{_t("The conversation continues here.")}
</a>
) : '';
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
<div className="mx_MessageComposer_replaced_valign">
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
<span className="mx_MessageComposer_roomReplaced_header">
{_t("This room has been replaced and is no longer active.")}
</span><br />
<a href={makeRoomPermalink(replacementRoomId)}
className="mx_MessageComposer_roomReplaced_link"
onClick={this._onTombstoneClick}
>
{_t("The conversation continues here.")}
</a>
{ continuesLink }
</div>
</div>);
} else {

View File

@@ -47,7 +47,7 @@ import {Completion} from "../../../autocomplete/Autocompleter";
import Markdown from '../../../Markdown';
import ComposerHistoryManager from '../../../ComposerHistoryManager';
import MessageComposerStore from '../../../stores/MessageComposerStore';
import ContentMessage from '../../../ContentMessages';
import ContentMessages from '../../../ContentMessages';
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
@@ -139,8 +139,6 @@ export default class MessageComposerInput extends React.Component {
// js-sdk Room object
room: PropTypes.object.isRequired,
onFilesPasted: PropTypes.func,
onInputStateChanged: PropTypes.func,
};
@@ -1014,7 +1012,7 @@ export default class MessageComposerInput extends React.Component {
// neither chrome nor firefox let you paste a plain file copied
// from Finder) but more images copied from a different website
// / word processor etc.
return ContentMessage.sharedInstance().sendContentListToRoom(
return ContentMessages.sharedInstance().sendContentListToRoom(
transfer.files, this.props.room.roomId, this.client,
);
case 'html': {

View File

@@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from "react";
import dis from "../../../dispatcher";
import MatrixClientPeg from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton';
import RoomAvatar from '../avatars/RoomAvatar';
import classNames from 'classnames';
import sdk from "../../../index";
import Analytics from "../../../Analytics";
import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from "../../../utils/FormattingUtils";
import DMRoomMap from "../../../utils/DMRoomMap";
import {_t} from "../../../languageHandler";
const MAX_ROOMS = 20;
@@ -36,46 +41,58 @@ export default class RoomBreadcrumbs extends React.Component {
componentWillMount() {
this._dispatcherRef = dis.register(this.onAction);
const roomStr = localStorage.getItem("mx_breadcrumb_rooms");
if (roomStr) {
try {
const roomIds = JSON.parse(roomStr);
this.setState({
rooms: roomIds.map((r) => {
return {
room: MatrixClientPeg.get().getRoom(r),
animated: false,
};
}).filter((r) => r.room),
});
} catch (e) {
console.error("Failed to parse breadcrumbs:", e);
let storedRooms = SettingsStore.getValue("breadcrumb_rooms");
if (!storedRooms || !storedRooms.length) {
// Fallback to the rooms stored in localstorage for those who would have had this.
// TODO: Remove this after a bit - the feature was only on develop, so a few weeks should be plenty time.
const roomStr = localStorage.getItem("mx_breadcrumb_rooms");
if (roomStr) {
try {
storedRooms = JSON.parse(roomStr);
} catch (e) {
console.error("Failed to parse breadcrumbs:", e);
}
}
}
this._loadRoomIds(storedRooms || []);
this._settingWatchRef = SettingsStore.watchSetting("breadcrumb_rooms", null, this.onBreadcrumbsChanged);
MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
MatrixClientPeg.get().on("Room.receipt", this.onRoomReceipt);
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Event.decrypted", this.onEventDecrypted);
}
componentWillUnmount() {
dis.unregister(this._dispatcherRef);
SettingsStore.unwatchSetting(this._settingWatchRef);
const client = MatrixClientPeg.get();
if (client) client.removeListener("Room.myMembership", this.onMyMembership);
if (client) {
client.removeListener("Room.myMembership", this.onMyMembership);
client.removeListener("Room.receipt", this.onRoomReceipt);
client.removeListener("Room.timeline", this.onRoomTimeline);
client.removeListener("Event.decrypted", this.onEventDecrypted);
}
}
componentDidUpdate() {
const rooms = this.state.rooms.slice();
if (rooms.length) {
const {room, animated} = rooms[0];
if (!animated) {
rooms[0] = {room, animated: true};
const roomModel = rooms[0];
if (!roomModel.animated) {
roomModel.animated = true;
setTimeout(() => this.setState({rooms}), 0);
}
}
const roomStr = JSON.stringify(rooms.map((r) => r.room.roomId));
localStorage.setItem("mx_breadcrumb_rooms", roomStr);
const roomIds = rooms.map((r) => r.room.roomId);
if (roomIds.length > 0) {
SettingsStore.setValue("breadcrumb_rooms", null, SettingLevel.ACCOUNT, roomIds);
}
}
onAction(payload) {
@@ -97,24 +114,142 @@ export default class RoomBreadcrumbs extends React.Component {
}
};
_appendRoomId(roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) {
return;
onRoomReceipt = (event, room) => {
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onRoomTimeline = (event, room) => {
if (this.state.rooms.map(r => r.room.roomId).includes(room.roomId)) {
this._calculateRoomBadges(room);
}
};
onEventDecrypted = (event) => {
if (this.state.rooms.map(r => r.room.roomId).includes(event.getRoomId())) {
this._calculateRoomBadges(MatrixClientPeg.get().getRoom(event.getRoomId()));
}
};
onBreadcrumbsChanged = (settingName, roomId, level, valueAtLevel, value) => {
if (!value) return;
const currentState = this.state.rooms.map((r) => r.room.roomId);
if (currentState.length === value.length) {
let changed = false;
for (let i = 0; i < currentState.length; i++) {
if (currentState[i] !== value[i]) {
changed = true;
break;
}
}
if (!changed) return;
}
this._loadRoomIds(value);
};
_loadRoomIds(roomIds) {
if (!roomIds || roomIds.length <= 0) return; // Skip updates with no rooms
// If we're here, the list changed.
const rooms = roomIds.map((r) => MatrixClientPeg.get().getRoom(r)).filter((r) => r).map((r) => {
const badges = this._calculateBadgesForRoom(r) || {};
return {
room: r,
animated: false,
...badges,
};
});
this.setState({
rooms: rooms,
});
}
_calculateBadgesForRoom(room) {
if (!room) return null;
// Reset the notification variables for simplicity
const roomModel = {
redBadge: false,
formattedCount: "0",
showCount: false,
};
const notifState = RoomNotifs.getRoomNotifsState(room.roomId);
if (RoomNotifs.MENTION_BADGE_STATES.includes(notifState)) {
const highlightNotifs = RoomNotifs.getUnreadNotificationCount(room, 'highlight');
const unreadNotifs = RoomNotifs.getUnreadNotificationCount(room);
const redBadge = highlightNotifs > 0;
const greyBadge = redBadge || (unreadNotifs > 0 && RoomNotifs.BADGE_STATES.includes(notifState));
if (redBadge || greyBadge) {
const notifCount = redBadge ? highlightNotifs : unreadNotifs;
const limitedCount = FormattingUtils.formatCount(notifCount);
roomModel.redBadge = redBadge;
roomModel.formattedCount = limitedCount;
roomModel.showCount = true;
}
}
return roomModel;
}
_calculateRoomBadges(room) {
if (!room) return;
const rooms = this.state.rooms.slice();
const roomModel = rooms.find((r) => r.room.roomId === room.roomId);
if (!roomModel) return; // No applicable room, so don't do math on it
const badges = this._calculateBadgesForRoom(room);
if (!badges) return; // No badges for some reason
Object.assign(roomModel, badges);
this.setState({rooms});
}
_appendRoomId(roomId) {
let room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return;
const rooms = this.state.rooms.slice();
// If the room is upgraded, use that room instead. We'll also splice out
// any children of the room.
const history = MatrixClientPeg.get().getRoomUpgradeHistory(roomId);
if (history.length > 1) {
room = history[history.length - 1]; // Last room is most recent
// Take out any room that isn't the most recent room
for (let i = 0; i < history.length - 1; i++) {
const idx = rooms.findIndex((r) => r.room.roomId === history[i].roomId);
if (idx !== -1) rooms.splice(idx, 1);
}
}
const existingIdx = rooms.findIndex((r) => r.room.roomId === room.roomId);
if (existingIdx !== -1) {
rooms.splice(existingIdx, 1);
}
rooms.splice(0, 0, {room, animated: false});
if (rooms.length > MAX_ROOMS) {
rooms.splice(MAX_ROOMS, rooms.length - MAX_ROOMS);
}
this.setState({rooms});
if (this.refs.scroller) {
this.refs.scroller.moveToOrigin();
}
}
_viewRoom(room) {
_viewRoom(room, index) {
Analytics.trackEvent("Breadcrumbs", "click_node", index);
dis.dispatch({action: "view_room", room_id: room.roomId});
}
@@ -134,17 +269,21 @@ export default class RoomBreadcrumbs extends React.Component {
this.setState({rooms});
}
_isDmRoom(room) {
const dmRooms = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
return Boolean(dmRooms);
}
render() {
const Tooltip = sdk.getComponent('elements.Tooltip');
const IndicatorScrollbar = sdk.getComponent('structures.IndicatorScrollbar');
// check for collapsed here and
// not at parent so we keep
// rooms in our state
// check for collapsed here and not at parent so we keep rooms in our state
// when collapsing and expanding
if (this.props.collapsed) {
return null;
}
const rooms = this.state.rooms;
const avatars = rooms.map((r, i) => {
const isFirst = i === 0;
@@ -160,16 +299,42 @@ export default class RoomBreadcrumbs extends React.Component {
tooltip = <Tooltip label={r.room.name} />;
}
let badge;
if (r.showCount) {
const badgeClasses = classNames({
'mx_RoomTile_badge': true,
'mx_RoomTile_badgeButton': true,
'mx_RoomTile_badgeRed': r.redBadge,
'mx_RoomTile_badgeUnread': !r.redBadge,
});
badge = <div className={badgeClasses}>{r.formattedCount}</div>;
}
let dmIndicator;
if (this._isDmRoom(r.room)) {
dmIndicator = <img
src={require("../../../../res/img/icon_person.svg")}
className="mx_RoomBreadcrumbs_dmIndicator"
width="13"
height="15"
alt={_t("Direct Chat")}
/>;
}
return (
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room)}
<AccessibleButton className={classes} key={r.room.roomId} onClick={() => this._viewRoom(r.room, i)}
onMouseEnter={() => this._onMouseEnter(r.room)} onMouseLeave={() => this._onMouseLeave(r.room)}>
<RoomAvatar room={r.room} width={32} height={32} />
{badge}
{dmIndicator}
{tooltip}
</AccessibleButton>
);
});
return (
<IndicatorScrollbar ref="scroller" className="mx_RoomBreadcrumbs" trackHorizontalOverflow={true}>
<IndicatorScrollbar ref="scroller" className="mx_RoomBreadcrumbs"
trackHorizontalOverflow={true} verticalScrollsHorizontally={true}>
{ avatars }
</IndicatorScrollbar>
);

View File

@@ -68,12 +68,11 @@ module.exports = React.createClass({
},
_shouldShowNotifBadge: function() {
const showBadgeInStates = [RoomNotifs.ALL_MESSAGES, RoomNotifs.ALL_MESSAGES_LOUD];
return showBadgeInStates.indexOf(this.state.notifState) > -1;
return RoomNotifs.BADGE_STATES.includes(this.state.notifState);
},
_shouldShowMentionBadge: function() {
return this.state.notifState !== RoomNotifs.MUTE;
return RoomNotifs.MENTION_BADGE_STATES.includes(this.state.notifState);
},
_isDirectMessageRoom: function(roomId) {

View File

@@ -37,6 +37,8 @@ const STICKERPICKER_Z_INDEX = 3500;
const PERSISTED_ELEMENT_KEY = "stickerPicker";
export default class Stickerpicker extends React.Component {
static currentWidget;
constructor(props) {
super(props);
this._onShowStickersClick = this._onShowStickersClick.bind(this);
@@ -130,8 +132,13 @@ export default class Stickerpicker extends React.Component {
_updateWidget() {
const stickerpickerWidget = WidgetUtils.getStickerpickerWidgets()[0];
if (!stickerpickerWidget) {
Stickerpicker.currentWidget = null;
this.setState({stickerpickerWidget: null, widgetId: null});
return;
}
const currentWidget = this.state.stickerpickerWidget;
const currentWidget = Stickerpicker.currentWidget;
let currentUrl = null;
if (currentWidget && currentWidget.content && currentWidget.content.url) {
currentUrl = currentWidget.content.url;
@@ -147,6 +154,7 @@ export default class Stickerpicker extends React.Component {
PersistedElement.destroyElement(PERSISTED_ELEMENT_KEY);
}
Stickerpicker.currentWidget = stickerpickerWidget;
this.setState({
stickerpickerWidget,
widgetId: stickerpickerWidget ? stickerpickerWidget.id : null,