Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering

This commit is contained in:
Tulir Asokan 2020-06-07 15:12:30 +03:00
commit 4521e9feb1
234 changed files with 6906 additions and 6817 deletions

View file

@ -15,8 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import React, {createRef} from 'react';
import classNames from 'classnames';
import flatMap from 'lodash/flatMap';
import {ICompletion, ISelectionRange, IProviderCompletions} from '../../../autocomplete/Autocompleter';
@ -54,7 +53,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
autocompleter: Autocompleter;
queryRequested: string;
debounceCompletionsRequest: NodeJS.Timeout;
containerRef: React.RefObject<HTMLDivElement>;
private containerRef = createRef<HTMLDivElement>();
constructor(props) {
super(props);
@ -78,8 +77,6 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
forceComplete: false,
};
this.containerRef = React.createRef();
}
componentDidMount() {
@ -256,14 +253,15 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
componentDidUpdate(prevProps: IProps) {
this.applyNewProps(prevProps.query, prevProps.room);
// this is the selected completion, so scroll it into view if needed
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`];
if (selectedCompletion && this.containerRef.current) {
const domNode = ReactDOM.findDOMNode(selectedCompletion);
const offsetTop = domNode && (domNode as HTMLElement).offsetTop;
if (offsetTop > this.containerRef.current.scrollTop + this.containerRef.current.offsetHeight ||
offsetTop < this.containerRef.current.scrollTop) {
this.containerRef.current.scrollTop = offsetTop - this.containerRef.current.offsetTop;
}
const selectedCompletion = this.refs[`completion${this.state.selectionOffset}`] as HTMLElement;
if (selectedCompletion) {
selectedCompletion.scrollIntoView({
behavior: "auto",
block: "nearest",
});
} else if (this.containerRef.current) {
this.containerRef.current.scrollTo({ top: 0 });
}
}

View file

@ -141,15 +141,6 @@ export default createReactClass({
return counters;
},
_onScroll: function(rect) {
if (this.props.onResize) {
this.props.onResize();
}
/* Force refresh of PersistedElements which may be partially hidden */
window.dispatchEvent(new Event('resize'));
},
render: function() {
const CallView = sdk.getComponent("voip.CallView");
const TintableSvg = sdk.getComponent("elements.TintableSvg");
@ -274,7 +265,7 @@ export default createReactClass({
}
return (
<AutoHideScrollbar className={classes} style={style} onScroll={this._onScroll}>
<AutoHideScrollbar className={classes} style={style} >
{ stateViews }
{ appsDrawer }
{ fileDropTarget }

View file

@ -74,6 +74,7 @@ function selectionEquals(a: Selection, b: Selection): boolean {
export default class BasicMessageEditor extends React.Component {
static propTypes = {
onChange: PropTypes.func,
onPaste: PropTypes.func, // returns true if handled and should skip internal onPaste handler
model: PropTypes.instanceOf(EditorModel).isRequired,
room: PropTypes.instanceOf(Room).isRequired,
placeholder: PropTypes.string,
@ -254,6 +255,12 @@ export default class BasicMessageEditor extends React.Component {
}
_onPaste = (event) => {
event.preventDefault(); // we always handle the paste ourselves
if (this.props.onPaste && this.props.onPaste(event, this.props.model)) {
// to prevent double handling, allow props.onPaste to skip internal onPaste
return true;
}
const {model} = this.props;
const {partCreator} = model;
const partsText = event.clipboardData.getData("application/x-riot-composer");
@ -269,7 +276,6 @@ export default class BasicMessageEditor extends React.Component {
this._modifiedFlag = true;
const range = getRangeForSelection(this._editorRef, model, document.getSelection());
replaceRangeAndMoveCaret(range, parts);
event.preventDefault();
}
_onInput = (event) => {
@ -353,6 +359,8 @@ export default class BasicMessageEditor extends React.Component {
}
_onSelectionChange = () => {
const {isEmpty} = this.props.model;
this._refreshLastCaretIfNeeded();
const selection = document.getSelection();
if (this._hasTextSelected && selection.isCollapsed) {
@ -360,7 +368,7 @@ export default class BasicMessageEditor extends React.Component {
if (this._formatBarRef) {
this._formatBarRef.hide();
}
} else if (!selection.isCollapsed) {
} else if (!selection.isCollapsed && !isEmpty) {
this._hasTextSelected = true;
if (this._formatBarRef) {
const selectionRect = selection.getRangeAt(0).getBoundingClientRect();
@ -503,10 +511,6 @@ export default class BasicMessageEditor extends React.Component {
}
}
getEditableRootNode() {
return this._editorRef;
}
isModified() {
return this._modifiedFlag;
}

View file

@ -20,7 +20,6 @@ import PropTypes from "prop-types";
import classNames from 'classnames';
import {_t, _td} from '../../../languageHandler';
import {useSettingValue} from "../../../hooks/useSettings";
import AccessibleButton from "../elements/AccessibleButton";
import Tooltip from "../elements/Tooltip";
@ -42,15 +41,6 @@ const crossSigningRoomTitles = {
[E2E_STATE.VERIFIED]: _td("Everyone in this room is verified"),
};
const legacyUserTitles = {
[E2E_STATE.WARNING]: _td("Some sessions for this user are not trusted"),
[E2E_STATE.VERIFIED]: _td("All sessions for this user are trusted"),
};
const legacyRoomTitles = {
[E2E_STATE.WARNING]: _td("Some sessions in this encrypted room are not trusted"),
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
};
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
const [hover, setHover] = useState(false);
@ -62,15 +52,10 @@ const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
}, className);
let e2eTitle;
const crossSigning = useSettingValue("feature_cross_signing");
if (crossSigning && isUser) {
if (isUser) {
e2eTitle = crossSigningUserTitles[status];
} else if (crossSigning && !isUser) {
} else {
e2eTitle = crossSigningRoomTitles[status];
} else if (!crossSigning && isUser) {
e2eTitle = legacyUserTitles[status];
} else if (!crossSigning && !isUser) {
e2eTitle = legacyRoomTitles[status];
}
let style;

View file

@ -31,6 +31,7 @@ import {EventStatus} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer";
import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
function _isReply(mxEvent) {
const relatesTo = mxEvent.getContent()["m.relates_to"];
@ -157,7 +158,7 @@ export default class EditMessageComposer extends React.Component {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
}
@ -165,7 +166,7 @@ export default class EditMessageComposer extends React.Component {
_cancelEdit = () => {
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
}
_isContentModified(newContent) {
@ -190,11 +191,12 @@ export default class EditMessageComposer extends React.Component {
const roomId = editedEvent.getRoomId();
this._cancelPreviousPendingEdit();
this.context.sendMessage(roomId, editContent);
dis.dispatch({action: "message_sent"});
}
// close the event editing and focus composer
dis.dispatch({action: "edit_event", event: null});
dis.dispatch({action: 'focus_composer'});
dis.fire(Action.FocusComposer);
};
_cancelPreviousPendingEdit() {

View file

@ -104,7 +104,7 @@ export function getHandlerTile(ev) {
// fall back to showing hidden events, if we're viewing hidden events
// XXX: This is extremely a hack. Possibly these components should have an interface for
// declining to render?
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
if (type === "m.key.verification.cancel" || type === "m.key.verification.done") {
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
return;
@ -325,15 +325,6 @@ export default createReactClass({
return;
}
// If cross-signing is off, the old behaviour is to scream at the user
// as if they've done something wrong, which they haven't
if (!SettingsStore.getValue("feature_cross_signing")) {
this.setState({
verified: E2E_STATE.WARNING,
}, this.props.onHeightChanged);
return;
}
if (!this.context.checkUserTrust(mxEvent.getSender()).isCrossSigningVerified()) {
this.setState({
verified: E2E_STATE.NORMAL,
@ -403,7 +394,7 @@ export default createReactClass({
},
shouldHighlight: function() {
const actions = this.context.getPushActionsForEvent(this.props.mxEvent);
const actions = this.context.getPushActionsForEvent(this.props.mxEvent.replacingEvent() || this.props.mxEvent);
if (!actions || !actions.tweaks) { return false; }
// don't show self-highlights from another of our clients
@ -802,6 +793,8 @@ export default createReactClass({
const groupTimestamp = !this.props.useIRCLayout ? linkedTimestamp : null;
const ircTimestamp = this.props.useIRCLayout ? linkedTimestamp : null;
const groupPadlock = !this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
const ircPadlock = this.props.useIRCLayout && !isBubbleMessage && this._renderE2EPadlock();
switch (this.props.tileShape) {
case 'notif': {
@ -873,9 +866,10 @@ export default createReactClass({
{ ircTimestamp }
{ avatar }
{ sender }
{ ircPadlock }
<div className="mx_EventTile_reply">
{ groupTimestamp }
{ !isBubbleMessage && this._renderE2EPadlock() }
{ groupPadlock }
{ thread }
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}
@ -904,9 +898,10 @@ export default createReactClass({
{ readAvatars }
</div>
{ sender }
{ ircPadlock }
<div className="mx_EventTile_line">
{ groupTimestamp }
{ !isBubbleMessage && this._renderE2EPadlock() }
{ groupPadlock }
{ thread }
<EventTileType ref={this._tile}
mxEvent={this.props.mxEvent}

View file

@ -1,59 +0,0 @@
/*
Copyright 2016 OpenMarket 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 PropTypes from 'prop-types';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import classNames from 'classnames';
export default class MemberDeviceInfo extends React.Component {
render() {
const DeviceVerifyButtons = sdk.getComponent('elements.DeviceVerifyButtons');
// XXX: These checks are not cross-signing aware but this component is only used
// from the old, pre-cross-signing memberinfopanel
const iconClasses = classNames({
mx_MemberDeviceInfo_icon: true,
mx_MemberDeviceInfo_icon_blacklisted: this.props.device.isBlocked(),
mx_MemberDeviceInfo_icon_verified: this.props.device.isVerified(),
mx_MemberDeviceInfo_icon_unverified: this.props.device.isUnverified(),
});
const indicator = (<div className={iconClasses} />);
const deviceName = (this.props.device.ambiguous || this.props.showDeviceId) ?
(this.props.device.getDisplayName() ? this.props.device.getDisplayName() : "") + " (" + this.props.device.deviceId + ")" :
this.props.device.getDisplayName();
// add the deviceId as a titletext to help with debugging
return (
<div className="mx_MemberDeviceInfo"
title={_t("device id: ") + this.props.device.deviceId} >
{ indicator }
<div className="mx_MemberDeviceInfo_deviceInfo">
<div className="mx_MemberDeviceInfo_deviceId">
{ deviceName }
</div>
</div>
<DeviceVerifyButtons userId={this.props.userId} device={this.props.device} />
</div>
);
}
}
MemberDeviceInfo.displayName = 'MemberDeviceInfo';
MemberDeviceInfo.propTypes = {
userId: PropTypes.string.isRequired,
device: PropTypes.object.isRequired,
};

File diff suppressed because it is too large Load diff

View file

@ -57,21 +57,19 @@ export default createReactClass({
}
}
if (SettingsStore.getValue("feature_cross_signing")) {
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
this.setState({
isRoomEncrypted,
});
if (isRoomEncrypted) {
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.updateE2EStatus();
} else {
// Listen for room to become encrypted
cli.on("RoomState.events", this.onRoomStateEvents);
}
const { roomId } = this.props.member;
if (roomId) {
const isRoomEncrypted = cli.isRoomEncrypted(roomId);
this.setState({
isRoomEncrypted,
});
if (isRoomEncrypted) {
cli.on("userTrustStatusChanged", this.onUserTrustStatusChanged);
cli.on("deviceVerificationChanged", this.onDeviceVerificationChanged);
this.updateE2EStatus();
} else {
// Listen for room to become encrypted
cli.on("RoomState.events", this.onRoomStateEvents);
}
}
},

View file

@ -27,6 +27,7 @@ import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
import {aboveLeftOf, ContextMenu, ContextMenuButton, useContextMenu} from "../../structures/ContextMenu";
function ComposerAvatar(props) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
@ -103,6 +104,32 @@ HangupButton.propTypes = {
roomId: PropTypes.string.isRequired,
};
const EmojiButton = ({addEmoji}) => {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu();
let contextMenu;
if (menuDisplayed) {
const buttonRect = button.current.getBoundingClientRect();
const EmojiPicker = sdk.getComponent('emojipicker.EmojiPicker');
contextMenu = <ContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu} catchTab={false}>
<EmojiPicker onChoose={addEmoji} showQuickReactions={true} />
</ContextMenu>;
}
return <React.Fragment>
<ContextMenuButton className="mx_MessageComposer_button mx_MessageComposer_emoji"
onClick={openMenu}
isExpanded={menuDisplayed}
label={_t('Emoji picker')}
inputRef={button}
>
</ContextMenuButton>
{ contextMenu }
</React.Fragment>;
};
class UploadButton extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
@ -281,37 +308,28 @@ export default class MessageComposer extends React.Component {
}
renderPlaceholderText() {
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
return _t('Send a reply…');
}
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
return _t('Send a message…');
}
return _t('Send a reply…');
}
} else {
if (this.state.isQuoting) {
if (this.props.e2eStatus) {
return _t('Send an encrypted reply…');
} else {
return _t('Send a reply (unencrypted)…');
}
if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
if (this.props.e2eStatus) {
return _t('Send an encrypted message…');
} else {
return _t('Send a message (unencrypted)…');
}
return _t('Send a message…');
}
}
}
addEmoji(emoji) {
dis.dispatch({
action: "insert_emoji",
emoji,
});
}
render() {
const controls = [
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
@ -335,8 +353,9 @@ export default class MessageComposer extends React.Component {
room={this.props.room}
placeholder={this.renderPlaceholderText()}
permalinkCreator={this.props.permalinkCreator} />,
<Stickerpicker key='stickerpicker_controls_button' room={this.props.room} />,
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />,
);
if (this.state.showCallButtons) {

View file

@ -168,10 +168,8 @@ export default createReactClass({
const joinRule = joinRules && joinRules.getContent().join_rule;
let privateIcon;
// Don't show an invite-only icon for DMs. Users know they're invite-only.
if (!dmUserId && SettingsStore.getValue("feature_cross_signing")) {
if (joinRule == "invite") {
privateIcon = <InviteOnlyIcon />;
}
if (!dmUserId && joinRule === "invite") {
privateIcon = <InviteOnlyIcon />;
}
if (this.props.onCancelClick) {

View file

@ -18,16 +18,17 @@ limitations under the License.
import * as React from "react";
import { _t, _td } from "../../../languageHandler";
import { Layout } from '../../../resizer/distributors/roomsublist2';
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore2";
import RoomListStore, { LISTS_UPDATE_EVENT, RoomListStore2 } from "../../../stores/room-list/RoomListStore2";
import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { Dispatcher } from "flux";
import dis from "../../../dispatcher/dispatcher";
import RoomSublist2 from "./RoomSublist2";
import { ActionPayload } from "../../../dispatcher/payloads";
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
import { ListLayout } from "../../../stores/room-list/ListLayout";
/*******************************************************************
* CAUTION *
@ -48,6 +49,7 @@ interface IProps {
interface IState {
sublists: ITagMap;
layouts: Map<TagID, ListLayout>;
}
const TAG_ORDER: TagID[] = [
@ -125,64 +127,43 @@ const TAG_AESTHETICS: {
};
export default class RoomList2 extends React.Component<IProps, IState> {
private sublistRefs: { [tagId: string]: React.RefObject<RoomSublist2> } = {};
private sublistSizes: { [tagId: string]: number } = {};
private sublistCollapseStates: { [tagId: string]: boolean } = {};
private unfilteredLayout: Layout;
private filteredLayout: Layout;
private searchFilter: NameFilterCondition = new NameFilterCondition();
constructor(props: IProps) {
super(props);
this.state = {sublists: {}};
this.loadSublistSizes();
this.prepareLayouts();
this.state = {
sublists: {},
layouts: new Map<TagID, ListLayout>(),
};
}
public componentDidUpdate(prevProps: Readonly<IProps>): void {
if (prevProps.searchFilter !== this.props.searchFilter) {
const hadSearch = !!this.searchFilter.search.trim();
const haveSearch = !!this.props.searchFilter.trim();
this.searchFilter.search = this.props.searchFilter;
if (!hadSearch && haveSearch) {
// started a new filter - add the condition
RoomListStore.instance.addFilter(this.searchFilter);
} else if (hadSearch && !haveSearch) {
// cleared a filter - remove the condition
RoomListStore.instance.removeFilter(this.searchFilter);
} // else the filter hasn't changed enough for us to care here
}
}
public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
console.log("new lists", store.orderedLists);
this.setState({sublists: store.orderedLists});
});
}
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store: RoomListStore2) => {
const newLists = store.orderedLists;
console.log("new lists", newLists);
private loadSublistSizes() {
const sizesJson = window.localStorage.getItem("mx_roomlist_sizes");
if (sizesJson) this.sublistSizes = JSON.parse(sizesJson);
const collapsedJson = window.localStorage.getItem("mx_roomlist_collapsed");
if (collapsedJson) this.sublistCollapseStates = JSON.parse(collapsedJson);
}
private saveSublistSizes() {
window.localStorage.setItem("mx_roomlist_sizes", JSON.stringify(this.sublistSizes));
window.localStorage.setItem("mx_roomlist_collapsed", JSON.stringify(this.sublistCollapseStates));
}
private prepareLayouts() {
// TODO: Change layout engine for FTUE support
this.unfilteredLayout = new Layout((tagId: string, height: number) => {
const sublist = this.sublistRefs[tagId];
if (sublist) sublist.current.setHeight(height);
// TODO: Check overflow (see old impl)
// Don't store a height for collapsed sublists
if (!this.sublistCollapseStates[tagId]) {
this.sublistSizes[tagId] = height;
this.saveSublistSizes();
const layoutMap = new Map<TagID, ListLayout>();
for (const tagId of Object.keys(newLists)) {
layoutMap.set(tagId, new ListLayout(tagId));
}
}, this.sublistSizes, this.sublistCollapseStates, {
allowWhitespace: false,
handleHeight: 1,
});
this.filteredLayout = new Layout((tagId: string, height: number) => {
const sublist = this.sublistRefs[tagId];
if (sublist) sublist.current.setHeight(height);
}, null, null, {
allowWhitespace: false,
handleHeight: 0,
this.setState({sublists: newLists, layouts: layoutMap});
});
}
@ -208,16 +189,19 @@ export default class RoomList2 extends React.Component<IProps, IState> {
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
components.push(<RoomSublist2
key={`sublist-${orderedTagId}`}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite}
/>);
components.push(
<RoomSublist2
key={`sublist-${orderedTagId}`}
forRooms={true}
rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
isInvite={aesthetics.isInvite}
layout={this.state.layouts.get(orderedTagId)}
/>
);
}
return components;
@ -232,7 +216,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
onFocus={this.props.onFocus}
onBlur={this.props.onBlur}
onKeyDown={onKeyDownHandler}
className="mx_RoomList"
className="mx_RoomList mx_RoomList2"
role="tree"
aria-label={_t("Rooms")}
// Firefox sometimes makes this element focusable due to

View file

@ -20,7 +20,6 @@ import * as React from "react";
import { createRef } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import classNames from 'classnames';
import IndicatorScrollbar from "../../structures/IndicatorScrollbar";
import * as RoomNotifs from '../../../RoomNotifs';
import { RovingTabIndexWrapper } from "../../../accessibility/RovingTabIndex";
import { _t } from "../../../languageHandler";
@ -28,6 +27,8 @@ import AccessibleButton from "../../views/elements/AccessibleButton";
import AccessibleTooltipButton from "../../views/elements/AccessibleTooltipButton";
import * as FormattingUtils from '../../../utils/FormattingUtils';
import RoomTile2 from "./RoomTile2";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
import { ListLayout } from "../../../stores/room-list/ListLayout";
/*******************************************************************
* CAUTION *
@ -45,9 +46,9 @@ interface IProps {
onAddRoom?: () => void;
addRoomLabel: string;
isInvite: boolean;
layout: ListLayout;
// TODO: Collapsed state
// TODO: Height
// TODO: Group invites
// TODO: Calls
// TODO: forceExpand?
@ -61,10 +62,6 @@ interface IState {
export default class RoomSublist2 extends React.Component<IProps, IState> {
private headerButton = createRef();
public setHeight(size: number) {
// TODO: Do a thing (maybe - height changes are different in FTUE)
}
private hasTiles(): boolean {
return this.numTiles > 0;
}
@ -79,6 +76,18 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (this.props.onAddRoom) this.props.onAddRoom();
};
private onResize = (e: React.MouseEvent, data: ResizeCallbackData) => {
const direction = e.movementY < 0 ? -1 : +1;
const tileDiff = this.props.layout.pixelsToTiles(Math.abs(e.movementY)) * direction;
this.props.layout.visibleTiles += tileDiff;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private onShowAllClick = () => {
this.props.layout.visibleTiles = this.numTiles;
this.forceUpdate(); // because the layout doesn't trigger a re-render
};
private renderTiles(): React.ReactElement[] {
const tiles: React.ReactElement[] = [];
@ -204,10 +213,57 @@ export default class RoomSublist2 extends React.Component<IProps, IState> {
if (tiles.length > 0) {
// TODO: Lazy list rendering
// TODO: Whatever scrolling magic needs to happen here
const layout = this.props.layout; // to shorten calls
const minTilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.minVisibleTiles));
const maxTilesPx = layout.tilesToPixels(tiles.length);
const tilesPx = layout.tilesToPixels(Math.min(tiles.length, layout.visibleTiles));
let handles = ['s'];
if (layout.visibleTiles >= tiles.length && tiles.length <= layout.minVisibleTiles) {
handles = []; // no handles, we're at a minimum
}
// TODO: This might need adjustment, however for now it is fine as a round.
const nVisible = Math.round(layout.visibleTiles);
const visibleTiles = tiles.slice(0, nVisible);
// If we're hiding rooms, show a 'show more' button to the user. This button
// replaces the last visible tile, so will always show 2+ rooms. We do this
// because if it said "show 1 more room" we had might as well show that room
// instead. We also replace the last item so we don't have to adjust our math
// on pixel heights, etc. It's much easier to pretend the button is a tile.
if (tiles.length > nVisible) {
// we have a cutoff condition - add the button to show all
// we +1 to account for the room we're about to hide with our 'show more' button
// this results in the button always being 1+, and not needing an i18n `count`.
const numMissing = (tiles.length - visibleTiles.length) + 1;
// TODO: CSS TBD
// TODO: Make this an actual tile
// TODO: This is likely to pop out of the list, consider that.
visibleTiles.splice(visibleTiles.length - 1, 1, (
<div
onClick={this.onShowAllClick}
style={{height: '34px', lineHeight: '34px', cursor: 'pointer'}}
key='showall'
>
{_t("Show %(n)s more", {n: numMissing})}
</div>
));
}
content = (
<IndicatorScrollbar className='mx_RoomSubList_scroll'>
{tiles}
</IndicatorScrollbar>
<ResizableBox
width={-1}
height={tilesPx}
axis="y"
minConstraints={[-1, minTilesPx]}
maxConstraints={[-1, maxTilesPx]}
resizeHandles={handles}
onResize={this.onResize}
className="mx_RoomSublist2_resizeBox"
>
{visibleTiles}
</ResizableBox>
)
}

View file

@ -155,9 +155,6 @@ export default createReactClass({
if (!cli.isRoomEncrypted(this.props.room.roomId)) {
return;
}
if (!SettingsStore.getValue("feature_cross_signing")) {
return;
}
/* At this point, the user has encryption on and cross-signing on */
this.setState({
@ -515,10 +512,8 @@ export default createReactClass({
}
let privateIcon = null;
if (SettingsStore.getValue("feature_cross_signing")) {
if (this.state.joinRule == "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}
if (this.state.joinRule === "invite" && !dmUserId) {
privateIcon = <InviteOnlyIcon collapsedPanel={this.props.collapsed} />;
}
let e2eIcon = null;

View file

@ -30,6 +30,8 @@ import * as RoomNotifs from '../../../RoomNotifs';
import { EffectiveMembership, getEffectiveMembership } from "../../../stores/room-list/membership";
import * as Unread from '../../../Unread';
import * as FormattingUtils from "../../../utils/FormattingUtils";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
/*******************************************************************
* CAUTION *
@ -86,10 +88,22 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
hover: false,
notificationState: this.getNotificationState(),
};
this.props.room.on("Room.receipt", this.handleRoomEventUpdate);
this.props.room.on("Room.timeline", this.handleRoomEventUpdate);
this.props.room.on("Room.redaction", this.handleRoomEventUpdate);
MatrixClientPeg.get().on("Event.decrypted", this.handleRoomEventUpdate);
}
public componentWillUnmount() {
// TODO: Listen for changes to the badge count and update as needed
if (this.props.room) {
this.props.room.removeListener("Room.receipt", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.timeline", this.handleRoomEventUpdate);
this.props.room.removeListener("Room.redaction", this.handleRoomEventUpdate);
}
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Event.decrypted", this.handleRoomEventUpdate);
}
}
// XXX: This is a bit of an awful-looking hack. We should probably be using state for
@ -99,7 +113,15 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
return getEffectiveMembership(this.props.room.getMyMembership()) === EffectiveMembership.Invite;
}
// TODO: Make use of this function when the notification state needs updating.
private handleRoomEventUpdate = (event: MatrixEvent) => {
const roomId = event.getRoomId();
// Sanity check: should never happen
if (roomId !== this.props.room.roomId) return;
this.updateNotificationState();
};
private updateNotificationState() {
this.setState({notificationState: this.getNotificationState()});
}
@ -214,7 +236,7 @@ export default class RoomTile2 extends React.Component<IProps, IState> {
let tooltip = null;
if (false) { // isCollapsed
if (this.state.hover) {
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} dir="auto"/>
tooltip = <Tooltip className="mx_RoomTile_tooltip" label={this.props.room.name} />
}
}

View file

@ -44,6 +44,7 @@ import {Key} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -312,6 +313,7 @@ export default class SendMessageComposer extends React.Component {
event: null,
});
}
dis.dispatch({action: "message_sent"});
}
this.sendHistoryManager.save(this.model);
@ -322,13 +324,8 @@ export default class SendMessageComposer extends React.Component {
this._clearStoredEditorState();
}
componentDidMount() {
this._editorRef.getEditableRootNode().addEventListener("paste", this._onPaste, true);
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
this._editorRef.getEditableRootNode().removeEventListener("paste", this._onPaste, true);
}
// TODO: [REACT-WARNING] Move this to constructor
@ -368,7 +365,7 @@ export default class SendMessageComposer extends React.Component {
onAction = (payload) => {
switch (payload.action) {
case 'reply_to_event':
case 'focus_composer':
case Action.FocusComposer:
this._editorRef && this._editorRef.focus();
break;
case 'insert_mention':
@ -377,6 +374,9 @@ export default class SendMessageComposer extends React.Component {
case 'quote':
this._insertQuotedMessage(payload.event);
break;
case 'insert_emoji':
this._insertEmoji(payload.emoji);
break;
}
};
@ -414,6 +414,17 @@ export default class SendMessageComposer extends React.Component {
this._editorRef && this._editorRef.focus();
}
_insertEmoji = (emoji) => {
const {model} = this;
const {partCreator} = model;
const caret = this._editorRef.getCaret();
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
model.transform(() => {
const addedLen = model.insert([partCreator.plain(emoji)], position);
return model.positionForOffset(caret.offset + addedLen, true);
});
};
_onPaste = (event) => {
const {clipboardData} = event;
if (clipboardData.files.length) {
@ -424,6 +435,7 @@ export default class SendMessageComposer extends React.Component {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(clipboardData.files), this.props.room.roomId, this.context,
);
return true; // to skip internal onPaste handler
}
}
@ -440,6 +452,7 @@ export default class SendMessageComposer extends React.Component {
label={this.props.placeholder}
placeholder={this.props.placeholder}
onChange={this._saveStoredEditorState}
onPaste={this._onPaste}
/>
</div>
);