Merge branch 'develop' into matthew/retina
This commit is contained in:
commit
15ba24f7fb
64 changed files with 1283 additions and 546 deletions
|
@ -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 }
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue