Merge branch 'develop' of https://github.com/matrix-org/matrix-react-sdk into t3chguy/display-capture

This commit is contained in:
Michael Telatynski 2020-08-28 17:26:10 +01:00
commit 2823156ef6
100 changed files with 4228 additions and 1844 deletions

View file

@ -378,7 +378,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/>
{SettingsStore.isFeatureEnabled("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div>
);

View file

@ -76,6 +76,8 @@ import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { SettingLevel } from "../../settings/SettingLevel";
import { leaveRoomBehaviour } from "../../utils/membership";
import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityPrototypeDialog";
/** constants for MatrixChat.state.view */
export enum Views {
@ -619,7 +621,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.createRoom(payload.public);
break;
case 'view_create_group': {
const CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = CreateCommunityPrototypeDialog;
}
Modal.createTrackedDialog('Create Community', '', CreateGroupDialog);
break;
}
@ -1082,50 +1087,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
button: _t("Leave"),
onFinished: (shouldLeave) => {
if (shouldLeave) {
const d = MatrixClientPeg.get().leaveRoomChain(roomId);
const d = leaveRoomBehaviour(roomId);
// FIXME: controller shouldn't be loading a view :(
const Loader = sdk.getComponent("elements.Spinner");
const modal = Modal.createDialog(Loader, null, 'mx_Dialog_spinner');
d.then((errors) => {
modal.close();
for (const leftRoomId of Object.keys(errors)) {
const err = errors[leftRoomId];
if (!err) continue;
console.error("Failed to leave room " + leftRoomId + " " + err);
let title = _t("Failed to leave room");
let message = _t("Server may be unavailable, overloaded, or you hit a bug.");
if (err.errcode === 'M_CANNOT_LEAVE_SERVER_NOTICE_ROOM') {
title = _t("Can't leave Server Notices room");
message = _t(
"This room is used for important messages from the Homeserver, " +
"so you cannot leave it.",
);
} else if (err && err.message) {
message = err.message;
}
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: title,
description: message,
});
return;
}
if (this.state.currentRoomId === roomId) {
dis.dispatch({action: 'view_next_room'});
}
}, (err) => {
// This should only happen if something went seriously wrong with leaving the chain.
modal.close();
console.error("Failed to leave room " + roomId + " " + err);
Modal.createTrackedDialog('Failed to leave room', '', ErrorDialog, {
title: _t("Failed to leave room"),
description: _t("Unknown error"),
});
});
d.finally(() => modal.close());
}
},
});
@ -2085,3 +2053,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
</ErrorBoundary>;
}
}
export function isLoggedIn(): boolean {
// JRS: Maybe we should move the step that writes this to the window out of
// `element-web` and into this file? Better yet, we should probably create a
// store to hold this state.
// See also https://github.com/vector-im/element-web/issues/15034.
const app = window.matrixChat;
return app && (app as MatrixChat).state.view === Views.LOGGED_IN;
}

View file

@ -30,6 +30,10 @@ import { instanceForInstanceId, protocolNameForInstanceId } from '../../utils/Di
import Analytics from '../../Analytics';
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {ALL_ROOMS} from "../views/directory/NetworkDropdown";
import SettingsStore from "../../settings/SettingsStore";
import TagOrderStore from "../../stores/TagOrderStore";
import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 160;
@ -46,6 +50,7 @@ export default createReactClass({
},
getInitialState: function() {
const selectedCommunityId = TagOrderStore.getSelectedTags()[0];
return {
publicRooms: [],
loading: true,
@ -54,6 +59,10 @@ export default createReactClass({
instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(),
filterString: null,
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
? selectedCommunityId
: null,
communityName: null,
};
},
@ -71,28 +80,39 @@ export default createReactClass({
this.setState({protocolsLoading: false});
return;
}
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
// thing you see when loading the client!
return;
}
track('Failed to get protocol list from homeserver');
const brand = SdkConfig.get().brand;
this.setState({
error: _t(
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
{ brand },
),
if (!this.state.selectedCommunityId) {
MatrixClientPeg.get().getThirdpartyProtocols().then((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
console.warn(`error loading third party protocols: ${err}`);
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
// thing you see when loading the client!
return;
}
track('Failed to get protocol list from homeserver');
const brand = SdkConfig.get().brand;
this.setState({
error: _t(
'%(brand)s failed to get the protocol list from the homeserver. ' +
'The homeserver may be too old to support third party networks.',
{brand},
),
});
});
});
} else {
// We don't use the protocols in the communities v2 prototype experience
this.setState({protocolsLoading: false});
// Grab the profile info async
FlairStore.getGroupProfileCached(MatrixClientPeg.get(), this.state.selectedCommunityId).then(profile => {
this.setState({communityName: profile.name});
});
}
this.refreshRoomList();
},
@ -105,6 +125,33 @@ export default createReactClass({
},
refreshRoomList: function() {
if (this.state.selectedCommunityId) {
this.setState({
publicRooms: GroupStore.getGroupRooms(this.state.selectedCommunityId).map(r => {
return {
// Translate all the group properties to the directory format
room_id: r.roomId,
name: r.name,
topic: r.topic,
canonical_alias: r.canonicalAlias,
num_joined_members: r.numJoinedMembers,
avatarUrl: r.avatarUrl,
world_readable: r.worldReadable,
guest_can_join: r.guestsCanJoin,
};
}).filter(r => {
const filterString = this.state.filterString;
if (filterString) {
const containedIn = (s: string) => (s || "").toLowerCase().includes(filterString.toLowerCase());
return containedIn(r.name) || containedIn(r.topic) || containedIn(r.canonical_alias);
}
return true;
}),
loading: false,
});
return;
}
this.nextBatch = null;
this.setState({
publicRooms: [],
@ -114,6 +161,7 @@ export default createReactClass({
},
getMoreRooms: function() {
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve();
this.setState({
@ -239,7 +287,7 @@ export default createReactClass({
},
onRoomClicked: function(room, ev) {
if (ev.shiftKey) {
if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault();
this.removeFromDirectory(room);
} else {
@ -610,6 +658,18 @@ export default createReactClass({
}
}
let dropdown = (
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
);
if (this.state.selectedCommunityId) {
dropdown = null;
}
listHeader = <div className="mx_RoomDirectory_listheader">
<DirectorySearchBox
className="mx_RoomDirectory_searchbox"
@ -619,12 +679,7 @@ export default createReactClass({
placeholder={placeholder}
showJoinButton={showJoinButton}
/>
<NetworkDropdown
protocols={this.protocols}
onOptionChange={this.onOptionChange}
selectedServerName={this.state.roomServer}
selectedInstanceId={this.state.instanceId}
/>
{dropdown}
</div>;
}
const explanation =
@ -637,12 +692,16 @@ export default createReactClass({
}},
);
const title = this.state.selectedCommunityId
? _t("Explore rooms in %(communityName)s", {
communityName: this.state.communityName || this.state.selectedCommunityId,
}) : _t("Explore rooms");
return (
<BaseDialog
className={'mx_RoomDirectory_dialog'}
hasCancel={true}
onFinished={this.props.onFinished}
title={_t("Explore rooms")}
title={title}
>
<div className="mx_RoomDirectory">
{explanation}

View file

@ -29,6 +29,8 @@ import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile";
const TagPanel = createReactClass({
displayName: 'TagPanel',
@ -93,20 +95,24 @@ const TagPanel = createReactClass({
}
},
onCreateGroupClick(ev) {
ev.stopPropagation();
dis.dispatch({action: 'view_create_group'});
},
onClearFilterClick(ev) {
dis.dispatch({action: 'deselect_tags'});
},
renderGlobalIcon() {
if (!SettingsStore.getValue("feature_communities_v2_prototypes")) return null;
return (
<div>
<UserTagTile />
<hr className="mx_TagPanel_divider" />
</div>
);
},
render() {
const DNDTagTile = sdk.getComponent('elements.DNDTagTile');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const ActionButton = sdk.getComponent('elements.ActionButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const tags = this.state.orderedTags.map((tag, index) => {
return <DNDTagTile
@ -118,26 +124,29 @@ const TagPanel = createReactClass({
});
const itemsSelected = this.state.selectedTags.length > 0;
let clearButton;
if (itemsSelected) {
clearButton = <AccessibleButton className="mx_TagPanel_clearButton" onClick={this.onClearFilterClick}>
<TintableSvg src={require("../../../res/img/icons-close.svg")} width="24" height="24"
alt={_t("Clear filter")}
title={_t("Clear filter")}
/>
</AccessibleButton>;
}
const classes = classNames('mx_TagPanel', {
mx_TagPanel_items_selected: itemsSelected,
});
return <div className={classes}>
<div className="mx_TagPanel_clearButton_container">
{ clearButton }
</div>
<div className="mx_TagPanel_divider" />
let createButton = (
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
createButton = (
<ActionButton
tooltip
label={_t("Create community")}
action="view_create_group"
className="mx_TagTile mx_TagTile_plus" />
);
}
return <div className={classes} onClick={this.onClearFilterClick}>
<AutoHideScrollbar
className="mx_TagPanel_scroller"
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
@ -153,13 +162,10 @@ const TagPanel = createReactClass({
className="mx_TagPanel_tagTileContainer"
ref={provided.innerRef}
>
{ this.renderGlobalIcon() }
{ tags }
<div>
<ActionButton
tooltip
label={_t("Communities")}
action="toggle_my_groups"
className="mx_TagTile mx_TagTile_plus" />
{createButton}
</div>
{ provided.placeholder }
</div>

View file

@ -295,7 +295,10 @@ export default class UserMenu extends React.Component<IProps, IState> {
public render() {
const avatarSize = 32; // should match border-radius of the avatar
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId();
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
let name = <span className="mx_UserMenu_userName">{displayName}</span>;
let buttons = (
<span className="mx_UserMenu_headerButtons">
{/* masked image in CSS */}
@ -324,9 +327,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
<div className="mx_UserMenu_row">
<span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar
idName={MatrixClientPeg.get().getUserId()}
name={OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId()}
url={OwnProfileStore.instance.getHttpAvatarUrl(avatarSize)}
idName={displayName}
name={displayName}
url={avatarUrl}
width={avatarSize}
height={avatarSize}
resizeMethod="crop"

View file

@ -42,34 +42,35 @@ interface IProps {
className?: string;
}
const calculateUrls = (url, urls) => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = urls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
return Array.from(new Set(_urls));
};
const useImageUrl = ({url, urls}): [string, () => void] => {
const [imageUrls, setUrls] = useState<string[]>([]);
const [urlsIndex, setIndex] = useState<number>();
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls));
const [urlsIndex, setIndex] = useState<number>(0);
const onError = useCallback(() => {
setIndex(i => i + 1); // try the next one
}, []);
const memoizedUrls = useMemo(() => urls, [JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
// work out the full set of urls to try to load. This is formed like so:
// imageUrls: [ props.url, ...props.urls ]
let _urls = [];
if (!SettingsStore.getValue("lowBandwidth")) {
_urls = memoizedUrls || [];
if (url) {
_urls.unshift(url); // put in urls[0]
}
}
// deduplicate URLs
_urls = Array.from(new Set(_urls));
setUrls(calculateUrls(url, urls));
setIndex(0);
setUrls(_urls);
}, [url, memoizedUrls]); // eslint-disable-line react-hooks/exhaustive-deps
}, [url, JSON.stringify(urls)]); // eslint-disable-line react-hooks/exhaustive-deps
const cli = useContext(MatrixClientContext);
const onClientSync = useCallback((syncState, prevState) => {

View file

@ -53,7 +53,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
}
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (!SettingsStore.getValue("feature_custom_status")) {
return;
}
const { user } = this.props.member;
@ -105,7 +105,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
resizeMethod={this.props.resizeMethod}
/>;
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (!SettingsStore.getValue("feature_custom_status")) {
return avatar;
}

View file

@ -114,9 +114,12 @@ export default class RoomAvatar extends React.Component<IProps, IState> {
}
private onRoomAvatarClick = () => {
const avatarUrl = this.props.room.getAvatarUrl(
MatrixClientPeg.get().getHomeserverUrl(),
null, null, null, false);
const avatarUrl = Avatar.avatarUrlForRoom(
this.props.room,
null,
null,
null,
);
const params = {
src: avatarUrl,
name: this.props.room.name,

View file

@ -81,7 +81,7 @@ export default createReactClass({
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false;
if (!SettingsStore.getValue("feature_pinning")) canPin = false;
this.setState({canRedact, canPin});
},

View file

@ -1,404 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
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 createReactClass from 'create-react-class';
import classNames from 'classnames';
import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import dis from '../../../dispatcher/dispatcher';
import DMRoomMap from '../../../utils/DMRoomMap';
import * as Rooms from '../../../Rooms';
import * as RoomNotifs from '../../../RoomNotifs';
import Modal from '../../../Modal';
import RoomListActions from '../../../actions/RoomListActions';
import RoomViewStore from '../../../stores/RoomViewStore';
import {sleep} from "../../../utils/promise";
import {MenuItem, MenuItemCheckbox, MenuItemRadio} from "../../structures/ContextMenu";
const RoomTagOption = ({active, onClick, src, srcSet, label}) => {
const classes = classNames('mx_RoomTileContextMenu_tag_field', {
'mx_RoomTileContextMenu_tag_fieldSet': active,
'mx_RoomTileContextMenu_tag_fieldDisabled': false,
});
return (
<MenuItemCheckbox className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_tag_icon" src={src} width="15" height="15" alt="" />
<img className="mx_RoomTileContextMenu_tag_icon_set" src={srcSet} width="15" height="15" alt="" />
{ label }
</MenuItemCheckbox>
);
};
const NotifOption = ({active, onClick, src, label}) => {
const classes = classNames('mx_RoomTileContextMenu_notif_field', {
'mx_RoomTileContextMenu_notif_fieldSet': active,
});
return (
<MenuItemRadio className={classes} onClick={onClick} active={active} label={label}>
<img className="mx_RoomTileContextMenu_notif_activeIcon" src={require("../../../../res/img/notif-active.svg")} width="12" height="12" alt="" />
<img className="mx_RoomTileContextMenu_notif_icon mx_filterFlipColor" src={src} width="16" height="12" alt="" />
{ label }
</MenuItemRadio>
);
};
export default createReactClass({
displayName: 'RoomTileContextMenu',
propTypes: {
room: PropTypes.object.isRequired,
/* callback called when the menu is dismissed */
onFinished: PropTypes.func,
},
getInitialState() {
const dmRoomMap = new DMRoomMap(MatrixClientPeg.get());
return {
roomNotifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
isFavourite: this.props.room.tags.hasOwnProperty("m.favourite"),
isLowPriority: this.props.room.tags.hasOwnProperty("m.lowpriority"),
isDirectMessage: Boolean(dmRoomMap.getUserIdForRoomId(this.props.room.roomId)),
};
},
componentDidMount: function() {
this._unmounted = false;
},
componentWillUnmount: function() {
this._unmounted = true;
},
_toggleTag: function(tagNameOn, tagNameOff) {
if (!MatrixClientPeg.get().isGuest()) {
sleep(500).then(() => {
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
tagNameOff, tagNameOn,
undefined, 0,
), true);
this.props.onFinished();
});
}
},
_onClickFavourite: function() {
// Tag room as 'Favourite'
if (!this.state.isFavourite && this.state.isLowPriority) {
this.setState({
isFavourite: true,
isLowPriority: false,
});
this._toggleTag("m.favourite", "m.lowpriority");
} else if (this.state.isFavourite) {
this.setState({isFavourite: false});
this._toggleTag(null, "m.favourite");
} else if (!this.state.isFavourite) {
this.setState({isFavourite: true});
this._toggleTag("m.favourite");
}
},
_onClickLowPriority: function() {
// Tag room as 'Low Priority'
if (!this.state.isLowPriority && this.state.isFavourite) {
this.setState({
isFavourite: false,
isLowPriority: true,
});
this._toggleTag("m.lowpriority", "m.favourite");
} else if (this.state.isLowPriority) {
this.setState({isLowPriority: false});
this._toggleTag(null, "m.lowpriority");
} else if (!this.state.isLowPriority) {
this.setState({isLowPriority: true});
this._toggleTag("m.lowpriority");
}
},
_onClickDM: function() {
if (MatrixClientPeg.get().isGuest()) return;
const newIsDirectMessage = !this.state.isDirectMessage;
this.setState({
isDirectMessage: newIsDirectMessage,
});
Rooms.guessAndSetDMRoom(
this.props.room, newIsDirectMessage,
).then(sleep(500)).finally(() => {
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
}, (err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to set Direct Message status of room', '', ErrorDialog, {
title: _t('Failed to set Direct Message status of room'),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
},
_onClickLeave: function() {
// Leave room
dis.dispatch({
action: 'leave_room',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickReject: function() {
dis.dispatch({
action: 'reject_invite',
room_id: this.props.room.roomId,
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_onClickForget: function() {
// FIXME: duplicated with RoomSettings (and dead code in RoomView)
MatrixClientPeg.get().forget(this.props.room.roomId).then(() => {
// Switch to another room view if we're currently viewing the
// historical room
if (RoomViewStore.getRoomId() === this.props.room.roomId) {
dis.dispatch({ action: 'view_next_room' });
}
}, function(err) {
const errCode = err.errcode || _td("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createTrackedDialog('Failed to forget room', '', ErrorDialog, {
title: _t('Failed to forget room %(errCode)s', {errCode: errCode}),
description: ((err && err.message) ? err.message : _t('Operation failed')),
});
});
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
},
_saveNotifState: function(newState) {
if (MatrixClientPeg.get().isGuest()) return;
const oldState = this.state.roomNotifState;
const roomId = this.props.room.roomId;
this.setState({
roomNotifState: newState,
});
RoomNotifs.setRoomNotifsState(roomId, newState).then(() => {
// delay slightly so that the user can see their state change
// before closing the menu
return sleep(500).then(() => {
if (this._unmounted) return;
// Close the context menu
if (this.props.onFinished) {
this.props.onFinished();
}
});
}, (error) => {
// TODO: some form of error notification to the user
// to inform them that their state change failed.
// For now we at least set the state back
if (this._unmounted) return;
this.setState({
roomNotifState: oldState,
});
});
},
_onClickAlertMe: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES_LOUD);
},
_onClickAllNotifs: function() {
this._saveNotifState(RoomNotifs.ALL_MESSAGES);
},
_onClickMentions: function() {
this._saveNotifState(RoomNotifs.MENTIONS_ONLY);
},
_onClickMute: function() {
this._saveNotifState(RoomNotifs.MUTE);
},
_renderNotifMenu: function() {
return (
<div className="mx_RoomTileContextMenu" role="group" aria-label={_t("Notification settings")}>
<div className="mx_RoomTileContextMenu_notif_picker" role="presentation">
<img src={require("../../../../res/img/notif-slider.svg")} width="20" height="107" alt="" />
</div>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES_LOUD}
label={_t('All messages (noisy)')}
onClick={this._onClickAlertMe}
src={require("../../../../res/img/icon-context-mute-off-copy.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.ALL_MESSAGES}
label={_t('All messages')}
onClick={this._onClickAllNotifs}
src={require("../../../../res/img/icon-context-mute-off.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MENTIONS_ONLY}
label={_t('Mentions only')}
onClick={this._onClickMentions}
src={require("../../../../res/img/icon-context-mute-mentions.svg")}
/>
<NotifOption
active={this.state.roomNotifState === RoomNotifs.MUTE}
label={_t('Mute')}
onClick={this._onClickMute}
src={require("../../../../res/img/icon-context-mute.svg")}
/>
</div>
);
},
_onClickSettings: function() {
dis.dispatch({
action: 'open_room_settings',
room_id: this.props.room.roomId,
});
if (this.props.onFinished) {
this.props.onFinished();
}
},
_renderSettingsMenu: function() {
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_tag_field" onClick={this._onClickSettings}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/feather-customised/settings.svg")} width="15" height="15" alt="" />
{ _t('Settings') }
</MenuItem>
</div>
);
},
_renderLeaveMenu: function(membership) {
if (!membership) {
return null;
}
let leaveClickHandler = null;
let leaveText = null;
switch (membership) {
case "join":
leaveClickHandler = this._onClickLeave;
leaveText = _t('Leave');
break;
case "leave":
case "ban":
leaveClickHandler = this._onClickForget;
leaveText = _t('Forget');
break;
case "invite":
leaveClickHandler = this._onClickReject;
leaveText = _t('Reject');
break;
}
return (
<div>
<MenuItem className="mx_RoomTileContextMenu_leave" onClick={leaveClickHandler}>
<img className="mx_RoomTileContextMenu_tag_icon" src={require("../../../../res/img/icon_context_delete.svg")} width="15" height="15" alt="" />
{ leaveText }
</MenuItem>
</div>
);
},
_renderRoomTagMenu: function() {
return (
<div>
<RoomTagOption
active={this.state.isFavourite}
label={_t('Favourite')}
onClick={this._onClickFavourite}
src={require("../../../../res/img/icon_context_fave.svg")}
srcSet={require("../../../../res/img/icon_context_fave_on.svg")}
/>
<RoomTagOption
active={this.state.isLowPriority}
label={_t('Low Priority')}
onClick={this._onClickLowPriority}
src={require("../../../../res/img/icon_context_low.svg")}
srcSet={require("../../../../res/img/icon_context_low_on.svg")}
/>
<RoomTagOption
active={this.state.isDirectMessage}
label={_t('Direct Chat')}
onClick={this._onClickDM}
src={require("../../../../res/img/icon_context_person.svg")}
srcSet={require("../../../../res/img/icon_context_person_on.svg")}
/>
</div>
);
},
render: function() {
const myMembership = this.props.room.getMyMembership();
switch (myMembership) {
case 'join':
return <div>
{ this._renderNotifMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderRoomTagMenu() }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
case 'invite':
return <div>
{ this._renderLeaveMenu(myMembership) }
</div>;
default:
return <div>
{ this._renderLeaveMenu(myMembership) }
<hr className="mx_RoomTileContextMenu_separator" role="separator" />
{ this._renderSettingsMenu() }
</div>;
}
},
});

View file

@ -1,155 +0,0 @@
/*
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
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 dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
import { getHostingLink } from '../../../utils/HostingLink';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {MenuItem} from "../../structures/ContextMenu";
import * as sdk from "../../../index";
import {getHomePageUrl} from "../../../utils/pages";
import {Action} from "../../../dispatcher/actions";
export default class TopLeftMenu extends React.Component {
static propTypes = {
displayName: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
onFinished: PropTypes.func,
// Optional function to collect a reference to the container
// of this component directly.
containerRef: PropTypes.func,
};
constructor() {
super();
this.viewHomePage = this.viewHomePage.bind(this);
this.openSettings = this.openSettings.bind(this);
this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
}
hasHomePage() {
return !!getHomePageUrl(SdkConfig.get());
}
render() {
const isGuest = MatrixClientPeg.get().isGuest();
const hostingSignupLink = getHostingLink('user-context-menu');
let hostingSignup = null;
if (hostingSignupLink) {
hostingSignup = <div className="mx_TopLeftMenu_upgradeLink">
{_t(
"<a>Upgrade</a> to your own domain", {},
{
a: sub =>
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
},
)}
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
</div>;
}
let homePageItem = null;
if (this.hasHomePage()) {
homePageItem = (
<MenuItem className="mx_TopLeftMenu_icon_home" onClick={this.viewHomePage}>
{_t("Home")}
</MenuItem>
);
}
let signInOutItem;
if (isGuest) {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signin" onClick={this.signIn}>
{_t("Sign in")}
</MenuItem>
);
} else {
signInOutItem = (
<MenuItem className="mx_TopLeftMenu_icon_signout" onClick={this.signOut}>
{_t("Sign out")}
</MenuItem>
);
}
const helpItem = (
<MenuItem className="mx_TopLeftMenu_icon_help" onClick={this.openHelp}>
{_t("Help")}
</MenuItem>
);
const settingsItem = (
<MenuItem className="mx_TopLeftMenu_icon_settings" onClick={this.openSettings}>
{_t("Settings")}
</MenuItem>
);
return <div className="mx_TopLeftMenu" ref={this.props.containerRef} role="menu">
<div className="mx_TopLeftMenu_section_noIcon" aria-readonly={true} tabIndex={-1}>
<div>{this.props.displayName}</div>
<div className="mx_TopLeftMenu_greyedText" aria-hidden={true}>{this.props.userId}</div>
{hostingSignup}
</div>
<ul className="mx_TopLeftMenu_section_withIcon" role="none">
{homePageItem}
{settingsItem}
{helpItem}
{signInOutItem}
</ul>
</div>;
}
openHelp = () => {
this.closeMenu();
const RedesignFeedbackDialog = sdk.getComponent("views.dialogs.RedesignFeedbackDialog");
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
};
viewHomePage() {
dis.dispatch({action: 'view_home_page'});
this.closeMenu();
}
openSettings() {
dis.fire(Action.ViewUserSettings);
this.closeMenu();
}
signIn() {
dis.dispatch({action: 'start_login'});
this.closeMenu();
}
signOut() {
Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog);
this.closeMenu();
}
closeMenu() {
if (this.props.onFinished) this.props.onFinished();
}
}

View file

@ -0,0 +1,252 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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, { ChangeEvent, FormEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import { arrayFastClone } from "../../../utils/arrays";
import SdkConfig from "../../../SdkConfig";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import InviteDialog from "./InviteDialog";
import BaseAvatar from "../avatars/BaseAvatar";
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
import {inviteMultipleToRoom, showAnyInviteErrors} from "../../../RoomInvite";
import {humanizeTime} from "../../../utils/humanize";
import StyledCheckbox from "../elements/StyledCheckbox";
import Modal from "../../../Modal";
import ErrorDialog from "./ErrorDialog";
interface IProps extends IDialogProps {
roomId: string;
communityName: string;
}
interface IPerson {
userId: string;
user: RoomMember;
lastActive: number;
}
interface IState {
emailTargets: string[];
userTargets: string[];
showPeople: boolean;
people: IPerson[];
numPeople: number;
busy: boolean;
}
export default class CommunityPrototypeInviteDialog extends React.PureComponent<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
emailTargets: [],
userTargets: [],
showPeople: false,
people: this.buildSuggestions(),
numPeople: 5, // arbitrary default
busy: false,
};
}
private buildSuggestions(): IPerson[] {
const alreadyInvited = new Set([MatrixClientPeg.get().getUserId(), SdkConfig.get()['welcomeUserId']]);
if (this.props.roomId) {
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
if (!room) throw new Error("Room ID given to InviteDialog does not look like a room");
room.getMembersWithMembership('invite').forEach(m => alreadyInvited.add(m.userId));
room.getMembersWithMembership('join').forEach(m => alreadyInvited.add(m.userId));
// add banned users, so we don't try to invite them
room.getMembersWithMembership('ban').forEach(m => alreadyInvited.add(m.userId));
}
return InviteDialog.buildRecents(alreadyInvited);
}
private onSubmit = async (ev: FormEvent) => {
ev.preventDefault();
ev.stopPropagation();
this.setState({busy: true});
try {
const targets = [...this.state.emailTargets, ...this.state.userTargets];
const result = await inviteMultipleToRoom(this.props.roomId, targets);
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
const success = showAnyInviteErrors(result.states, room, result.inviter);
if (success) {
this.props.onFinished(true);
} else {
this.setState({busy: false});
}
} catch (e) {
this.setState({busy: false});
console.error(e);
Modal.createTrackedDialog('Failed to invite', '', ErrorDialog, {
title: _t("Failed to invite"),
description: ((e && e.message) ? e.message : _t("Operation failed")),
});
}
};
private onAddressChange = (ev: ChangeEvent<HTMLInputElement>, index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) {
targets.push(ev.target.value);
} else {
targets[index] = ev.target.value;
}
this.setState({emailTargets: targets});
};
private onAddressBlur = (index: number) => {
const targets = arrayFastClone(this.state.emailTargets);
if (index >= targets.length) return; // not important
if (targets[index].trim() === "") {
targets.splice(index, 1);
this.setState({emailTargets: targets});
}
};
private onShowPeopleClick = () => {
this.setState({showPeople: !this.state.showPeople});
};
private setPersonToggle = (person: IPerson, selected: boolean) => {
const targets = arrayFastClone(this.state.userTargets);
if (selected && !targets.includes(person.userId)) {
targets.push(person.userId);
} else if (!selected && targets.includes(person.userId)) {
targets.splice(targets.indexOf(person.userId), 1);
}
this.setState({userTargets: targets});
};
private renderPerson(person: IPerson, key: any) {
const avatarSize = 36;
return (
<div className="mx_CommunityPrototypeInviteDialog_person" key={key}>
<BaseAvatar
url={getHttpUriForMxc(
MatrixClientPeg.get().getHomeserverUrl(), person.user.getMxcAvatarUrl(),
avatarSize, avatarSize, "crop")}
name={person.user.name}
idName={person.user.userId}
width={avatarSize}
height={avatarSize}
/>
<div className="mx_CommunityPrototypeInviteDialog_personIdentifiers">
<span className="mx_CommunityPrototypeInviteDialog_personName">{person.user.name}</span>
<span className="mx_CommunityPrototypeInviteDialog_personId">{person.userId}</span>
</div>
<StyledCheckbox onChange={(e) => this.setPersonToggle(person, e.target.checked)} />
</div>
);
}
private onShowMorePeople = () => {
this.setState({numPeople: this.state.numPeople + 5}); // arbitrary increase
};
public render() {
const emailAddresses = [];
this.state.emailTargets.forEach((address, i) => {
emailAddresses.push(
<Field
key={i}
value={address}
onChange={(e) => this.onAddressChange(e, i)}
label={_t("Email address")}
placeholder={_t("Email address")}
onBlur={() => this.onAddressBlur(i)}
/>
);
});
// Push a clean input
emailAddresses.push(
<Field
key={emailAddresses.length}
value={""}
onChange={(e) => this.onAddressChange(e, emailAddresses.length)}
label={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
placeholder={emailAddresses.length > 0 ? _t("Add another email") : _t("Email address")}
/>
);
let peopleIntro = null;
let people = [];
if (this.state.showPeople) {
const humansToPresent = this.state.people.slice(0, this.state.numPeople);
humansToPresent.forEach((person, i) => {
people.push(this.renderPerson(person, i));
});
if (humansToPresent.length < this.state.people.length) {
people.push(
<AccessibleButton
onClick={this.onShowMorePeople}
kind="link" key="more"
className="mx_CommunityPrototypeInviteDialog_morePeople"
>{_t("Show more")}</AccessibleButton>
);
}
}
if (this.state.people.length > 0) {
peopleIntro = (
<div className="mx_CommunityPrototypeInviteDialog_people">
<span>{_t("People you know on %(brand)s", {brand: SdkConfig.get().brand})}</span>
<AccessibleButton onClick={this.onShowPeopleClick}>
{this.state.showPeople ? _t("Hide") : _t("Show")}
</AccessibleButton>
</div>
);
}
let buttonText = _t("Skip");
const targetCount = this.state.userTargets.length + this.state.emailTargets.length;
if (targetCount > 0) {
buttonText = _t("Send %(count)s invites", {count: targetCount});
}
return (
<BaseDialog
className="mx_CommunityPrototypeInviteDialog"
onFinished={this.props.onFinished}
title={_t("Invite people to join %(communityName)s", {communityName: this.props.communityName})}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
{emailAddresses}
{peopleIntro}
{people}
<AccessibleButton
kind="primary" onClick={this.onSubmit}
disabled={this.state.busy}
className="mx_CommunityPrototypeInviteDialog_primaryButton"
>{buttonText}</AccessibleButton>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,223 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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, { ChangeEvent } from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import Field from "../elements/Field";
import AccessibleButton from "../elements/AccessibleButton";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import InfoTooltip from "../elements/InfoTooltip";
import dis from "../../../dispatcher/dispatcher";
import {showCommunityRoomInviteDialog} from "../../../RoomInvite";
import GroupStore from "../../../stores/GroupStore";
interface IProps extends IDialogProps {
}
interface IState {
name: string;
localpart: string;
error: string;
busy: boolean;
avatarFile: File;
avatarPreview: string;
}
export default class CreateCommunityPrototypeDialog extends React.PureComponent<IProps, IState> {
private avatarUploadRef: React.RefObject<HTMLInputElement> = React.createRef();
constructor(props: IProps) {
super(props);
this.state = {
name: "",
localpart: "",
error: null,
busy: false,
avatarFile: null,
avatarPreview: null,
};
}
private onNameChange = (ev: ChangeEvent<HTMLInputElement>) => {
const localpart = (ev.target.value || "").toLowerCase().replace(/[^a-z0-9.\-_]/g, '-');
this.setState({name: ev.target.value, localpart});
};
private onSubmit = async (ev) => {
ev.preventDefault();
ev.stopPropagation();
if (this.state.busy) return;
// We'll create the community now to see if it's taken, leaving it active in
// the background for the user to look at while they invite people.
this.setState({busy: true});
try {
let avatarUrl = ''; // must be a string for synapse to accept it
if (this.state.avatarFile) {
avatarUrl = await MatrixClientPeg.get().uploadContent(this.state.avatarFile);
}
const result = await MatrixClientPeg.get().createGroup({
localpart: this.state.localpart,
profile: {
name: this.state.name,
avatar_url: avatarUrl,
},
});
// Ensure the tag gets selected now that we've created it
dis.dispatch({action: 'deselect_tags'}, true);
dis.dispatch({
action: 'select_tag',
tag: result.group_id,
});
// Close our own dialog before moving much further
this.props.onFinished(true);
if (result.room_id) {
// Force the group store to update as it might have missed the general chat
await GroupStore.refreshGroupRooms(result.group_id);
dis.dispatch({
action: 'view_room',
room_id: result.room_id,
});
showCommunityRoomInviteDialog(result.room_id, this.state.name);
} else {
dis.dispatch({
action: 'view_group',
group_id: result.group_id,
group_is_new: true,
});
}
} catch (e) {
console.error(e);
this.setState({
busy: false,
error: _t(
"There was an error creating your community. The name may be taken or the " +
"server is unable to process your request.",
),
});
}
};
private onAvatarChanged = (e: ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || !e.target.files.length) {
this.setState({avatarFile: null});
} else {
this.setState({busy: true});
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (ev: ProgressEvent<FileReader>) => {
this.setState({avatarFile: file, busy: false, avatarPreview: ev.target.result as string});
};
reader.readAsDataURL(file);
}
};
private onChangeAvatar = () => {
if (this.avatarUploadRef.current) this.avatarUploadRef.current.click();
};
public render() {
let communityId = null;
if (this.state.localpart) {
communityId = (
<span className="mx_CreateCommunityPrototypeDialog_communityId">
{_t("Community ID: +<localpart />:%(domain)s", {
domain: MatrixClientPeg.getHomeserverName(),
}, {
localpart: () => <u>{this.state.localpart}</u>,
})}
<InfoTooltip
tooltip={_t(
"Use this when referencing your community to others. The community ID " +
"cannot be changed.",
)}
/>
</span>
);
}
let helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{_t("You can change this later if needed.")}
</span>
);
if (this.state.error) {
helpText = (
<span className="mx_CreateCommunityPrototypeDialog_subtext mx_CreateCommunityPrototypeDialog_subtext_error">
{this.state.error}
</span>
);
}
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
}
return (
<BaseDialog
className="mx_CreateCommunityPrototypeDialog"
onFinished={this.props.onFinished}
title={_t("What's the name of your community or team?")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="mx_CreateCommunityPrototypeDialog_colName">
<Field
value={this.state.name}
onChange={this.onNameChange}
placeholder={_t("Enter name")}
label={_t("Enter name")}
/>
{helpText}
<span className="mx_CreateCommunityPrototypeDialog_subtext">
{/*nbsp is to reserve the height of this element when there's nothing*/}
&nbsp;{communityId}
</span>
<AccessibleButton kind="primary" onClick={this.onSubmit} disabled={this.state.busy}>
{_t("Create")}
</AccessibleButton>
</div>
<div className="mx_CreateCommunityPrototypeDialog_colAvatar">
<input
type="file" style={{display: "none"}}
ref={this.avatarUploadRef} accept="image/*"
onChange={this.onAvatarChanged}
/>
<AccessibleButton onClick={this.onChangeAvatar} className="mx_CreateCommunityPrototypeDialog_avatarContainer">
{preview}
</AccessibleButton>
<div className="mx_CreateCommunityPrototypeDialog_tip">
<b>{_t("Add image (optional)")}</b>
<span>
{_t("An image will help people identify your community.")}
</span>
</div>
</div>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -25,6 +25,8 @@ import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {Key} from "../../../Keyboard";
import {privateShouldBeEncrypted} from "../../../createRoom";
import TagOrderStore from "../../../stores/TagOrderStore";
import GroupStore from "../../../stores/GroupStore";
export default createReactClass({
displayName: 'CreateRoomDialog',
@ -70,6 +72,10 @@ export default createReactClass({
opts.encryption = this.state.isEncrypted;
}
if (TagOrderStore.getSelectedPrototypeTag()) {
opts.associatedWithCommunity = TagOrderStore.getSelectedPrototypeTag();
}
return opts;
},
@ -178,18 +184,25 @@ export default createReactClass({
const LabelledToggleSwitch = sdk.getComponent('views.elements.LabelledToggleSwitch');
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
let publicPrivateLabel;
let aliasField;
if (this.state.isPublic) {
publicPrivateLabel = (<p>{_t("Set a room address to easily share your room with other people.")}</p>);
const domain = MatrixClientPeg.get().getDomain();
aliasField = (
<div className="mx_CreateRoomDialog_aliasContainer">
<RoomAliasField ref={ref => this._aliasFieldRef = ref} onChange={this.onAliasChange} domain={domain} value={this.state.alias} />
</div>
);
} else {
publicPrivateLabel = (<p>{_t("This room is private, and can only be joined by invitation.")}</p>);
}
let publicPrivateLabel = <p>{_t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone.",
)}</p>;
if (TagOrderStore.getSelectedPrototypeTag()) {
publicPrivateLabel = <p>{_t(
"Private rooms can be found and joined by invitation only. Public rooms can be " +
"found and joined by anyone in this community.",
)}</p>;
}
let e2eeSection;
@ -212,7 +225,25 @@ export default createReactClass({
</React.Fragment>;
}
const title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
let federateLabel = _t(
"You might enable this if the room will only be used for collaborating with internal " +
"teams on your homeserver. This cannot be changed later.",
);
if (SdkConfig.get().default_federate === false) {
// We only change the label if the default setting is different to avoid jarring text changes to the
// user. They will have read the implications of turning this off/on, so no need to rephrase for them.
federateLabel = _t(
"You might disable this if the room will be used for collaborating with external " +
"teams who have their own homeserver. This cannot be changed later.",
);
}
let title = this.state.isPublic ? _t('Create a public room') : _t('Create a private room');
if (TagOrderStore.getSelectedPrototypeTag()) {
const summary = GroupStore.getSummary(TagOrderStore.getSelectedPrototypeTag());
const name = summary?.profile?.name || TagOrderStore.getSelectedPrototypeTag();
title = _t("Create a room in %(communityName)s", {communityName: name});
}
return (
<BaseDialog className="mx_CreateRoomDialog" onFinished={this.props.onFinished}
title={title}
@ -227,7 +258,15 @@ export default createReactClass({
{ aliasField }
<details ref={this.collectDetailsRef} className="mx_CreateRoomDialog_details">
<summary className="mx_CreateRoomDialog_details_summary">{ this.state.detailsOpen ? _t('Hide advanced') : _t('Show advanced') }</summary>
<LabelledToggleSwitch label={ _t('Block users on other matrix homeservers from joining this room (This setting cannot be changed later!)')} onChange={this.onNoFederateChange} value={this.state.noFederate} />
<LabelledToggleSwitch
label={_t(
"Block anyone not part of %(serverName)s from ever joining this room.",
{serverName: MatrixClientPeg.getHomeserverName()},
)}
onChange={this.onNoFederateChange}
value={this.state.noFederate}
/>
<p>{federateLabel}</p>
</details>
</div>
</form>

View file

@ -0,0 +1,19 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export interface IDialogProps {
onFinished: (bool) => void;
}

View file

@ -327,7 +327,7 @@ export default class InviteDialog extends React.PureComponent {
this.state = {
targets: [], // array of Member objects (see interface above)
filterText: "",
recents: this._buildRecents(alreadyInvited),
recents: InviteDialog.buildRecents(alreadyInvited),
numRecentsShown: INITIAL_ROOMS_SHOWN,
suggestions: this._buildSuggestions(alreadyInvited),
numSuggestionsShown: INITIAL_ROOMS_SHOWN,
@ -344,7 +344,7 @@ export default class InviteDialog extends React.PureComponent {
this._editorRef = createRef();
}
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
static buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
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

View file

@ -87,7 +87,7 @@ export default class RoomSettingsDialog extends React.Component {
<NotificationSettingsTab roomId={this.props.roomId} />,
));
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) {
if (SettingsStore.getValue("feature_bridge_state")) {
tabs.push(new Tab(
ROOM_BRIDGES_TAB,
_td("Bridges"),

View file

@ -27,9 +27,9 @@ import Spinner from "../elements/Spinner";
import AccessibleButton from "../elements/AccessibleButton";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { IDialogProps } from "./IDialogProps";
interface IProps {
onFinished: (bool) => void;
interface IProps extends IDialogProps {
}
export default class ServerOfflineDialog extends React.PureComponent<IProps> {

View file

@ -31,6 +31,7 @@ import {toRightOf} from "../../structures/ContextMenu";
import {copyPlaintext, selectText} from "../../../utils/strings";
import StyledCheckbox from '../elements/StyledCheckbox';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
import { IDialogProps } from "./IDialogProps";
const socials = [
{
@ -60,8 +61,7 @@ const socials = [
},
];
interface IProps {
onFinished: () => void;
interface IProps extends IDialogProps {
target: Room | User | Group | RoomMember | MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
}

View file

@ -54,7 +54,7 @@ export default class UserSettingsDialog extends React.Component {
super();
this.state = {
mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"),
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
};
}
@ -116,7 +116,7 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) {
if (SdkConfig.get()['showLabsSettings']) {
tabs.push(new Tab(
USER_LABS_TAB,
_td("Labs"),

View file

@ -311,7 +311,7 @@ export default class AppTile extends React.Component {
this.props.onEditClick();
} else {
// TODO: Open the right manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
'type_' + this.props.app.type,

View file

@ -0,0 +1,73 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.
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 classNames from 'classnames';
import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip';
import { _t } from "../../../languageHandler";
interface ITooltipProps {
tooltip?: React.ReactNode;
tooltipClassName?: string;
}
interface IState {
hover: boolean;
}
export default class InfoTooltip extends React.PureComponent<ITooltipProps, IState> {
constructor(props: ITooltipProps) {
super(props);
this.state = {
hover: false,
};
}
onMouseOver = () => {
this.setState({
hover: true,
});
};
onMouseLeave = () => {
this.setState({
hover: false,
});
};
render() {
const {tooltip, children, tooltipClassName} = this.props;
const title = _t("Information");
// Tooltip are forced on the right for a more natural feel to them on info icons
const tip = this.state.hover ? <Tooltip
className="mx_InfoTooltip_container"
tooltipClassName={classNames("mx_InfoTooltip_tooltip", tooltipClassName)}
label={tooltip || title}
forceOnRight={true}
/> : <div />;
return (
<div onMouseOver={this.onMouseOver} onMouseLeave={this.onMouseLeave} className="mx_InfoTooltip">
<span className="mx_InfoTooltip_icon" aria-label={title} />
{children}
{tip}
</div>
);
}
}

View file

@ -28,7 +28,7 @@ export default createReactClass({
const imgClass = this.props.imgClassName || "";
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg");
} else {
imageSource = require("../../../../res/img/spinner.gif");

View file

@ -34,7 +34,7 @@ export default class ManageIntegsButton extends React.Component {
if (!managers.hasManager()) {
managers.openNoManagerDialog();
} else {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
managers.openAll(this.props.room);
} else {
managers.getPrimaryManager().open(this.props.room);

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) {
if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg");
} else {
imageSource = require("../../../../res/img/spinner.gif");

View file

@ -30,6 +30,7 @@ import GroupStore from '../../../stores/GroupStore';
import TagOrderStore from '../../../stores/TagOrderStore';
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AccessibleButton from "./AccessibleButton";
import SettingsStore from "../../../settings/SettingsStore";
// A class for a child of TagPanel (possibly wrapped in a DNDTagTile) that represents
// a thing to click on for the user to filter the visible rooms in the RoomList to:
@ -46,6 +47,7 @@ export default createReactClass({
contextMenuButtonRef: PropTypes.object,
openMenu: PropTypes.func,
menuDisplayed: PropTypes.bool,
selected: PropTypes.bool,
},
statics: {
@ -112,6 +114,7 @@ export default createReactClass({
},
onMouseOver: function() {
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: true });
},
@ -123,6 +126,7 @@ export default createReactClass({
// Prevent the TagTile onClick event firing as well
e.stopPropagation();
e.preventDefault();
if (SettingsStore.getValue("feature_communities_v2_prototypes")) return;
this.setState({ hover: false });
this.props.openMenu();
},
@ -137,9 +141,12 @@ export default createReactClass({
profile.avatarUrl, avatarHeight, avatarHeight, "crop",
) : null;
const isPrototype = SettingsStore.getValue("feature_communities_v2_prototypes");
const className = classNames({
mx_TagTile: true,
mx_TagTile_selected: this.props.selected,
mx_TagTile_prototype: isPrototype,
mx_TagTile_selected: this.props.selected && !isPrototype,
mx_TagTile_selected_prototype: this.props.selected && isPrototype,
});
const badge = TagOrderStore.getGroupBadge(this.props.tag);

View file

@ -0,0 +1,85 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
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 defaultDispatcher from "../../../dispatcher/dispatcher";
import * as fbEmitter from "fbemitter";
import TagOrderStore from "../../../stores/TagOrderStore";
import AccessibleTooltipButton from "./AccessibleTooltipButton";
import classNames from "classnames";
import { _t } from "../../../languageHandler";
interface IProps {
}
interface IState {
selected: boolean;
}
export default class UserTagTile extends React.PureComponent<IProps, IState> {
private tagStoreRef: fbEmitter.EventSubscription;
constructor(props: IProps) {
super(props);
this.state = {
selected: TagOrderStore.getSelectedTags().length === 0,
};
}
public componentDidMount() {
this.tagStoreRef = TagOrderStore.addListener(this.onTagStoreUpdate);
}
public componentWillUnmount() {
this.tagStoreRef.remove();
}
private onTagStoreUpdate = () => {
const selected = TagOrderStore.getSelectedTags().length === 0;
this.setState({selected});
};
private onTileClick = (ev) => {
ev.preventDefault();
ev.stopPropagation();
// Deselect all tags
defaultDispatcher.dispatch({action: "deselect_tags"});
};
public render() {
// XXX: We reuse TagTile classes for ease of demonstration - we should probably generify
// TagTile instead if we continue to use this component.
const className = classNames({
mx_TagTile: true,
mx_TagTile_prototype: true,
mx_TagTile_selected_prototype: this.state.selected,
mx_TagTile_home: true,
});
return (
<AccessibleTooltipButton
className={className}
onClick={this.onTileClick}
title={_t("Home")}
>
<div className="mx_TagTile_avatar">
<div className="mx_TagTile_homeIcon" />
</div>
</AccessibleTooltipButton>
);
}
}

View file

@ -95,7 +95,7 @@ export default createReactClass({
}
}
if (SettingsStore.isFeatureEnabled("feature_mjolnir")) {
if (SettingsStore.getValue("feature_mjolnir")) {
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
const allowRender = localStorage.getItem(key) === "true";

View file

@ -1428,7 +1428,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
presenceLastActiveAgo = member.user.lastActiveAgo;
presenceCurrentlyActive = member.user.currentlyActive;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (SettingsStore.getValue("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage;
}
}

View file

@ -1,154 +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 createReactClass from 'create-react-class';
import Tinter from '../../../Tinter';
import dis from '../../../dispatcher/dispatcher';
import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel";
const ROOM_COLORS = [
// magic room default values courtesy of Ribot
[Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
["#81bddb", "#eaf1f4"],
["#bd79cb", "#f3eaf5"],
["#c65d94", "#f5eaef"],
["#e55e5e", "#f5eaea"],
["#eca46f", "#f5eeea"],
["#dad658", "#f5f4ea"],
["#80c553", "#eef5ea"],
["#bb814e", "#eee8e3"],
//["#595959", "#ececec"], // Grey makes everything appear disabled, so remove it for now
];
// Dev note: this component is not attached anywhere, but is left here as it
// has a high possibility of being used in the nearish future.
// Ref: https://github.com/vector-im/element-web/issues/8421
export default createReactClass({
displayName: 'ColorSettings',
propTypes: {
room: PropTypes.object.isRequired,
},
getInitialState: function() {
const data = {
index: 0,
primary_color: ROOM_COLORS[0][0],
secondary_color: ROOM_COLORS[0][1],
hasChanged: false,
};
const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
if (scheme.primary_color && scheme.secondary_color) {
// We only use the user's scheme if the scheme is valid.
data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color;
}
data.index = this._getColorIndex(data);
if (data.index === -1) {
// append the unrecognised colours to our palette
data.index = ROOM_COLORS.length;
ROOM_COLORS.push([
scheme.primary_color, scheme.secondary_color,
]);
}
return data;
},
saveSettings: function() { // : Promise
if (!this.state.hasChanged) {
return Promise.resolve(); // They didn't explicitly give a color to save.
}
const originalState = this.getInitialState();
if (originalState.primary_color !== this.state.primary_color ||
originalState.secondary_color !== this.state.secondary_color) {
console.log("ColorSettings: Saving new color");
// We would like guests to be able to set room colour but currently
// they can't, so we still send the request but display a sensible
// error if it fails.
// TODO: Support guests for room color. Technically this is possible via granular settings
// Granular settings would mean the guest is forced to use the DEVICE level though.
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
primary_color: this.state.primary_color,
secondary_color: this.state.secondary_color,
}).catch(function(err) {
if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'require_registration'});
}
});
}
return Promise.resolve(); // no color diff
},
_getColorIndex: function(scheme) {
if (!scheme || !scheme.primary_color || !scheme.secondary_color) {
return -1;
}
// XXX: we should validate these values
for (let i = 0; i < ROOM_COLORS.length; i++) {
const room_color = ROOM_COLORS[i];
if (room_color[0] === String(scheme.primary_color).toLowerCase() &&
room_color[1] === String(scheme.secondary_color).toLowerCase()) {
return i;
}
}
return -1;
},
_onColorSchemeChanged: function(index) {
// preview what the user just changed the scheme to.
Tinter.tint(ROOM_COLORS[index][0], ROOM_COLORS[index][1]);
this.setState({
index: index,
primary_color: ROOM_COLORS[index][0],
secondary_color: ROOM_COLORS[index][1],
hasChanged: true,
});
},
render: function() {
return (
<div className="mx_ColorSettings_roomColors">
{ ROOM_COLORS.map((room_color, i) => {
let selected;
if (i === this.state.index) {
selected = (
<div className="mx_ColorSettings_roomColor_selected">
<img src={require("../../../../res/img/tick.svg")} width="17" height="14" alt="./" />
</div>
);
}
const boundClick = this._onColorSchemeChanged.bind(this, i);
return (
<div className="mx_ColorSettings_roomColor"
key={"room_color_" + i}
style={{ backgroundColor: room_color[1] }}
onClick={boundClick}>
{ selected }
<div className="mx_ColorSettings_roomColorPrimary" style={{ backgroundColor: room_color[0] }}></div>
</div>
);
}) }
</div>
);
},
});

View file

@ -130,7 +130,7 @@ export default createReactClass({
},
_launchManageIntegrations: function() {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll();
} else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');

View file

@ -104,7 +104,7 @@ export default createReactClass({
},
_rateLimitedUpdate: new RateLimitedFunc(function() {
if (SettingsStore.isFeatureEnabled("feature_state_counters")) {
if (SettingsStore.getValue("feature_state_counters")) {
this.setState({counters: this._computeCounters()});
}
}, 500),
@ -112,7 +112,7 @@ export default createReactClass({
_computeCounters: function() {
let counters = [];
if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) {
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
stateEvs.sort((a, b) => {
return a.getStateKey() < b.getStateKey();
@ -206,7 +206,7 @@ export default createReactClass({
/>;
let stateViews = null;
if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) {
if (this.state.counters && SettingsStore.getValue("feature_state_counters")) {
let counters = [];
this.state.counters.forEach((counter, idx) => {

View file

@ -50,7 +50,7 @@ export default createReactClass({
componentDidMount() {
const cli = MatrixClientPeg.get();
if (SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (SettingsStore.getValue("feature_custom_status")) {
const { user } = this.props.member;
if (user) {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
@ -209,7 +209,7 @@ export default createReactClass({
const presenceState = member.user ? member.user.presence : null;
let statusMessage = null;
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) {
if (member.user && SettingsStore.getValue("feature_custom_status")) {
statusMessage = this.state.statusMessage;
}

View file

@ -226,7 +226,7 @@ export default createReactClass({
title={_t("Settings")} />;
}
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null;
if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);

View file

@ -45,6 +45,7 @@ import { arrayFastClone, arrayHasDiff } from "../../../utils/arrays";
import { objectShallowClone, objectWithOnly } from "../../../utils/objects";
import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../context_menus/IconizedContextMenu";
import AccessibleButton from "../elements/AccessibleButton";
import TagOrderStore from "../../../stores/TagOrderStore";
interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void;
@ -129,7 +130,9 @@ const TAG_AESTHETICS: {
}}
/>
<IconizedContextMenuOption
label={_t("Explore public rooms")}
label={TagOrderStore.getSelectedPrototypeTag()
? _t("Explore community rooms")
: _t("Explore public rooms")}
iconClassName="mx_RoomList_iconExplore"
onClick={(e) => {
e.preventDefault();

View file

@ -26,6 +26,8 @@ import classNames from 'classnames';
import { _t } from '../../../languageHandler';
import SdkConfig from "../../../SdkConfig";
import IdentityAuthClient from '../../../IdentityAuthClient';
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
const MessageCase = Object.freeze({
NotLoggedIn: "NotLoggedIn",
@ -100,6 +102,7 @@ export default createReactClass({
componentDidMount: function() {
this._checkInvitedEmail();
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this._onCommunityUpdate);
},
componentDidUpdate: function(prevProps, prevState) {
@ -108,6 +111,10 @@ export default createReactClass({
}
},
componentWillUnmount: function() {
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this._onCommunityUpdate);
},
_checkInvitedEmail: async function() {
// If this is an invite and we've been told what email address was
// invited, fetch the user's account emails and discovery bindings so we
@ -143,6 +150,13 @@ export default createReactClass({
}
},
_onCommunityUpdate: function (roomId) {
if (this.props.room && this.props.room.roomId !== roomId) {
return;
}
this.forceUpdate(); // we have nothing to update
},
_getMessageCase() {
const isGuest = MatrixClientPeg.get().isGuest();
@ -219,8 +233,15 @@ export default createReactClass({
}
},
_communityProfile: function() {
if (this.props.room) return CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
return {displayName: null, avatarMxc: null};
},
_roomName: function(atStart = false) {
const name = this.props.room ? this.props.room.name : this.props.roomAlias;
let name = this.props.room ? this.props.room.name : this.props.roomAlias;
const profile = this._communityProfile();
if (profile.displayName) name = profile.displayName;
if (name) {
return name;
} else if (atStart) {
@ -439,7 +460,10 @@ export default createReactClass({
}
case MessageCase.Invite: {
const RoomAvatar = sdk.getComponent("views.avatars.RoomAvatar");
const avatar = <RoomAvatar room={this.props.room} oobData={this.props.oobData} />;
const oobData = Object.assign({}, this.props.oobData, {
avatarUrl: this._communityProfile().avatarMxc,
});
const avatar = <RoomAvatar room={this.props.room} oobData={oobData} />;
const inviteMember = this._getInviteMember();
let inviterElement;

View file

@ -27,7 +27,7 @@ import defaultDispatcher from '../../../dispatcher/dispatcher';
import { Key } from "../../../Keyboard";
import ActiveRoomObserver from "../../../ActiveRoomObserver";
import { _t } from "../../../languageHandler";
import { ChevronFace, ContextMenuTooltipButton, MenuItemRadio } from "../../structures/ContextMenu";
import { ChevronFace, ContextMenuTooltipButton } from "../../structures/ContextMenu";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import { MessagePreviewStore, ROOM_PREVIEW_CHANGED } from "../../../stores/room-list/MessagePreviewStore";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
@ -47,8 +47,11 @@ import { PROPERTY_UPDATED } from "../../../stores/local-echo/GenericEchoChamber"
import IconizedContextMenu, {
IconizedContextMenuCheckbox,
IconizedContextMenuOption,
IconizedContextMenuOptionList, IconizedContextMenuRadio
IconizedContextMenuOptionList,
IconizedContextMenuRadio
} from "../context_menus/IconizedContextMenu";
import { CommunityPrototypeStore, IRoomProfile } from "../../../stores/CommunityPrototypeStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
interface IProps {
room: Room;
@ -101,6 +104,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
this.notificationState.on(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
this.roomProps = EchoChamber.forRoom(this.props.room);
this.roomProps.on(PROPERTY_UPDATED, this.onRoomPropertyUpdate);
CommunityPrototypeStore.instance.on(UPDATE_EVENT, this.onCommunityUpdate);
}
private onNotificationUpdate = () => {
@ -140,6 +144,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef);
MessagePreviewStore.instance.off(ROOM_PREVIEW_CHANGED, this.onRoomPreviewChanged);
this.notificationState.off(NOTIFICATION_STATE_UPDATE, this.onNotificationUpdate);
CommunityPrototypeStore.instance.off(UPDATE_EVENT, this.onCommunityUpdate);
}
private onAction = (payload: ActionPayload) => {
@ -150,6 +155,11 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
}
};
private onCommunityUpdate = (roomId: string) => {
if (roomId !== this.props.room.roomId) return;
this.forceUpdate(); // we don't have anything to actually update
};
private onRoomPreviewChanged = (room: Room) => {
if (this.props.room && room.roomId === this.props.room.roomId) {
// generatePreview() will return nothing if the user has previews disabled
@ -461,11 +471,21 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
'mx_RoomTile_minimized': this.props.isMinimized,
});
let roomProfile: IRoomProfile = {displayName: null, avatarMxc: null};
if (this.props.tag === DefaultTagID.Invite) {
roomProfile = CommunityPrototypeStore.instance.getInviteProfile(this.props.room.roomId);
}
let name = roomProfile.displayName || this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
const roomAvatar = <DecoratedRoomAvatar
room={this.props.room}
avatarSize={32}
tag={this.props.tag}
displayBadge={this.props.isMinimized}
oobData={({avatarUrl: roomProfile.avatarMxc})}
/>;
let badge: React.ReactNode;
@ -482,10 +502,6 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
);
}
let name = this.props.room.name;
if (typeof name !== 'string') name = '';
name = name.replace(":", ":\u200b"); // add a zero-width space to allow linewrapping after the colon
let messagePreview = null;
if (this.showMessagePreview && this.state.messagePreview) {
messagePreview = (

View file

@ -363,7 +363,7 @@ export default class Stickerpicker extends React.Component {
*/
_launchManageIntegrations() {
// TODO: Open the right integration manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) {
if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(
this.props.room,
`type_${WidgetType.STICKERPICKER.preferred}`,

View file

@ -89,6 +89,7 @@ export default class CrossSigningPanel extends React.PureComponent {
const homeserverSupportsCrossSigning =
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageReady = await cli.isSecretStorageReady();
this.setState({
crossSigningPublicKeysOnDevice,
@ -101,6 +102,7 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageReady,
});
}
@ -151,6 +153,7 @@ export default class CrossSigningPanel extends React.PureComponent {
secretStorageKeyInAccount,
homeserverSupportsCrossSigning,
crossSigningReady,
secretStorageReady,
} = this.state;
let errorSection;
@ -166,14 +169,19 @@ export default class CrossSigningPanel extends React.PureComponent {
summarisedStatus = <p>{_t(
"Your homeserver does not support cross-signing.",
)}</p>;
} else if (crossSigningReady) {
} else if (crossSigningReady && secretStorageReady) {
summarisedStatus = <p> {_t(
"Cross-signing and secret storage are enabled.",
"Cross-signing and secret storage are ready for use.",
)}</p>;
} else if (crossSigningReady && !secretStorageReady) {
summarisedStatus = <p> {_t(
"Cross-signing is ready for use, but secret storage is " +
"currently not being used to backup your keys.",
)}</p>;
} else if (crossSigningPrivateKeysInStorage) {
summarisedStatus = <p>{_t(
"Your account has a cross-signing identity in secret storage, but it " +
"is not yet trusted by this session.",
"Your account has a cross-signing identity in secret storage, " +
"but it is not yet trusted by this session.",
)}</p>;
} else {
summarisedStatus = <p>{_t(

View file

@ -21,6 +21,7 @@ import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { isSecureBackupRequired } from '../../../utils/WellKnownUtils';
export default class KeyBackupPanel extends React.PureComponent {
constructor(props) {
@ -315,14 +316,19 @@ export default class KeyBackupPanel extends React.PureComponent {
trustedLocally = _t("This backup is trusted because it has been restored on this session");
}
let deleteBackupButton;
if (!isSecureBackupRequired()) {
deleteBackupButton = <AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>;
}
const buttonRow = (
<div className="mx_KeyBackupPanel_buttonRow">
<AccessibleButton kind="primary" onClick={this._restoreBackup}>
{restoreButtonCaption}
</AccessibleButton>&nbsp;&nbsp;&nbsp;
<AccessibleButton kind="danger" onClick={this._deleteBackup}>
{_t("Delete Backup")}
</AccessibleButton>
{deleteBackupButton}
</div>
);

View file

@ -237,7 +237,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
}
let customThemeForm: JSX.Element;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) {
if (SettingsStore.getValue("feature_custom_themes")) {
let messageElement = null;
if (this.state.customThemeMessage.text) {
if (this.state.customThemeMessage.isError) {

View file

@ -28,14 +28,15 @@ export class LabsSettingToggle extends React.Component {
};
_onChange = async (checked) => {
await SettingsStore.setFeatureEnabled(this.props.featureId, checked);
await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
this.forceUpdate();
};
render() {
const label = SettingsStore.getDisplayName(this.props.featureId);
const value = SettingsStore.isFeatureEnabled(this.props.featureId);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} />;
const value = SettingsStore.getValue(this.props.featureId);
const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} disabled={!canChange} />;
}
}
@ -46,7 +47,7 @@ export default class LabsUserSettingsTab extends React.Component {
render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = SettingsStore.getLabsFeatures().map(f => <LabsSettingToggle featureId={f} key={f} />);
const flags = SettingsStore.getFeatureSettingNames().map(f => <LabsSettingToggle featureId={f} key={f} />);
return (
<div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div>

View file

@ -157,6 +157,9 @@ export default class VoiceUserSettingsTab extends React.Component {
label: _t('Default Device'),
};
const getDefaultDevice = (devices) => {
// Note we're looking for a device with deviceId 'default' but adding a device
// with deviceId == the empty string: this is because Chrome gives us a device
// with deviceId 'default', so we're looking for this, not the one we are adding.
if (!devices.some((i) => i.deviceId === 'default')) {
devices.unshift(defaultOption);
return '';