mirror of
https://github.com/element-hq/element-web
synced 2024-11-27 11:47:23 +03:00
Merge branch 'develop' into gsouquet/switch-rooms
This commit is contained in:
commit
d894cc6f7a
13 changed files with 216 additions and 122 deletions
|
@ -61,6 +61,7 @@ limitations under the License.
|
||||||
&.mx_RoomSublist_headerContainer_sticky {
|
&.mx_RoomSublist_headerContainer_sticky {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 32px; // to match the header container
|
height: 32px; // to match the header container
|
||||||
|
// width set by JS because of a compat issue between Firefox and Chrome
|
||||||
width: calc(100% - 15px);
|
width: calc(100% - 15px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
src/@types/global.d.ts
vendored
|
@ -43,6 +43,7 @@ import TypingStore from "../stores/TypingStore";
|
||||||
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
||||||
import PerformanceMonitor from "../performance";
|
import PerformanceMonitor from "../performance";
|
||||||
|
import UIStore from "../stores/UIStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -82,6 +83,7 @@ declare global {
|
||||||
mxEventIndexPeg: EventIndexPeg;
|
mxEventIndexPeg: EventIndexPeg;
|
||||||
mxPerformanceMonitor: PerformanceMonitor;
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
mxPerformanceEntryNames: any;
|
mxPerformanceEntryNames: any;
|
||||||
|
mxUIStore: UIStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
|
|
@ -462,6 +462,9 @@ export default class CallHandler extends EventEmitter {
|
||||||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||||
title = _t("Call Declined");
|
title = _t("Call Declined");
|
||||||
description = _t("The other party declined the call.");
|
description = _t("The other party declined the call.");
|
||||||
|
} else if (call.hangupReason === CallErrorCode.UserBusy) {
|
||||||
|
title = _t("User Busy");
|
||||||
|
description = _t("The user you called is busy.");
|
||||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||||
title = _t("Call Failed");
|
title = _t("Call Failed");
|
||||||
// XXX: full stop appended as some relic here, but these
|
// XXX: full stop appended as some relic here, but these
|
||||||
|
|
|
@ -67,6 +67,7 @@ const cssClasses = [
|
||||||
|
|
||||||
@replaceableComponent("structures.LeftPanel")
|
@replaceableComponent("structures.LeftPanel")
|
||||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
private ref: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private groupFilterPanelWatcherRef: string;
|
private groupFilterPanelWatcherRef: string;
|
||||||
private bgImageWatcherRef: string;
|
private bgImageWatcherRef: string;
|
||||||
|
@ -93,6 +94,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current);
|
||||||
|
UIStore.instance.on("ListContainer", this.refreshStickyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.groupFilterPanelWatcherRef);
|
||||||
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
||||||
|
@ -100,6 +106,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
|
||||||
|
UIStore.instance.stopTrackingElementDimensions("ListContainer");
|
||||||
|
UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate(prevProps: IProps, prevState: IState): void {
|
||||||
|
if (prevState.activeSpace !== this.state.activeSpace) {
|
||||||
|
this.refreshStickyHeaders();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateActiveSpace = (activeSpace: Room) => {
|
private updateActiveSpace = (activeSpace: Room) => {
|
||||||
|
@ -245,10 +259,24 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (!header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
header.classList.add("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const listDimensions = UIStore.instance.getElementDimensions("ListContainer");
|
||||||
|
if (listDimensions) {
|
||||||
|
const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
|
||||||
|
const headerStickyWidth = listDimensions.width - headerRightMargin;
|
||||||
|
const newWidth = `${headerStickyWidth}px`;
|
||||||
|
if (header.style.width !== newWidth) {
|
||||||
|
header.style.width = newWidth;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (!style.stickyTop && !style.stickyBottom) {
|
} else if (!style.stickyTop && !style.stickyBottom) {
|
||||||
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
if (header.classList.contains("mx_RoomSublist_headerContainer_sticky")) {
|
||||||
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
header.classList.remove("mx_RoomSublist_headerContainer_sticky");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (header.style.width) {
|
||||||
|
header.style.removeProperty('width');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -407,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
activeSpace={this.state.activeSpace}
|
activeSpace={this.state.activeSpace}
|
||||||
|
onListCollapse={this.refreshStickyHeaders}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
|
@ -420,7 +449,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses} ref={this.ref}>
|
||||||
{leftLeftPanel}
|
{leftLeftPanel}
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
|
|
|
@ -232,6 +232,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
private accountPasswordTimer?: NodeJS.Timeout;
|
private accountPasswordTimer?: NodeJS.Timeout;
|
||||||
private focusComposer: boolean;
|
private focusComposer: boolean;
|
||||||
private subTitleStatus: string;
|
private subTitleStatus: string;
|
||||||
|
private prevWindowWidth: number;
|
||||||
|
|
||||||
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
private readonly loggedInView: React.RefObject<LoggedInViewType>;
|
||||||
private readonly dispatcherRef: any;
|
private readonly dispatcherRef: any;
|
||||||
|
@ -277,6 +278,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.prevWindowWidth = UIStore.instance.windowWidth || 1000;
|
||||||
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
UIStore.instance.on(UI_EVENTS.Resize, this.handleResize);
|
||||||
|
|
||||||
this.pageChanging = false;
|
this.pageChanging = false;
|
||||||
|
@ -1821,13 +1823,15 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
const LHS_THRESHOLD = 1000;
|
const LHS_THRESHOLD = 1000;
|
||||||
const width = UIStore.instance.windowWidth;
|
const width = UIStore.instance.windowWidth;
|
||||||
|
|
||||||
if (width <= LHS_THRESHOLD && !this.state.collapseLhs) {
|
if (this.prevWindowWidth < LHS_THRESHOLD && width >= LHS_THRESHOLD) {
|
||||||
dis.dispatch({ action: 'hide_left_panel' });
|
|
||||||
}
|
|
||||||
if (width > LHS_THRESHOLD && this.state.collapseLhs) {
|
|
||||||
dis.dispatch({ action: 'show_left_panel' });
|
dis.dispatch({ action: 'show_left_panel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.prevWindowWidth >= LHS_THRESHOLD && width < LHS_THRESHOLD) {
|
||||||
|
dis.dispatch({ action: 'hide_left_panel' });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.prevWindowWidth = width;
|
||||||
this.state.resizeNotifier.notifyWindowResized();
|
this.state.resizeNotifier.notifyWindowResized();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -319,7 +319,7 @@ export const HierarchyLevel = ({
|
||||||
key={roomId}
|
key={roomId}
|
||||||
room={rooms.get(roomId)}
|
room={rooms.get(roomId)}
|
||||||
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
numChildRooms={Array.from(relations.get(roomId)?.values() || [])
|
||||||
.filter(ev => rooms.get(ev.state_key)?.room_type !== RoomType.Space).length}
|
.filter(ev => rooms.has(ev.state_key) && !rooms.get(ev.state_key).room_type).length}
|
||||||
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
suggested={relations.get(spaceId)?.get(roomId)?.content.suggested}
|
||||||
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
selected={selectedMap?.get(spaceId)?.has(roomId)}
|
||||||
onViewRoomClick={(autoJoin) => {
|
onViewRoomClick={(autoJoin) => {
|
||||||
|
@ -437,7 +437,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
|
|
||||||
let content;
|
let content;
|
||||||
if (roomsMap) {
|
if (roomsMap) {
|
||||||
const numRooms = Array.from(roomsMap.values()).filter(r => r.room_type !== RoomType.Space).length;
|
const numRooms = Array.from(roomsMap.values()).filter(r => !r.room_type).length;
|
||||||
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
const numSpaces = roomsMap.size - numRooms - 1; // -1 at the end to exclude the space we are looking at
|
||||||
|
|
||||||
let countsStr;
|
let countsStr;
|
||||||
|
|
|
@ -47,10 +47,18 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
import {getAddressType} from "../../../UserAddress";
|
import {getAddressType} from "../../../UserAddress";
|
||||||
|
import BaseAvatar from '../avatars/BaseAvatar';
|
||||||
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
|
|
||||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
|
interface IRecentUser {
|
||||||
|
userId: string,
|
||||||
|
user: RoomMember,
|
||||||
|
lastActive: number,
|
||||||
|
}
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||||
|
@ -61,43 +69,41 @@ const INCREMENT_ROOMS_SHOWN = 5; // Number of rooms to add when 'show more' is c
|
||||||
// This is the interface that is expected by various components in this file. It is a bit
|
// This is the interface that is expected by various components in this file. It is a bit
|
||||||
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
// awkward because it also matches the RoomMember class from the js-sdk with some extra support
|
||||||
// for 3PIDs/email addresses.
|
// for 3PIDs/email addresses.
|
||||||
//
|
abstract class Member {
|
||||||
// XXX: We should use TypeScript interfaces instead of this weird "abstract" class.
|
|
||||||
class Member {
|
|
||||||
/**
|
/**
|
||||||
* The display name of this Member. For users this should be their profile's display
|
* The display name of this Member. For users this should be their profile's display
|
||||||
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
* name or user ID if none set. For 3PIDs this should be the 3PID address (email).
|
||||||
*/
|
*/
|
||||||
get name(): string { throw new Error("Member class not implemented"); }
|
public abstract get name(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
* The ID of this Member. For users this should be their user ID. For 3PIDs this should
|
||||||
* be the 3PID address (email).
|
* be the 3PID address (email).
|
||||||
*/
|
*/
|
||||||
get userId(): string { throw new Error("Member class not implemented"); }
|
public abstract get userId(): string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
* Gets the MXC URL of this Member's avatar. For users this should be their profile's
|
||||||
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
* avatar MXC URL or null if none set. For 3PIDs this should always be null.
|
||||||
*/
|
*/
|
||||||
getMxcAvatarUrl(): string { throw new Error("Member class not implemented"); }
|
public abstract getMxcAvatarUrl(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DirectoryMember extends Member {
|
class DirectoryMember extends Member {
|
||||||
_userId: string;
|
private readonly _userId: string;
|
||||||
_displayName: string;
|
private readonly displayName: string;
|
||||||
_avatarUrl: string;
|
private readonly avatarUrl: string;
|
||||||
|
|
||||||
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
constructor(userDirResult: {user_id: string, display_name: string, avatar_url: string}) {
|
||||||
super();
|
super();
|
||||||
this._userId = userDirResult.user_id;
|
this._userId = userDirResult.user_id;
|
||||||
this._displayName = userDirResult.display_name;
|
this.displayName = userDirResult.display_name;
|
||||||
this._avatarUrl = userDirResult.avatar_url;
|
this.avatarUrl = userDirResult.avatar_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
// These next class members are for the Member interface
|
// These next class members are for the Member interface
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this._displayName || this._userId;
|
return this.displayName || this._userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
get userId(): string {
|
get userId(): string {
|
||||||
|
@ -105,32 +111,32 @@ class DirectoryMember extends Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMxcAvatarUrl(): string {
|
getMxcAvatarUrl(): string {
|
||||||
return this._avatarUrl;
|
return this.avatarUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThreepidMember extends Member {
|
class ThreepidMember extends Member {
|
||||||
_id: string;
|
private readonly id: string;
|
||||||
|
|
||||||
constructor(id: string) {
|
constructor(id: string) {
|
||||||
super();
|
super();
|
||||||
this._id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a getter that would be falsey on all other implementations. Until we have
|
// This is a getter that would be falsey on all other implementations. Until we have
|
||||||
// better type support in the react-sdk we can use this trick to determine the kind
|
// better type support in the react-sdk we can use this trick to determine the kind
|
||||||
// of 3PID we're dealing with, if any.
|
// of 3PID we're dealing with, if any.
|
||||||
get isEmail(): boolean {
|
get isEmail(): boolean {
|
||||||
return this._id.includes('@');
|
return this.id.includes('@');
|
||||||
}
|
}
|
||||||
|
|
||||||
// These next class members are for the Member interface
|
// These next class members are for the Member interface
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return this._id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
get userId(): string {
|
get userId(): string {
|
||||||
return this._id;
|
return this.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
getMxcAvatarUrl(): string {
|
getMxcAvatarUrl(): string {
|
||||||
|
@ -140,11 +146,11 @@ class ThreepidMember extends Member {
|
||||||
|
|
||||||
interface IDMUserTileProps {
|
interface IDMUserTileProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
onRemove: (RoomMember) => any;
|
onRemove(member: RoomMember): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
_onRemove = (e) => {
|
private onRemove = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -153,9 +159,6 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
|
||||||
|
|
||||||
const avatarSize = 20;
|
const avatarSize = 20;
|
||||||
const avatar = this.props.member.isEmail
|
const avatar = this.props.member.isEmail
|
||||||
? <img
|
? <img
|
||||||
|
@ -177,7 +180,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
closeButton = (
|
closeButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className='mx_InviteDialog_userTile_remove'
|
className='mx_InviteDialog_userTile_remove'
|
||||||
onClick={this._onRemove}
|
onClick={this.onRemove}
|
||||||
>
|
>
|
||||||
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
<img src={require("../../../../res/img/icon-pill-remove.svg")}
|
||||||
alt={_t('Remove')} width={8} height={8}
|
alt={_t('Remove')} width={8} height={8}
|
||||||
|
@ -201,13 +204,13 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
interface IDMRoomTileProps {
|
interface IDMRoomTileProps {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
lastActiveTs: number;
|
lastActiveTs: number;
|
||||||
onToggle: (RoomMember) => any;
|
onToggle(member: RoomMember): void;
|
||||||
highlightWord: string;
|
highlightWord: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
_onClick = (e) => {
|
private onClick = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
@ -215,7 +218,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
this.props.onToggle(this.props.member);
|
this.props.onToggle(this.props.member);
|
||||||
};
|
};
|
||||||
|
|
||||||
_highlightName(str: string) {
|
private highlightName(str: string) {
|
||||||
if (!this.props.highlightWord) return str;
|
if (!this.props.highlightWord) return str;
|
||||||
|
|
||||||
// We convert things to lowercase for index searching, but pull substrings from
|
// We convert things to lowercase for index searching, but pull substrings from
|
||||||
|
@ -252,8 +255,6 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseAvatar = sdk.getComponent("views.avatars.BaseAvatar");
|
|
||||||
|
|
||||||
let timestamp = null;
|
let timestamp = null;
|
||||||
if (this.props.lastActiveTs) {
|
if (this.props.lastActiveTs) {
|
||||||
const humanTs = humanizeTime(this.props.lastActiveTs);
|
const humanTs = humanizeTime(this.props.lastActiveTs);
|
||||||
|
@ -291,13 +292,13 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
|
|
||||||
const caption = this.props.member.isEmail
|
const caption = this.props.member.isEmail
|
||||||
? _t("Invite by email")
|
? _t("Invite by email")
|
||||||
: this._highlightName(this.props.member.userId);
|
: this.highlightName(this.props.member.userId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_InviteDialog_roomTile' onClick={this._onClick}>
|
<div className='mx_InviteDialog_roomTile' onClick={this.onClick}>
|
||||||
{stackedAvatar}
|
{stackedAvatar}
|
||||||
<span className="mx_InviteDialog_roomTile_nameStack">
|
<span className="mx_InviteDialog_roomTile_nameStack">
|
||||||
<div className='mx_InviteDialog_roomTile_name'>{this._highlightName(this.props.member.name)}</div>
|
<div className='mx_InviteDialog_roomTile_name'>{this.highlightName(this.props.member.name)}</div>
|
||||||
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
<div className='mx_InviteDialog_roomTile_userId'>{caption}</div>
|
||||||
</span>
|
</span>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
|
@ -308,7 +309,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
|
|
||||||
interface IInviteDialogProps {
|
interface IInviteDialogProps {
|
||||||
// Takes an array of user IDs/emails to invite.
|
// Takes an array of user IDs/emails to invite.
|
||||||
onFinished: (toInvite?: string[]) => any;
|
onFinished: (toInvite?: string[]) => void;
|
||||||
|
|
||||||
// The kind of invite being performed. Assumed to be KIND_DM if
|
// The kind of invite being performed. Assumed to be KIND_DM if
|
||||||
// not provided.
|
// not provided.
|
||||||
|
@ -349,8 +350,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
initialText: "",
|
initialText: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
_debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
private debounceTimer: NodeJS.Timeout = null; // actually number because we're in the browser
|
||||||
_editorRef: any = null;
|
private editorRef = createRef<HTMLInputElement>();
|
||||||
|
private unmounted = false;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -378,7 +380,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
filterText: this.props.initialText,
|
filterText: this.props.initialText,
|
||||||
recents: InviteDialog.buildRecents(alreadyInvited),
|
recents: InviteDialog.buildRecents(alreadyInvited),
|
||||||
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
numRecentsShown: INITIAL_ROOMS_SHOWN,
|
||||||
suggestions: this._buildSuggestions(alreadyInvited),
|
suggestions: this.buildSuggestions(alreadyInvited),
|
||||||
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
|
||||||
serverResultsMixin: [],
|
serverResultsMixin: [],
|
||||||
threepidResultsMixin: [],
|
threepidResultsMixin: [],
|
||||||
|
@ -390,21 +392,23 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
busy: false,
|
busy: false,
|
||||||
errorText: null,
|
errorText: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._editorRef = createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.initialText) {
|
if (this.props.initialText) {
|
||||||
this._updateSuggestions(this.props.initialText);
|
this.updateSuggestions(this.props.initialText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.unmounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
private onConsultFirstChange = (ev) => {
|
private onConsultFirstChange = (ev) => {
|
||||||
this.setState({consultFirst: ev.target.checked});
|
this.setState({consultFirst: ev.target.checked});
|
||||||
}
|
}
|
||||||
|
|
||||||
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number}[] {
|
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
|
||||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||||
|
|
||||||
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
// Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the
|
||||||
|
@ -467,7 +471,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return recents;
|
return recents;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
private buildSuggestions(excludedTargetIds: Set<string>): {userId: string, user: RoomMember}[] {
|
||||||
const maxConsideredMembers = 200;
|
const maxConsideredMembers = 200;
|
||||||
const joinedRooms = MatrixClientPeg.get().getRooms()
|
const joinedRooms = MatrixClientPeg.get().getRooms()
|
||||||
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
.filter(r => r.getMyMembership() === 'join' && r.getJoinedMemberCount() <= maxConsideredMembers);
|
||||||
|
@ -585,7 +589,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
return members.map(m => ({userId: m.member.userId, user: m.member}));
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldAbortAfterInviteError(result): boolean {
|
private shouldAbortAfterInviteError(result): boolean {
|
||||||
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error');
|
||||||
if (failedUsers.length > 0) {
|
if (failedUsers.length > 0) {
|
||||||
console.log("Failed to invite users: ", result);
|
console.log("Failed to invite users: ", result);
|
||||||
|
@ -600,7 +604,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_convertFilter(): Member[] {
|
private convertFilter(): Member[] {
|
||||||
// Check to see if there's anything to convert first
|
// Check to see if there's anything to convert first
|
||||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||||
|
|
||||||
|
@ -617,10 +621,10 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return newTargets;
|
return newTargets;
|
||||||
}
|
}
|
||||||
|
|
||||||
_startDm = async () => {
|
private startDm = async () => {
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
|
|
||||||
// Check if there is already a DM with these people and reuse it if possible.
|
// Check if there is already a DM with these people and reuse it if possible.
|
||||||
|
@ -694,11 +698,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_inviteUsers = async () => {
|
private inviteUsers = async () => {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
this.setState({busy: true});
|
this.setState({busy: true});
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -715,7 +719,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
try {
|
try {
|
||||||
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
|
||||||
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
|
||||||
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
|
if (!this.shouldAbortAfterInviteError(result)) { // handles setting error message too
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -749,9 +753,9 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_transferCall = async () => {
|
private transferCall = async () => {
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
const targets = this._convertFilter();
|
const targets = this.convertFilter();
|
||||||
const targetIds = targets.map(t => t.userId);
|
const targetIds = targets.map(t => t.userId);
|
||||||
if (targetIds.length > 1) {
|
if (targetIds.length > 1) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -790,26 +794,26 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onKeyDown = (e) => {
|
private onKeyDown = (e) => {
|
||||||
if (this.state.busy) return;
|
if (this.state.busy) return;
|
||||||
const value = e.target.value.trim();
|
const value = e.target.value.trim();
|
||||||
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
const hasModifiers = e.ctrlKey || e.shiftKey || e.metaKey;
|
||||||
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
if (!value && this.state.targets.length > 0 && e.key === Key.BACKSPACE && !hasModifiers) {
|
||||||
// when the field is empty and the user hits backspace remove the right-most target
|
// when the field is empty and the user hits backspace remove the right-most target
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._removeMember(this.state.targets[this.state.targets.length - 1]);
|
this.removeMember(this.state.targets[this.state.targets.length - 1]);
|
||||||
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
} else if (value && e.key === Key.ENTER && !hasModifiers) {
|
||||||
// when the user hits enter with something in their field try to convert it
|
// when the user hits enter with something in their field try to convert it
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
} else if (value && e.key === Key.SPACE && !hasModifiers && value.includes("@") && !value.includes(" ")) {
|
||||||
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
// when the user hits space and their input looks like an e-mail/MXID then try to convert it
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this._convertFilter();
|
this.convertFilter();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateSuggestions = async (term) => {
|
private updateSuggestions = async (term) => {
|
||||||
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
MatrixClientPeg.get().searchUserDirectory({term}).then(async r => {
|
||||||
if (term !== this.state.filterText) {
|
if (term !== this.state.filterText) {
|
||||||
// Discard the results - we were probably too slow on the server-side to make
|
// Discard the results - we were probably too slow on the server-side to make
|
||||||
|
@ -918,30 +922,30 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_updateFilter = (e) => {
|
private updateFilter = (e) => {
|
||||||
const term = e.target.value;
|
const term = e.target.value;
|
||||||
this.setState({filterText: term});
|
this.setState({filterText: term});
|
||||||
|
|
||||||
// Debounce server lookups to reduce spam. We don't clear the existing server
|
// Debounce server lookups to reduce spam. We don't clear the existing server
|
||||||
// results because they might still be vaguely accurate, likewise for races which
|
// results because they might still be vaguely accurate, likewise for races which
|
||||||
// could happen here.
|
// could happen here.
|
||||||
if (this._debounceTimer) {
|
if (this.debounceTimer) {
|
||||||
clearTimeout(this._debounceTimer);
|
clearTimeout(this.debounceTimer);
|
||||||
}
|
}
|
||||||
this._debounceTimer = setTimeout(() => {
|
this.debounceTimer = setTimeout(() => {
|
||||||
this._updateSuggestions(term);
|
this.updateSuggestions(term);
|
||||||
}, 150); // 150ms debounce (human reaction time + some)
|
}, 150); // 150ms debounce (human reaction time + some)
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreRecents = () => {
|
private showMoreRecents = () => {
|
||||||
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
this.setState({numRecentsShown: this.state.numRecentsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
};
|
};
|
||||||
|
|
||||||
_showMoreSuggestions = () => {
|
private showMoreSuggestions = () => {
|
||||||
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
this.setState({numSuggestionsShown: this.state.numSuggestionsShown + INCREMENT_ROOMS_SHOWN});
|
||||||
};
|
};
|
||||||
|
|
||||||
_toggleMember = (member: Member) => {
|
private toggleMember = (member: Member) => {
|
||||||
if (!this.state.busy) {
|
if (!this.state.busy) {
|
||||||
let filterText = this.state.filterText;
|
let filterText = this.state.filterText;
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
|
@ -954,13 +958,13 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
}
|
}
|
||||||
this.setState({targets, filterText});
|
this.setState({targets, filterText});
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_removeMember = (member: Member) => {
|
private removeMember = (member: Member) => {
|
||||||
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
const targets = this.state.targets.map(t => t); // cheap clone for mutation
|
||||||
const idx = targets.indexOf(member);
|
const idx = targets.indexOf(member);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
|
@ -968,12 +972,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({targets});
|
this.setState({targets});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onPaste = async (e) => {
|
private onPaste = async (e) => {
|
||||||
if (this.state.filterText) {
|
if (this.state.filterText) {
|
||||||
// if the user has already typed something, just let them
|
// if the user has already typed something, just let them
|
||||||
// paste normally.
|
// paste normally.
|
||||||
|
@ -1027,6 +1031,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
failed.push(address);
|
failed.push(address);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (this.unmounted) return;
|
||||||
|
|
||||||
if (failed.length > 0) {
|
if (failed.length > 0) {
|
||||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
|
@ -1043,17 +1048,17 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({targets: [...this.state.targets, ...toAdd]});
|
this.setState({targets: [...this.state.targets, ...toAdd]});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onClickInputArea = (e) => {
|
private onClickInputArea = (e) => {
|
||||||
// Stop the browser from highlighting text
|
// Stop the browser from highlighting text
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (this._editorRef && this._editorRef.current) {
|
if (this.editorRef && this.editorRef.current) {
|
||||||
this._editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUseDefaultIdentityServerClick = (e) => {
|
private onUseDefaultIdentityServerClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Update the IS in account data. Actually using it may trigger terms.
|
// Update the IS in account data. Actually using it may trigger terms.
|
||||||
|
@ -1062,21 +1067,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
this.setState({canUseIdentityServer: true, tryingIdentityServer: false});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onManageSettingsClick = (e) => {
|
private onManageSettingsClick = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
dis.fire(Action.ViewUserSettings);
|
dis.fire(Action.ViewUserSettings);
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCommunityInviteClick = (e) => {
|
private onCommunityInviteClick = (e) => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
showCommunityInviteDialog(CommunityPrototypeStore.instance.getSelectedCommunityId());
|
||||||
};
|
};
|
||||||
|
|
||||||
_renderSection(kind: "recents"|"suggestions") {
|
private renderSection(kind: "recents"|"suggestions") {
|
||||||
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
let sourceMembers = kind === 'recents' ? this.state.recents : this.state.suggestions;
|
||||||
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
let showNum = kind === 'recents' ? this.state.numRecentsShown : this.state.numSuggestionsShown;
|
||||||
const showMoreFn = kind === 'recents' ? this._showMoreRecents.bind(this) : this._showMoreSuggestions.bind(this);
|
const showMoreFn = kind === 'recents' ? this.showMoreRecents.bind(this) : this.showMoreSuggestions.bind(this);
|
||||||
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
const lastActive = (m) => kind === 'recents' ? m.lastActive : null;
|
||||||
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
let sectionName = kind === 'recents' ? _t("Recent Conversations") : _t("Suggestions");
|
||||||
let sectionSubname = null;
|
let sectionSubname = null;
|
||||||
|
@ -1156,7 +1161,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
member={r.user}
|
member={r.user}
|
||||||
lastActiveTs={lastActive(r)}
|
lastActiveTs={lastActive(r)}
|
||||||
key={r.userId}
|
key={r.userId}
|
||||||
onToggle={this._toggleMember}
|
onToggle={this.toggleMember}
|
||||||
highlightWord={this.state.filterText}
|
highlightWord={this.state.filterText}
|
||||||
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
isSelected={this.state.targets.some(t => t.userId === r.userId)}
|
||||||
/>
|
/>
|
||||||
|
@ -1171,32 +1176,32 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderEditor() {
|
private renderEditor() {
|
||||||
const targets = this.state.targets.map(t => (
|
const targets = this.state.targets.map(t => (
|
||||||
<DMUserTile member={t} onRemove={!this.state.busy && this._removeMember} key={t.userId} />
|
<DMUserTile member={t} onRemove={!this.state.busy && this.removeMember} key={t.userId} />
|
||||||
));
|
));
|
||||||
const input = (
|
const input = (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
onKeyDown={this._onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this._updateFilter}
|
onChange={this.updateFilter}
|
||||||
value={this.state.filterText}
|
value={this.state.filterText}
|
||||||
ref={this._editorRef}
|
ref={this.editorRef}
|
||||||
onPaste={this._onPaste}
|
onPaste={this.onPaste}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<div className='mx_InviteDialog_editor' onClick={this._onClickInputArea}>
|
<div className='mx_InviteDialog_editor' onClick={this.onClickInputArea}>
|
||||||
{targets}
|
{targets}
|
||||||
{input}
|
{input}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderIdentityServerWarning() {
|
private renderIdentityServerWarning() {
|
||||||
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
if (!this.state.tryingIdentityServer || this.state.canUseIdentityServer ||
|
||||||
!SettingsStore.getValue(UIFeature.IdentityServer)
|
!SettingsStore.getValue(UIFeature.IdentityServer)
|
||||||
) {
|
) {
|
||||||
|
@ -1214,8 +1219,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
default: sub => <a href="#" onClick={this._onUseDefaultIdentityServerClick}>{sub}</a>,
|
default: sub => <a href="#" onClick={this.onUseDefaultIdentityServerClick}>{sub}</a>,
|
||||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</div>
|
)}</div>
|
||||||
);
|
);
|
||||||
|
@ -1225,7 +1230,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
"Use an identity server to invite by email. " +
|
"Use an identity server to invite by email. " +
|
||||||
"Manage in <settings>Settings</settings>.",
|
"Manage in <settings>Settings</settings>.",
|
||||||
{}, {
|
{}, {
|
||||||
settings: sub => <a href="#" onClick={this._onManageSettingsClick}>{sub}</a>,
|
settings: sub => <a href="#" onClick={this.onManageSettingsClick}>{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</div>
|
)}</div>
|
||||||
);
|
);
|
||||||
|
@ -1298,7 +1303,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="link"
|
kind="link"
|
||||||
onClick={this._onCommunityInviteClick}
|
onClick={this.onCommunityInviteClick}
|
||||||
>{sub}</AccessibleButton>
|
>{sub}</AccessibleButton>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
@ -1309,7 +1314,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
buttonText = _t("Go");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this.startDm;
|
||||||
} else if (this.props.kind === KIND_INVITE) {
|
} else if (this.props.kind === KIND_INVITE) {
|
||||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||||
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||||
|
@ -1348,7 +1353,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
});
|
});
|
||||||
|
|
||||||
buttonText = _t("Invite");
|
buttonText = _t("Invite");
|
||||||
goButtonFn = this._inviteUsers;
|
goButtonFn = this.inviteUsers;
|
||||||
|
|
||||||
if (cli.isRoomEncrypted(this.props.roomId)) {
|
if (cli.isRoomEncrypted(this.props.roomId)) {
|
||||||
const room = cli.getRoom(this.props.roomId);
|
const room = cli.getRoom(this.props.roomId);
|
||||||
|
@ -1370,7 +1375,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||||
title = _t("Transfer");
|
title = _t("Transfer");
|
||||||
buttonText = _t("Transfer");
|
buttonText = _t("Transfer");
|
||||||
goButtonFn = this._transferCall;
|
goButtonFn = this.transferCall;
|
||||||
consultSection = <div>
|
consultSection = <div>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
<input type="checkbox" checked={this.state.consultFirst} onChange={this.onConsultFirstChange} />
|
||||||
|
@ -1393,7 +1398,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
<div className='mx_InviteDialog_content'>
|
<div className='mx_InviteDialog_content'>
|
||||||
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
<p className='mx_InviteDialog_helpText'>{helpText}</p>
|
||||||
<div className='mx_InviteDialog_addressBar'>
|
<div className='mx_InviteDialog_addressBar'>
|
||||||
{this._renderEditor()}
|
{this.renderEditor()}
|
||||||
<div className='mx_InviteDialog_buttonAndSpinner'>
|
<div className='mx_InviteDialog_buttonAndSpinner'>
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary"
|
kind="primary"
|
||||||
|
@ -1407,11 +1412,11 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{keySharingWarning}
|
{keySharingWarning}
|
||||||
{this._renderIdentityServerWarning()}
|
{this.renderIdentityServerWarning()}
|
||||||
<div className='error'>{this.state.errorText}</div>
|
<div className='error'>{this.state.errorText}</div>
|
||||||
<div className='mx_InviteDialog_userSections'>
|
<div className='mx_InviteDialog_userSections'>
|
||||||
{this._renderSection('recents')}
|
{this.renderSection('recents')}
|
||||||
{this._renderSection('suggestions')}
|
{this.renderSection('suggestions')}
|
||||||
</div>
|
</div>
|
||||||
{consultSection}
|
{consultSection}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -55,6 +55,7 @@ interface IProps {
|
||||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||||
onFocus: (ev: React.FocusEvent) => void;
|
onFocus: (ev: React.FocusEvent) => void;
|
||||||
onBlur: (ev: React.FocusEvent) => void;
|
onBlur: (ev: React.FocusEvent) => void;
|
||||||
|
onListCollapse?: (isExpanded: boolean) => void;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
activeSpace: Room;
|
activeSpace: Room;
|
||||||
|
@ -538,6 +539,7 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
|
||||||
extraTiles={extraTiles}
|
extraTiles={extraTiles}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
alwaysVisible={ALWAYS_VISIBLE_TAGS.includes(orderedTagId)}
|
||||||
|
onListCollapse={this.props.onListCollapse}
|
||||||
/>
|
/>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,7 @@ interface IProps {
|
||||||
alwaysVisible?: boolean;
|
alwaysVisible?: boolean;
|
||||||
resizeNotifier: ResizeNotifier;
|
resizeNotifier: ResizeNotifier;
|
||||||
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
extraTiles?: ReactComponentElement<typeof ExtraTile>[];
|
||||||
|
onListCollapse?: (isExpanded: boolean) => void;
|
||||||
|
|
||||||
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
// TODO: Account for https://github.com/vector-im/element-web/issues/14179
|
||||||
}
|
}
|
||||||
|
@ -472,6 +473,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
|
||||||
private toggleCollapsed = () => {
|
private toggleCollapsed = () => {
|
||||||
this.layout.isCollapsed = this.state.isExpanded;
|
this.layout.isCollapsed = this.state.isExpanded;
|
||||||
this.setState({isExpanded: !this.layout.isCollapsed});
|
this.setState({isExpanded: !this.layout.isCollapsed});
|
||||||
|
if (this.props.onListCollapse) {
|
||||||
|
this.props.onListCollapse(!this.layout.isCollapsed)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useState} from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
@ -127,6 +127,12 @@ const SpacePanel = () => {
|
||||||
const [invites, spaces, activeSpace] = useSpaces();
|
const [invites, spaces, activeSpace] = useSpaces();
|
||||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPanelCollapsed && menuDisplayed) {
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
}, [isPanelCollapsed]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const newClasses = classNames("mx_SpaceButton_new", {
|
const newClasses = classNames("mx_SpaceButton_new", {
|
||||||
mx_SpaceButton_newCancel: menuDisplayed,
|
mx_SpaceButton_newCancel: menuDisplayed,
|
||||||
});
|
});
|
||||||
|
@ -235,18 +241,15 @@ const SpacePanel = () => {
|
||||||
className={newClasses}
|
className={newClasses}
|
||||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||||
onClick={menuDisplayed ? closeMenu : () => {
|
onClick={menuDisplayed ? closeMenu : () => {
|
||||||
openMenu();
|
|
||||||
if (!isPanelCollapsed) setPanelCollapsed(true);
|
if (!isPanelCollapsed) setPanelCollapsed(true);
|
||||||
|
openMenu();
|
||||||
}}
|
}}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||||
onClick={() => {
|
onClick={() => setPanelCollapsed(!isPanelCollapsed)}
|
||||||
setPanelCollapsed(!isPanelCollapsed);
|
|
||||||
if (menuDisplayed) closeMenu();
|
|
||||||
}}
|
|
||||||
title={expandCollapseButtonTitle}
|
title={expandCollapseButtonTitle}
|
||||||
/>
|
/>
|
||||||
{ contextMenu }
|
{ contextMenu }
|
||||||
|
|
|
@ -37,6 +37,8 @@
|
||||||
"Call Failed": "Call Failed",
|
"Call Failed": "Call Failed",
|
||||||
"Call Declined": "Call Declined",
|
"Call Declined": "Call Declined",
|
||||||
"The other party declined the call.": "The other party declined the call.",
|
"The other party declined the call.": "The other party declined the call.",
|
||||||
|
"User Busy": "User Busy",
|
||||||
|
"The user you called is busy.": "The user you called is busy.",
|
||||||
"The remote side failed to pick up": "The remote side failed to pick up",
|
"The remote side failed to pick up": "The remote side failed to pick up",
|
||||||
"The call could not be established": "The call could not be established",
|
"The call could not be established": "The call could not be established",
|
||||||
"Answered Elsewhere": "Answered Elsewhere",
|
"Answered Elsewhere": "Answered Elsewhere",
|
||||||
|
|
|
@ -24,12 +24,14 @@ export enum UI_EVENTS {
|
||||||
|
|
||||||
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
export type ResizeObserverCallbackFunction = (entries: ResizeObserverEntry[]) => void;
|
||||||
|
|
||||||
|
|
||||||
export default class UIStore extends EventEmitter {
|
export default class UIStore extends EventEmitter {
|
||||||
private static _instance: UIStore = null;
|
private static _instance: UIStore = null;
|
||||||
|
|
||||||
private resizeObserver: ResizeObserver;
|
private resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
private uiElementDimensions = new Map<string, DOMRectReadOnly>();
|
||||||
|
private trackedUiElements = new Map<Element, string>();
|
||||||
|
|
||||||
public windowWidth: number;
|
public windowWidth: number;
|
||||||
public windowHeight: number;
|
public windowHeight: number;
|
||||||
|
|
||||||
|
@ -60,14 +62,51 @@ export default class UIStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
public getElementDimensions(name: string): DOMRectReadOnly {
|
||||||
const { width, height } = entries
|
return this.uiElementDimensions.get(name);
|
||||||
.find(entry => entry.target === document.body)
|
}
|
||||||
.contentRect;
|
|
||||||
|
|
||||||
this.windowWidth = width;
|
public trackElementDimensions(name: string, element: Element): void {
|
||||||
this.windowHeight = height;
|
this.trackedUiElements.set(element, name);
|
||||||
|
this.resizeObserver.observe(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopTrackingElementDimensions(name: string): void {
|
||||||
|
let trackedElement: Element;
|
||||||
|
this.trackedUiElements.forEach((trackedElementName, element) => {
|
||||||
|
if (trackedElementName === name) {
|
||||||
|
trackedElement = element;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (trackedElement) {
|
||||||
|
this.resizeObserver.unobserve(trackedElement);
|
||||||
|
this.uiElementDimensions.delete(name);
|
||||||
|
this.trackedUiElements.delete(trackedElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isTrackingElementDimensions(name: string): boolean {
|
||||||
|
return this.uiElementDimensions.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resizeObserverCallback = (entries: ResizeObserverEntry[]) => {
|
||||||
|
const windowEntry = entries.find(entry => entry.target === document.body);
|
||||||
|
|
||||||
|
if (windowEntry) {
|
||||||
|
this.windowWidth = windowEntry.contentRect.width;
|
||||||
|
this.windowHeight = windowEntry.contentRect.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.forEach(entry => {
|
||||||
|
const trackedElementName = this.trackedUiElements.get(entry.target);
|
||||||
|
if (trackedElementName) {
|
||||||
|
this.uiElementDimensions.set(trackedElementName, entry.contentRect);
|
||||||
|
this.emit(trackedElementName, UI_EVENTS.Resize, entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.emit(UI_EVENTS.Resize, entries);
|
this.emit(UI_EVENTS.Resize, entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.mxUIStore = UIStore.instance;
|
||||||
|
|
|
@ -8438,9 +8438,9 @@ write@1.0.3:
|
||||||
mkdirp "^0.5.1"
|
mkdirp "^0.5.1"
|
||||||
|
|
||||||
ws@^7.2.3:
|
ws@^7.2.3:
|
||||||
version "7.4.2"
|
version "7.4.6"
|
||||||
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd"
|
resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c"
|
||||||
integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA==
|
integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==
|
||||||
|
|
||||||
xml-name-validator@^3.0.0:
|
xml-name-validator@^3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
|
|
Loading…
Reference in a new issue