Merge branch 'develop' of git+ssh://github.com/matrix-org/matrix-react-sdk into develop
This commit is contained in:
commit
c02d03cc5b
146 changed files with 8365 additions and 1072 deletions
|
@ -24,7 +24,7 @@ import Modal from '../../../Modal';
|
|||
import * as Avatar from '../../../Avatar';
|
||||
import {ResizeMethod} from "../../../Avatar";
|
||||
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick">{
|
||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||
// Room may be left unset here, but if it is,
|
||||
// oobData.avatarUrl should be set (else there
|
||||
// would be nowhere to get the avatar from)
|
||||
|
|
208
src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Normal file
208
src/components/views/dialogs/AddExistingToSpaceDialog.tsx
Normal file
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
Copyright 2021 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, {useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Dropdown from "../elements/Dropdown";
|
||||
import SearchBox from "../../structures/SearchBox";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {getDisplayAliasForRoom} from "../../../Rooms";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
onCreateRoomClick(cli: MatrixClient, space: Room): void;
|
||||
}
|
||||
|
||||
const Entry = ({ room, checked, onChange }) => {
|
||||
return <div className="mx_AddExistingToSpaceDialog_entry">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<span className="mx_AddExistingToSpaceDialog_entry_name">{ room.name }</span>
|
||||
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space, onCreateRoomClick, onFinished }) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const lcQuery = query.toLowerCase();
|
||||
|
||||
const [selectedSpace, setSelectedSpace] = useState(space);
|
||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||
|
||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const existingSubspacesSet = new Set(existingSubspaces);
|
||||
const spaces = SpaceStore.instance.getSpaces().filter(s => {
|
||||
return !existingSubspacesSet.has(s) // not already in space
|
||||
&& space !== s // not the top-level space
|
||||
&& selectedSpace !== s // not the selected space
|
||||
&& s.name.toLowerCase().includes(lcQuery); // contains query
|
||||
});
|
||||
|
||||
const existingRooms = SpaceStore.instance.getChildRooms(space.roomId);
|
||||
const existingRoomsSet = new Set(existingRooms);
|
||||
const rooms = cli.getVisibleRooms().filter(room => {
|
||||
return !existingRoomsSet.has(room) // not already in space
|
||||
&& room.name.toLowerCase().includes(lcQuery) // contains query
|
||||
&& !DMRoomMap.shared().getUserIdForRoomId(room.roomId); // not a DM
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
let spaceOptionSection;
|
||||
if (existingSubspacesSet.size > 0) {
|
||||
const options = [space, ...existingSubspaces].map((space) => {
|
||||
const classes = classNames("mx_AddExistingToSpaceDialog_dropdownOption", {
|
||||
mx_AddExistingToSpaceDialog_dropdownOptionActive: space === selectedSpace,
|
||||
});
|
||||
return <div key={space.roomId} className={classes}>
|
||||
<RoomAvatar room={space} width={24} height={24} />
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
});
|
||||
|
||||
spaceOptionSection = (
|
||||
<Dropdown
|
||||
id="mx_SpaceSelectDropdown"
|
||||
onOptionChange={(key: string) => {
|
||||
setSelectedSpace(existingSubspaces.find(space => space.roomId === key) || space);
|
||||
}}
|
||||
value={selectedSpace.roomId}
|
||||
label={_t("Space selection")}
|
||||
>
|
||||
{ options }
|
||||
</Dropdown>
|
||||
);
|
||||
} else {
|
||||
spaceOptionSection = <div className="mx_AddExistingToSpaceDialog_onlySpace">
|
||||
{ space.name || getDisplayAliasForRoom(space) || space.roomId }
|
||||
</div>;
|
||||
}
|
||||
|
||||
const title = <React.Fragment>
|
||||
<RoomAvatar room={selectedSpace} height={40} width={40} />
|
||||
<div>
|
||||
<h1>{ _t("Add existing spaces/rooms") }</h1>
|
||||
{ spaceOptionSection }
|
||||
</div>
|
||||
</React.Fragment>;
|
||||
|
||||
return <BaseDialog
|
||||
title={title}
|
||||
className="mx_AddExistingToSpaceDialog"
|
||||
contentId="mx_AddExistingToSpaceDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
|
||||
|
||||
<SearchBox
|
||||
className="mx_textinput_icon mx_textinput_search"
|
||||
placeholder={ _t("Filter your rooms and spaces") }
|
||||
onSearch={setQuery}
|
||||
/>
|
||||
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
|
||||
{ spaces.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
|
||||
<h3>{ _t("Spaces") }</h3>
|
||||
{ spaces.map(space => {
|
||||
return <Entry
|
||||
key={space.roomId}
|
||||
room={space}
|
||||
checked={selectedToAdd.has(space)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(space);
|
||||
} else {
|
||||
selectedToAdd.delete(space);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : null }
|
||||
|
||||
{ rooms.length > 0 ? (
|
||||
<div className="mx_AddExistingToSpaceDialog_section">
|
||||
<h3>{ _t("Rooms") }</h3>
|
||||
{ rooms.map(room => {
|
||||
return <Entry
|
||||
key={room.roomId}
|
||||
room={room}
|
||||
checked={selectedToAdd.has(room)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
selectedToAdd.add(room);
|
||||
} else {
|
||||
selectedToAdd.delete(room);
|
||||
}
|
||||
setSelectedToAdd(new Set(selectedToAdd));
|
||||
}}
|
||||
/>;
|
||||
}) }
|
||||
</div>
|
||||
) : undefined }
|
||||
|
||||
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
|
||||
{ _t("No results") }
|
||||
</span> : undefined }
|
||||
</AutoHideScrollbar>
|
||||
|
||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||
<span>
|
||||
<div>{ _t("Don't want to add an existing room?") }</div>
|
||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||
{ _t("Create a new room") }
|
||||
</AccessibleButton>
|
||||
</span>
|
||||
|
||||
<FormButton
|
||||
label={busy ? _t("Applying...") : _t("Apply")}
|
||||
disabled={busy || selectedToAdd.size < 1}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
||||
onFinished(true);
|
||||
} catch (e) {
|
||||
console.error("Failed to add rooms to space", e);
|
||||
setError(_t("Failed to add rooms to space"));
|
||||
}
|
||||
setBusy(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default AddExistingToSpaceDialog;
|
||||
|
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
import withValidation from '../elements/Validation';
|
||||
|
@ -30,6 +32,7 @@ export default class CreateRoomDialog extends React.Component {
|
|||
static propTypes = {
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
defaultPublic: PropTypes.bool,
|
||||
parentSpace: PropTypes.instanceOf(Room),
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -85,6 +88,10 @@ export default class CreateRoomDialog extends React.Component {
|
|||
opts.associatedWithCommunity = CommunityPrototypeStore.instance.getSelectedCommunityId();
|
||||
}
|
||||
|
||||
if (this.props.parentSpace) {
|
||||
opts.parentSpace = this.props.parentSpace;
|
||||
}
|
||||
|
||||
return opts;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ export default class InfoDialog extends React.Component {
|
|||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.node,
|
||||
button: PropTypes.string,
|
||||
button: PropTypes.oneOfType(PropTypes.string, PropTypes.bool),
|
||||
onFinished: PropTypes.func,
|
||||
hasCloseButton: PropTypes.bool,
|
||||
onKeyDown: PropTypes.func,
|
||||
|
@ -60,11 +60,11 @@ export default class InfoDialog extends React.Component {
|
|||
<div className={classNames("mx_Dialog_content", this.props.className)} id="mx_Dialog_content">
|
||||
{ this.props.description }
|
||||
</div>
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
{ this.props.button !== false && <DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
onPrimaryButtonClick={this.onFinished}
|
||||
hasCancel={false}
|
||||
>
|
||||
</DialogButtons>
|
||||
</DialogButtons> }
|
||||
</BaseDialog>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React, {createRef} from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {_t, _td} from "../../../languageHandler";
|
||||
import * as sdk from "../../../index";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {makeRoomPermalink, makeUserPermalink} from "../../../utils/permalinks/Permalinks";
|
||||
|
@ -48,6 +48,7 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
|||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
export const KIND_SPACE_INVITE = "space_invite";
|
||||
export const KIND_CALL_TRANSFER = "call_transfer";
|
||||
|
||||
const INITIAL_ROOMS_SHOWN = 3; // Number of rooms to show at first
|
||||
|
@ -309,7 +310,7 @@ interface IInviteDialogProps {
|
|||
// not provided.
|
||||
kind: string,
|
||||
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE.
|
||||
// The room ID this dialog is for. Only required for KIND_INVITE and KIND_SPACE_INVITE.
|
||||
roomId: string,
|
||||
|
||||
// The call to transfer. Only required for KIND_CALL_TRANSFER.
|
||||
|
@ -348,8 +349,8 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
if (props.kind === KIND_INVITE && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE a roomId is required for an InviteDialog");
|
||||
if ((props.kind === KIND_INVITE || props.kind === KIND_SPACE_INVITE) && !props.roomId) {
|
||||
throw new Error("When using KIND_INVITE or KIND_SPACE_INVITE a roomId is required for an InviteDialog");
|
||||
} else if (props.kind === KIND_CALL_TRANSFER && !props.call) {
|
||||
throw new Error("When using KIND_CALL_TRANSFER a call is required for an InviteDialog");
|
||||
}
|
||||
|
@ -1026,7 +1027,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
sectionSubname = _t("May include members not in %(communityName)s", {communityName});
|
||||
}
|
||||
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
sectionName = kind === 'recents' ? _t("Recently Direct Messaged") : _t("Suggestions");
|
||||
}
|
||||
|
||||
|
@ -1247,38 +1248,35 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
|||
}
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
} else if (this.props.kind === KIND_INVITE) {
|
||||
title = _t("Invite to this room");
|
||||
} else if (this.props.kind === KIND_INVITE || this.props.kind === KIND_SPACE_INVITE) {
|
||||
title = this.props.kind === KIND_INVITE ? _t("Invite to this room") : _t("Invite to this space");
|
||||
|
||||
if (identityServersEnabled) {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, email address, username (like <userId/>) or " +
|
||||
"<a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
helpText = _t(
|
||||
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
|
||||
{},
|
||||
{
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
);
|
||||
let helpTextUntranslated;
|
||||
if (this.props.kind === KIND_INVITE) {
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this room</a>.");
|
||||
}
|
||||
} else { // KIND_SPACE_INVITE
|
||||
if (identityServersEnabled) {
|
||||
helpTextUntranslated = _td("Invite someone using their name, email address, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
} else {
|
||||
helpTextUntranslated = _td("Invite someone using their name, username " +
|
||||
"(like <userId/>) or <a>share this space</a>.");
|
||||
}
|
||||
}
|
||||
|
||||
helpText = _t(helpTextUntranslated, {}, {
|
||||
userId: () =>
|
||||
<a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{userId}</a>,
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
});
|
||||
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
} else if (this.props.kind === KIND_CALL_TRANSFER) {
|
||||
|
|
162
src/components/views/dialogs/SpaceSettingsDialog.tsx
Normal file
162
src/components/views/dialogs/SpaceSettingsDialog.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
Copyright 2021 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, {useState} from 'react';
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {_t} from '../../../languageHandler';
|
||||
import {IDialogProps} from "./IDialogProps";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import DevtoolsDialog from "./DevtoolsDialog";
|
||||
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
|
||||
import {getTopic} from "../elements/RoomTopic";
|
||||
import {avatarUrlForRoom} from "../../../Avatar";
|
||||
import ToggleSwitch from "../elements/ToggleSwitch";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import Modal from "../../../Modal";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import {allSettled} from "../../../utils/promise";
|
||||
import {useDispatcher} from "../../../hooks/useDispatcher";
|
||||
|
||||
interface IProps extends IDialogProps {
|
||||
matrixClient: MatrixClient;
|
||||
space: Room;
|
||||
}
|
||||
|
||||
const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFinished }) => {
|
||||
useDispatcher(defaultDispatcher, ({action, ...params}) => {
|
||||
if (action === "after_leave_room" && params.room_id === space.roomId) {
|
||||
onFinished(false);
|
||||
}
|
||||
});
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
const userId = cli.getUserId();
|
||||
|
||||
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar
|
||||
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId);
|
||||
const avatarChanged = newAvatar !== null;
|
||||
|
||||
const [name, setName] = useState<string>(space.name);
|
||||
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId);
|
||||
const nameChanged = name !== space.name;
|
||||
|
||||
const currentTopic = getTopic(space);
|
||||
const [topic, setTopic] = useState<string>(currentTopic);
|
||||
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId);
|
||||
const topicChanged = topic !== currentTopic;
|
||||
|
||||
const currentJoinRule = space.getJoinRule();
|
||||
const [joinRule, setJoinRule] = useState(currentJoinRule);
|
||||
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId);
|
||||
const joinRuleChanged = joinRule !== currentJoinRule;
|
||||
|
||||
const onSave = async () => {
|
||||
setBusy(true);
|
||||
const promises = [];
|
||||
|
||||
if (avatarChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
|
||||
url: await cli.uploadContent(newAvatar),
|
||||
}, ""));
|
||||
}
|
||||
|
||||
if (nameChanged) {
|
||||
promises.push(cli.setRoomName(space.roomId, name));
|
||||
}
|
||||
|
||||
if (topicChanged) {
|
||||
promises.push(cli.setRoomTopic(space.roomId, topic));
|
||||
}
|
||||
|
||||
if (joinRuleChanged) {
|
||||
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
|
||||
}
|
||||
|
||||
const results = await allSettled(promises);
|
||||
setBusy(false);
|
||||
const failures = results.filter(r => r.status === "rejected");
|
||||
if (failures.length > 0) {
|
||||
console.error("Failed to save space settings: ", failures);
|
||||
setError(_t("Failed to save space settings."));
|
||||
}
|
||||
};
|
||||
|
||||
return <BaseDialog
|
||||
title={_t("Space settings")}
|
||||
className="mx_SpaceSettingsDialog"
|
||||
contentId="mx_SpaceSettingsDialog"
|
||||
onFinished={onFinished}
|
||||
fixedWidth={false}
|
||||
>
|
||||
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog">
|
||||
<div>{ _t("Edit settings relating to your space.") }</div>
|
||||
|
||||
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> }
|
||||
|
||||
<SpaceBasicSettings
|
||||
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
|
||||
avatarDisabled={!canSetAvatar}
|
||||
setAvatar={setNewAvatar}
|
||||
name={name}
|
||||
nameDisabled={!canSetName}
|
||||
setName={setName}
|
||||
topic={topic}
|
||||
topicDisabled={!canSetTopic}
|
||||
setTopic={setTopic}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{ _t("Make this space private") }
|
||||
<ToggleSwitch
|
||||
checked={joinRule === "private"}
|
||||
onChange={checked => setJoinRule(checked ? "private" : "invite")}
|
||||
disabled={!canSetJoinRule}
|
||||
aria-label={_t("Make this space private")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormButton
|
||||
kind="danger"
|
||||
label={_t("Leave Space")}
|
||||
onClick={() => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: space.roomId,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="mx_SpaceSettingsDialog_buttons">
|
||||
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
|
||||
{ _t("View dev tools") }
|
||||
</AccessibleButton>
|
||||
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
|
||||
{ _t("Cancel") }
|
||||
</AccessibleButton>
|
||||
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} />
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>;
|
||||
};
|
||||
|
||||
export default SpaceSettingsDialog;
|
||||
|
|
@ -100,10 +100,10 @@ export default class LanguageDropdown extends React.Component {
|
|||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown
|
||||
|
|
|
@ -31,6 +31,7 @@ export default class PersistentApp extends React.Component {
|
|||
componentDidMount() {
|
||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||
ActiveWidgetStore.on('update', this._onActiveWidgetStoreUpdate);
|
||||
MatrixClientPeg.get().on("Room.myMembership", this._onMyMembership);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -38,6 +39,9 @@ export default class PersistentApp extends React.Component {
|
|||
this._roomStoreToken.remove();
|
||||
}
|
||||
ActiveWidgetStore.removeListener('update', this._onActiveWidgetStoreUpdate);
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener("Room.myMembership", this._onMyMembership);
|
||||
}
|
||||
}
|
||||
|
||||
_onRoomViewStoreUpdate = payload => {
|
||||
|
@ -53,16 +57,28 @@ export default class PersistentApp extends React.Component {
|
|||
});
|
||||
};
|
||||
|
||||
_onMyMembership = async (room, membership) => {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
if (membership !== "join") {
|
||||
// we're not in the room anymore - delete
|
||||
if (room.roomId === persistentWidgetInRoomId) {
|
||||
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.persistentWidgetId) {
|
||||
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId);
|
||||
if (this.state.roomId !== persistentWidgetInRoomId) {
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||
// thus no room is associated anymore.
|
||||
if (!persistentWidgetInRoom) return null;
|
||||
const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId);
|
||||
|
||||
// Sanity check the room - the widget may have been destroyed between render cycles, and
|
||||
// thus no room is associated anymore.
|
||||
if (!persistentWidgetInRoom) return null;
|
||||
|
||||
const myMembership = persistentWidgetInRoom.getMyMembership();
|
||||
if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
|
||||
// get the widget data
|
||||
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
|
||||
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId();
|
||||
|
|
126
src/components/views/elements/SpellCheckLanguagesDropdown.tsx
Normal file
126
src/components/views/elements/SpellCheckLanguagesDropdown.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 Dropdown from "../../views/elements/Dropdown"
|
||||
import * as sdk from '../../../index';
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
function languageMatchesSearchQuery(query, language) {
|
||||
if (language.label.toUpperCase().includes(query.toUpperCase())) return true;
|
||||
if (language.value.toUpperCase() === query.toUpperCase()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIProps {
|
||||
className: string,
|
||||
value: string,
|
||||
onOptionChange(language: string),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesDropdownIState {
|
||||
searchQuery: string,
|
||||
languages: any,
|
||||
}
|
||||
|
||||
export default class SpellCheckLanguagesDropdown extends React.Component<SpellCheckLanguagesDropdownIProps,
|
||||
SpellCheckLanguagesDropdownIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this._onSearchChange = this._onSearchChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
searchQuery: '',
|
||||
languages: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
plaf.getAvailableSpellCheckLanguages().then((languages) => {
|
||||
languages.sort(function(a, b) {
|
||||
if (a < b) return -1;
|
||||
if (a > b) return 1;
|
||||
return 0;
|
||||
});
|
||||
const langs = [];
|
||||
languages.forEach((language) => {
|
||||
langs.push({
|
||||
label: language,
|
||||
value: language,
|
||||
})
|
||||
})
|
||||
this.setState({languages: langs});
|
||||
}).catch((e) => {
|
||||
this.setState({languages: ['en']});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onSearchChange(search) {
|
||||
this.setState({
|
||||
searchQuery: search,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.languages === null) {
|
||||
const Spinner = sdk.getComponent('elements.Spinner');
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
let displayedLanguages;
|
||||
if (this.state.searchQuery) {
|
||||
displayedLanguages = this.state.languages.filter((lang) => {
|
||||
return languageMatchesSearchQuery(this.state.searchQuery, lang);
|
||||
});
|
||||
} else {
|
||||
displayedLanguages = this.state.languages;
|
||||
}
|
||||
|
||||
const options = displayedLanguages.map((language) => {
|
||||
return <div key={language.value}>
|
||||
{ language.label }
|
||||
</div>;
|
||||
});
|
||||
|
||||
// default value here too, otherwise we need to handle null / undefined;
|
||||
// values between mounting and the initial value propgating
|
||||
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
|
||||
let value = null;
|
||||
if (language) {
|
||||
value = this.props.value || language;
|
||||
} else {
|
||||
language = navigator.language || navigator.userLanguage;
|
||||
value = this.props.value || language;
|
||||
}
|
||||
|
||||
return <Dropdown
|
||||
id="mx_LanguageDropdown"
|
||||
className={this.props.className}
|
||||
onOptionChange={this.props.onOptionChange}
|
||||
onSearchChange={this._onSearchChange}
|
||||
searchEnabled={true}
|
||||
value={value}
|
||||
label={_t("Language Dropdown")}>
|
||||
{ options }
|
||||
</Dropdown>;
|
||||
}
|
||||
}
|
|
@ -61,7 +61,9 @@ import QuestionDialog from "../dialogs/QuestionDialog";
|
|||
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
interface IDevice {
|
||||
deviceId: string;
|
||||
|
@ -303,7 +305,8 @@ const UserOptionsSection: React.FC<{
|
|||
member: RoomMember;
|
||||
isIgnored: boolean;
|
||||
canInvite: boolean;
|
||||
}> = ({member, isIgnored, canInvite}) => {
|
||||
isSpace?: boolean;
|
||||
}> = ({member, isIgnored, canInvite, isSpace}) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
|
||||
let ignoreButton = null;
|
||||
|
@ -343,7 +346,7 @@ const UserOptionsSection: React.FC<{
|
|||
</AccessibleButton>
|
||||
);
|
||||
|
||||
if (member.roomId) {
|
||||
if (member.roomId && !isSpace) {
|
||||
const onReadReceiptButton = function() {
|
||||
const room = cli.getRoom(member.roomId);
|
||||
dis.dispatch({
|
||||
|
@ -435,14 +438,18 @@ const UserOptionsSection: React.FC<{
|
|||
);
|
||||
};
|
||||
|
||||
const warnSelfDemote = async () => {
|
||||
const warnSelfDemote = async (isSpace) => {
|
||||
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||
title: _t("Demote yourself?"),
|
||||
description:
|
||||
<div>
|
||||
{ _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
{ isSpace
|
||||
? _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the space it will be impossible " +
|
||||
"to regain privileges.")
|
||||
: _t("You will not be able to undo this change as you are demoting yourself, " +
|
||||
"if you are the last privileged user in the room it will be impossible " +
|
||||
"to regain privileges.") }
|
||||
</div>,
|
||||
button: _t("Demote"),
|
||||
});
|
||||
|
@ -718,7 +725,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
|
|||
// if muting self, warn as it may be irreversible
|
||||
if (target === cli.getUserId()) {
|
||||
try {
|
||||
if (!(await warnSelfDemote())) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
return;
|
||||
|
@ -807,7 +814,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
|||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||
}
|
||||
if (me.powerLevel >= redactPowerLevel) {
|
||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
||||
redactButton = (
|
||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||
);
|
||||
|
@ -1086,7 +1093,7 @@ const PowerLevelEditor: React.FC<{
|
|||
} else if (myUserId === target) {
|
||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||
try {
|
||||
if (!(await warnSelfDemote())) return;
|
||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
||||
} catch (e) {
|
||||
console.error("Failed to warn about self demotion: ", e);
|
||||
}
|
||||
|
@ -1316,12 +1323,10 @@ const BasicUserInfo: React.FC<{
|
|||
if (!isRoomEncrypted) {
|
||||
if (!cryptoEnabled) {
|
||||
text = _t("This client does not support end-to-end encryption.");
|
||||
} else if (room) {
|
||||
} else if (room && !room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||
} else {
|
||||
// TODO what to render for GroupMember
|
||||
}
|
||||
} else {
|
||||
} else if (!room.isSpaceRoom()) {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
|
@ -1397,7 +1402,9 @@ const BasicUserInfo: React.FC<{
|
|||
<UserOptionsSection
|
||||
canInvite={roomPermissions.canInvite}
|
||||
isIgnored={isIgnored}
|
||||
member={member} />
|
||||
member={member}
|
||||
isSpace={room?.isSpaceRoom()}
|
||||
/>
|
||||
|
||||
{ adminToolsContainer }
|
||||
|
||||
|
@ -1514,7 +1521,7 @@ interface IProps {
|
|||
user: Member;
|
||||
groupId?: string;
|
||||
room?: Room;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo;
|
||||
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.GroupMemberInfo | RightPanelPhases.SpaceMemberInfo;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
|
@ -1558,7 +1565,9 @@ const UserInfo: React.FC<Props> = ({
|
|||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||
refireParams = {member: member};
|
||||
} else if (room) {
|
||||
previousPhase = RightPanelPhases.RoomMemberList;
|
||||
previousPhase = previousPhase = room.isSpaceRoom()
|
||||
? RightPanelPhases.SpaceMemberList
|
||||
: RightPanelPhases.RoomMemberList;
|
||||
}
|
||||
|
||||
const onEncryptionPanelClose = () => {
|
||||
|
@ -1573,6 +1582,7 @@ const UserInfo: React.FC<Props> = ({
|
|||
switch (phase) {
|
||||
case RightPanelPhases.RoomMemberInfo:
|
||||
case RightPanelPhases.GroupMemberInfo:
|
||||
case RightPanelPhases.SpaceMemberInfo:
|
||||
content = (
|
||||
<BasicUserInfo
|
||||
room={room}
|
||||
|
@ -1603,7 +1613,18 @@ const UserInfo: React.FC<Props> = ({
|
|||
}
|
||||
}
|
||||
|
||||
const header = <UserInfoHeader member={member} e2eStatus={e2eStatus} />;
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const header = <React.Fragment>
|
||||
{ scopeHeader }
|
||||
<UserInfoHeader member={member} e2eStatus={e2eStatus} />
|
||||
</React.Fragment>;
|
||||
return <BaseCard
|
||||
className={classes.join(" ")}
|
||||
header={header}
|
||||
|
|
|
@ -100,10 +100,11 @@ export default class RoomProfileSettings extends React.Component {
|
|||
const newState = {};
|
||||
|
||||
// TODO: What do we do about errors?
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setRoomName(this.props.roomId, this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
await client.setRoomName(this.props.roomId, displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
|
|
|
@ -17,10 +17,8 @@ limitations under the License.
|
|||
import React from 'react';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||
import * as sdk from '../../../index';
|
||||
import dis from "../../../dispatcher/dispatcher";
|
||||
import AppsDrawer from './AppsDrawer';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import classNames from 'classnames';
|
||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
|
@ -36,9 +34,6 @@ interface IProps {
|
|||
userId: string,
|
||||
showApps: boolean, // Render apps
|
||||
|
||||
// set to true to show the file drop target
|
||||
draggingFile: boolean,
|
||||
|
||||
// maxHeight attribute for the aux panel and the video
|
||||
// therein
|
||||
maxHeight: number,
|
||||
|
@ -149,21 +144,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||
|
||||
let fileDropTarget = null;
|
||||
if (this.props.draggingFile) {
|
||||
fileDropTarget = (
|
||||
<div className="mx_RoomView_fileDropTarget">
|
||||
<div className="mx_RoomView_fileDropTargetLabel" title={_t("Drop File Here")}>
|
||||
<TintableSvg src={require("../../../../res/img/upload-big.svg")} width="45" height="59" />
|
||||
<br />
|
||||
{ _t("Drop file here to upload") }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const callView = (
|
||||
<CallViewForRoom
|
||||
roomId={this.props.room.roomId}
|
||||
|
@ -246,7 +226,6 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
|||
<AutoHideScrollbar className={classes} style={style} >
|
||||
{ stateViews }
|
||||
{ appsDrawer }
|
||||
{ fileDropTarget }
|
||||
{ callView }
|
||||
{ this.props.children }
|
||||
</AutoHideScrollbar>
|
||||
|
|
|
@ -952,9 +952,6 @@ export default class EventTile extends React.Component {
|
|||
return (
|
||||
<div className={classes} tabIndex={-1} aria-live={ariaLive} aria-atomic="true">
|
||||
{ ircTimestamp }
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{ sender }
|
||||
{ ircPadlock }
|
||||
<div className="mx_EventTile_line">
|
||||
|
@ -973,6 +970,9 @@ export default class EventTile extends React.Component {
|
|||
{ reactionsRow }
|
||||
{ actionBar }
|
||||
</div>
|
||||
<div className="mx_EventTile_msgOption">
|
||||
{ readAvatars }
|
||||
</div>
|
||||
{
|
||||
// The avatar goes after the event tile as it's absolutely positioned to be over the
|
||||
// event tile line, so needs to be later in the DOM so it appears on top (this avoids
|
||||
|
|
|
@ -27,6 +27,8 @@ import * as sdk from "../../../index";
|
|||
import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore";
|
||||
import BaseCard from "../right_panel/BaseCard";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||
|
@ -456,6 +458,8 @@ export default class MemberList extends React.Component {
|
|||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||
if (chat && chat.roomId === this.props.roomId) {
|
||||
inviteButtonText = _t("Invite to this community");
|
||||
} else if (room.isSpaceRoom()) {
|
||||
inviteButtonText = _t("Invite to this space");
|
||||
}
|
||||
|
||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||
|
@ -483,12 +487,26 @@ export default class MemberList extends React.Component {
|
|||
onSearch={ this.onSearchQueryChanged } />
|
||||
);
|
||||
|
||||
let previousPhase = RightPanelPhases.RoomSummary;
|
||||
// We have no previousPhase for when viewing a MemberList from a Space
|
||||
let scopeHeader;
|
||||
if (room?.isSpaceRoom()) {
|
||||
previousPhase = undefined;
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={room} height={32} width={32} />
|
||||
<RoomName room={room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <BaseCard
|
||||
className="mx_MemberList"
|
||||
header={inviteButton}
|
||||
header={<React.Fragment>
|
||||
{ scopeHeader }
|
||||
{ inviteButton }
|
||||
</React.Fragment>}
|
||||
footer={footer}
|
||||
onClose={this.props.onClose}
|
||||
previousPhase={RightPanelPhases.RoomSummary}
|
||||
previousPhase={previousPhase}
|
||||
>
|
||||
<div className="mx_MemberList_wrapper">
|
||||
<TruncatedList className="mx_MemberList_section mx_MemberList_joined" truncateAt={this.state.truncateAtJoined}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015-2018, 2020, 2021 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.
|
||||
|
@ -19,7 +17,6 @@ import React, {createRef} from 'react';
|
|||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CallHandler from '../../../CallHandler';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import * as sdk from '../../../index';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -33,11 +30,8 @@ import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|||
import ReplyPreview from "./ReplyPreview";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
import WidgetStore from "../../../stores/WidgetStore";
|
||||
import WidgetUtils from "../../../utils/WidgetUtils";
|
||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
||||
import ActiveWidgetStore from "../../../stores/ActiveWidgetStore";
|
||||
import { PlaceCallType } from "../../../CallHandler";
|
||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||
|
||||
function ComposerAvatar(props) {
|
||||
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
|
||||
|
@ -50,95 +44,18 @@ ComposerAvatar.propTypes = {
|
|||
me: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function CallButton(props) {
|
||||
const onVoiceCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: PlaceCallType.Voice,
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return (<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_voicecall"
|
||||
onClick={onVoiceCallClick}
|
||||
title={_t('Voice call')}
|
||||
/>);
|
||||
}
|
||||
|
||||
CallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function VideoCallButton(props) {
|
||||
const onCallClick = (ev) => {
|
||||
dis.dispatch({
|
||||
action: 'place_call',
|
||||
type: ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video,
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_videocall"
|
||||
onClick={onCallClick}
|
||||
title={_t('Video call')}
|
||||
/>;
|
||||
}
|
||||
|
||||
VideoCallButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
function HangupButton(props) {
|
||||
const onHangupClick = () => {
|
||||
if (props.isConference) {
|
||||
dis.dispatch({
|
||||
action: props.canEndConference ? 'end_conference' : 'hangup_conference',
|
||||
room_id: props.roomId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const call = CallHandler.sharedInstance().getCallForRoom(props.roomId);
|
||||
if (!call) {
|
||||
return;
|
||||
}
|
||||
|
||||
const action = call.state === CallState.Ringing ? 'reject' : 'hangup';
|
||||
|
||||
dis.dispatch({
|
||||
action,
|
||||
// hangup the call for this room. NB. We use the room in props as the room ID
|
||||
// as call.roomId may be the 'virtual room', and the dispatch actions always
|
||||
// use the user-facing room (there was a time when we deliberately used
|
||||
// call.roomId and *not* props.roomId, but that was for the old
|
||||
// style Freeswitch conference calls and those times are gone.)
|
||||
room_id: props.roomId,
|
||||
});
|
||||
};
|
||||
|
||||
let tooltip = _t("Hangup");
|
||||
if (props.isConference && props.canEndConference) {
|
||||
tooltip = _t("End conference");
|
||||
}
|
||||
|
||||
const canLeaveConference = !props.isConference ? true : props.isInConference;
|
||||
function SendButton(props) {
|
||||
return (
|
||||
<AccessibleTooltipButton
|
||||
className="mx_MessageComposer_button mx_MessageComposer_hangup"
|
||||
onClick={onHangupClick}
|
||||
title={tooltip}
|
||||
disabled={!canLeaveConference}
|
||||
className="mx_MessageComposer_sendMessage"
|
||||
onClick={props.onClick}
|
||||
title={_t('Send message')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
HangupButton.propTypes = {
|
||||
roomId: PropTypes.string.isRequired,
|
||||
isConference: PropTypes.bool.isRequired,
|
||||
canEndConference: PropTypes.bool,
|
||||
isInConference: PropTypes.bool,
|
||||
SendButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const EmojiButton = ({addEmoji}) => {
|
||||
|
@ -265,9 +182,9 @@ export default class MessageComposer extends React.Component {
|
|||
this.state = {
|
||||
tombstone: this._getRoomTombstone(),
|
||||
canSendMessages: this.props.room.maySendMessage(),
|
||||
showCallButtons: SettingsStore.getValue("showCallButtonsInComposer"),
|
||||
hasConference: WidgetStore.instance.doesRoomHaveConference(this.props.room),
|
||||
joinedConference: WidgetStore.instance.isJoinedToConferenceIn(this.props.room),
|
||||
isComposerEmpty: true,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -396,6 +313,16 @@ export default class MessageComposer extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
sendMessage = () => {
|
||||
this.messageComposerInput._sendMessage();
|
||||
}
|
||||
|
||||
onChange = (model) => {
|
||||
this.setState({
|
||||
isComposerEmpty: model.isEmpty,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const controls = [
|
||||
this.state.me ? <ComposerAvatar key="controls_avatar" me={this.state.me} /> : null,
|
||||
|
@ -405,12 +332,7 @@ export default class MessageComposer extends React.Component {
|
|||
];
|
||||
|
||||
if (!this.state.tombstone && this.state.canSendMessages) {
|
||||
// This also currently includes the call buttons. Really we should
|
||||
// check separately for whether we can call, but this is slightly
|
||||
// complex because of conference calls.
|
||||
|
||||
const SendMessageComposer = sdk.getComponent("rooms.SendMessageComposer");
|
||||
const callInProgress = this.props.callState && this.props.callState !== 'ended';
|
||||
|
||||
controls.push(
|
||||
<SendMessageComposer
|
||||
|
@ -421,6 +343,7 @@ export default class MessageComposer extends React.Component {
|
|||
resizeNotifier={this.props.resizeNotifier}
|
||||
permalinkCreator={this.props.permalinkCreator}
|
||||
replyToEvent={this.props.replyToEvent}
|
||||
onChange={this.onChange}
|
||||
/>,
|
||||
<UploadButton key="controls_upload" roomId={this.props.room.roomId} />,
|
||||
<EmojiButton key="emoji_button" addEmoji={this.addEmoji} />,
|
||||
|
@ -431,28 +354,10 @@ export default class MessageComposer extends React.Component {
|
|||
controls.push(<Stickerpicker key="stickerpicker_controls_button" room={this.props.room} />);
|
||||
}
|
||||
|
||||
if (this.state.showCallButtons) {
|
||||
if (this.state.hasConference) {
|
||||
const canEndConf = WidgetUtils.canUserModifyWidgets(this.props.room.roomId);
|
||||
controls.push(
|
||||
<HangupButton
|
||||
key="controls_hangup"
|
||||
roomId={this.props.room.roomId}
|
||||
isConference={true}
|
||||
canEndConference={canEndConf}
|
||||
isInConference={this.state.joinedConference}
|
||||
/>,
|
||||
);
|
||||
} else if (callInProgress) {
|
||||
controls.push(
|
||||
<HangupButton key="controls_hangup" roomId={this.props.room.roomId} isConference={false} />,
|
||||
);
|
||||
} else {
|
||||
controls.push(
|
||||
<CallButton key="controls_call" roomId={this.props.room.roomId} />,
|
||||
<VideoCallButton key="controls_videocall" roomId={this.props.room.roomId} />,
|
||||
);
|
||||
}
|
||||
if (!this.state.isComposerEmpty) {
|
||||
controls.push(
|
||||
<SendButton key="controls_send" onClick={this.sendMessage} />,
|
||||
);
|
||||
}
|
||||
} else if (this.state.tombstone) {
|
||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||
|
|
|
@ -32,7 +32,7 @@ try {
|
|||
} catch (e) {
|
||||
}
|
||||
|
||||
export default class ReadReceiptMarker extends React.Component {
|
||||
export default class ReadReceiptMarker extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// the RoomMember to show the RR for
|
||||
member: PropTypes.object,
|
||||
|
@ -155,7 +155,15 @@ export default class ReadReceiptMarker extends React.Component {
|
|||
|
||||
// then shift to the rightmost column,
|
||||
// and then it will drop down to its resting position
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0px' });
|
||||
//
|
||||
// XXX: We use a fractional left value to trick velocity-animate into actually animating.
|
||||
// This is a very annoying bug where if it thinks there's no change to `left` then it'll
|
||||
// skip applying it, thus making our read receipt at +14px instead of +0px like it
|
||||
// should be. This does cause a tiny amount of drift for read receipts, however with a
|
||||
// value so small it's not perceived by a user.
|
||||
// Note: Any smaller values (or trying to interchange units) might cause read receipts to
|
||||
// fail to fall down or cause gaps.
|
||||
startStyles.push({ top: startTopOffset+'px', left: '0.001px' });
|
||||
enterTransitionOpts.push({
|
||||
duration: bounce ? Math.min(Math.log(Math.abs(startTopOffset)) * 200, 3000) : 300,
|
||||
easing: bounce ? 'easeOutBounce' : 'easeOutCubic',
|
||||
|
|
|
@ -31,6 +31,7 @@ import {DefaultTagID} from "../../../stores/room-list/models";
|
|||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
import {PlaceCallType} from "../../../CallHandler";
|
||||
|
||||
export default class RoomHeader extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -45,6 +46,7 @@ export default class RoomHeader extends React.Component {
|
|||
e2eStatus: PropTypes.string,
|
||||
onAppsClick: PropTypes.func,
|
||||
appsShown: PropTypes.bool,
|
||||
onCallPlaced: PropTypes.func, // (PlaceCallType) => void;
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -226,8 +228,26 @@ export default class RoomHeader extends React.Component {
|
|||
title={_t("Search")} />;
|
||||
}
|
||||
|
||||
let voiceCallButton;
|
||||
let videoCallButton;
|
||||
if (this.props.inRoom && SettingsStore.getValue("showCallButtonsInComposer")) {
|
||||
voiceCallButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
||||
onClick={() => this.props.onCallPlaced(PlaceCallType.Voice)}
|
||||
title={_t("Voice call")} />;
|
||||
videoCallButton =
|
||||
<AccessibleTooltipButton
|
||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
||||
onClick={(ev) => this.props.onCallPlaced(
|
||||
ev.shiftKey ? PlaceCallType.ScreenSharing : PlaceCallType.Video)}
|
||||
title={_t("Video call")} />;
|
||||
}
|
||||
|
||||
const rightRow =
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ videoCallButton }
|
||||
{ voiceCallButton }
|
||||
{ pinnedEventsButton }
|
||||
{ forgetButton }
|
||||
{ appsButton }
|
||||
|
|
|
@ -47,6 +47,9 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
|
|||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
|
||||
import CallHandler from "../../../CallHandler";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
interface IProps {
|
||||
onKeyDown: (ev: React.KeyboardEvent) => void;
|
||||
|
@ -152,6 +155,50 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
|
|||
defaultHidden: false,
|
||||
addRoomLabel: _td("Add room"),
|
||||
addRoomContextMenu: (onFinished: () => void) => {
|
||||
if (SpaceStore.instance.activeSpace) {
|
||||
const canAddRooms = SpaceStore.instance.activeSpace.currentState.maySendStateEvent(EventType.SpaceChild,
|
||||
MatrixClientPeg.get().getUserId());
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
iconClassName="mx_RoomList_iconPlus"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showCreateNewRoom(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to create new rooms in this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Add existing room")}
|
||||
iconClassName="mx_RoomList_iconHash"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
showAddExistingRooms(MatrixClientPeg.get(), SpaceStore.instance.activeSpace);
|
||||
}}
|
||||
disabled={!canAddRooms}
|
||||
tooltip={canAddRooms ? undefined
|
||||
: _t("You do not have permissions to add rooms to this space")}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Explore space rooms")}
|
||||
iconClassName="mx_RoomList_iconExplore"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onFinished();
|
||||
defaultDispatcher.fire(Action.ViewRoomDirectory);
|
||||
}}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
return <IconizedContextMenuOptionList first>
|
||||
<IconizedContextMenuOption
|
||||
label={_t("Create new room")}
|
||||
|
|
|
@ -117,6 +117,7 @@ export default class SendMessageComposer extends React.Component {
|
|||
placeholder: PropTypes.string,
|
||||
permalinkCreator: PropTypes.object.isRequired,
|
||||
replyToEvent: PropTypes.object,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
static contextType = MatrixClientContext;
|
||||
|
@ -403,7 +404,9 @@ export default class SendMessageComposer extends React.Component {
|
|||
this._editorRef.clearUndoHistory();
|
||||
this._editorRef.focus();
|
||||
this._clearStoredEditorState();
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
if (SettingsStore.getValue("scrollToBottomOnMessageSent")) {
|
||||
dis.dispatch({action: "scroll_to_bottom"});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -536,10 +539,15 @@ export default class SendMessageComposer extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onChange = () => {
|
||||
if (this.props.onChange) this.props.onChange(this.model);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SendMessageComposer" onClick={this.focusComposer} onKeyDown={this._onKeyDown}>
|
||||
<BasicMessageComposer
|
||||
onChange={this.onChange}
|
||||
ref={this._setEditorRef}
|
||||
model={this.model}
|
||||
room={this.props.room}
|
||||
|
|
|
@ -23,6 +23,8 @@ import dis from "../../../dispatcher/dispatcher";
|
|||
import * as sdk from "../../../index";
|
||||
import Modal from "../../../Modal";
|
||||
import {isValid3pidInvite} from "../../../RoomInvite";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
||||
export default class ThirdPartyMemberInfo extends React.Component {
|
||||
static propTypes = {
|
||||
|
@ -32,14 +34,14 @@ export default class ThirdPartyMemberInfo extends React.Component {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
this.room = MatrixClientPeg.get().getRoom(this.props.event.getRoomId());
|
||||
const me = this.room.getMember(MatrixClientPeg.get().getUserId());
|
||||
const powerLevels = this.room.currentState.getStateEvents("m.room.power_levels", "");
|
||||
|
||||
let kickLevel = powerLevels ? powerLevels.getContent().kick : 50;
|
||||
if (typeof(kickLevel) !== 'number') kickLevel = 50;
|
||||
|
||||
const sender = room.getMember(this.props.event.getSender());
|
||||
const sender = this.room.getMember(this.props.event.getSender());
|
||||
|
||||
this.state = {
|
||||
stateKey: this.props.event.getStateKey(),
|
||||
|
@ -119,9 +121,18 @@ export default class ThirdPartyMemberInfo extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
let scopeHeader;
|
||||
if (this.room.isSpaceRoom()) {
|
||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||
<RoomAvatar room={this.room} height={32} width={32} />
|
||||
<RoomName room={this.room} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// We shamelessly rip off the MemberInfo styles here.
|
||||
return (
|
||||
<div className="mx_MemberInfo" role="tabpanel">
|
||||
{ scopeHeader }
|
||||
<div className="mx_MemberInfo_name">
|
||||
<AccessibleButton className="mx_MemberInfo_cancel"
|
||||
onClick={this.onCancel}
|
||||
|
|
|
@ -81,10 +81,12 @@ export default class ProfileSettings extends React.Component {
|
|||
const client = MatrixClientPeg.get();
|
||||
const newState = {};
|
||||
|
||||
const displayName = this.state.displayName.trim();
|
||||
try {
|
||||
if (this.state.originalDisplayName !== this.state.displayName) {
|
||||
await client.setDisplayName(this.state.displayName);
|
||||
newState.originalDisplayName = this.state.displayName;
|
||||
await client.setDisplayName(displayName);
|
||||
newState.originalDisplayName = displayName;
|
||||
newState.displayName = displayName;
|
||||
}
|
||||
|
||||
if (this.state.avatarFile) {
|
||||
|
|
111
src/components/views/settings/SpellCheckSettings.tsx
Normal file
111
src/components/views/settings/SpellCheckSettings.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
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 SpellCheckLanguagesDropdown from "../../../components/views/elements/SpellCheckLanguagesDropdown";
|
||||
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
interface ExistingSpellCheckLanguageIProps {
|
||||
language: string,
|
||||
onRemoved(language: string),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIProps {
|
||||
languages: Array<string>,
|
||||
onLanguagesChange(languages: Array<string>),
|
||||
}
|
||||
|
||||
interface SpellCheckLanguagesIState {
|
||||
newLanguage: string,
|
||||
}
|
||||
|
||||
export class ExistingSpellCheckLanguage extends React.Component<ExistingSpellCheckLanguageIProps> {
|
||||
_onRemove = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
return this.props.onRemoved(this.props.language);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_ExistingSpellCheckLanguage">
|
||||
<span className="mx_ExistingSpellCheckLanguage_language">{this.props.language}</span>
|
||||
<AccessibleButton onClick={this._onRemove} kind="danger_sm">
|
||||
{_t("Remove")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpellCheckLanguages extends React.Component<SpellCheckLanguagesIProps, SpellCheckLanguagesIState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
newLanguage: "",
|
||||
}
|
||||
}
|
||||
|
||||
_onRemoved = (language) => {
|
||||
const languages = this.props.languages.filter((e) => e !== language);
|
||||
this.props.onLanguagesChange(languages);
|
||||
};
|
||||
|
||||
_onAddClick = (e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
const language = this.state.newLanguage;
|
||||
|
||||
if (!language) return;
|
||||
if (this.props.languages.includes(language)) return;
|
||||
|
||||
this.props.languages.push(language)
|
||||
this.props.onLanguagesChange(this.props.languages);
|
||||
};
|
||||
|
||||
_onNewLanguageChange = (language: string) => {
|
||||
if (this.state.newLanguage === language) return;
|
||||
this.setState({newLanguage: language});
|
||||
};
|
||||
|
||||
render() {
|
||||
const existingSpellCheckLanguages = this.props.languages.map((e) => {
|
||||
return <ExistingSpellCheckLanguage language={e} onRemoved={this._onRemoved} key={e} />;
|
||||
});
|
||||
|
||||
const addButton = (
|
||||
<AccessibleButton onClick={this._onAddClick} kind="primary">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx_SpellCheckLanguages">
|
||||
{existingSpellCheckLanguages}
|
||||
<form onSubmit={this._onAddClick} noValidate={true}>
|
||||
<SpellCheckLanguagesDropdown
|
||||
className="mx_GeneralUserSettingsTab_spellCheckLanguageInput"
|
||||
value={this.state.newLanguage}
|
||||
onOptionChange={this._onNewLanguageChange} />
|
||||
{addButton}
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import ProfileSettings from "../../ProfileSettings";
|
|||
import * as languageHandler from "../../../../../languageHandler";
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import LanguageDropdown from "../../../elements/LanguageDropdown";
|
||||
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||
import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
|
||||
import PropTypes from "prop-types";
|
||||
|
@ -49,6 +50,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
|
||||
this.state = {
|
||||
language: languageHandler.getCurrentLanguage(),
|
||||
spellCheckLanguages: [],
|
||||
haveIdServer: Boolean(MatrixClientPeg.get().getIdentityServerUrl()),
|
||||
serverSupportsSeparateAddAndBind: null,
|
||||
idServerHasUnsignedTerms: false,
|
||||
|
@ -85,6 +87,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
this._getThreepidState();
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
this.setState({
|
||||
spellCheckLanguages: await plaf.getSpellCheckLanguages(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
}
|
||||
|
@ -182,6 +193,15 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
PlatformPeg.get().reload();
|
||||
};
|
||||
|
||||
_onSpellCheckLanguagesChange = (languages) => {
|
||||
this.setState({spellCheckLanguages: languages});
|
||||
|
||||
const plaf = PlatformPeg.get();
|
||||
if (plaf) {
|
||||
plaf.setSpellCheckLanguages(languages);
|
||||
}
|
||||
};
|
||||
|
||||
_onPasswordChangeError = (err) => {
|
||||
// TODO: Figure out a design that doesn't involve replacing the current dialog
|
||||
let errMsg = err.error || "";
|
||||
|
@ -303,6 +323,16 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderSpellCheckSection() {
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
<span className="mx_SettingsTab_subheading">{_t("Spell check dictionaries")}</span>
|
||||
<SpellCheckSettings languages={this.state.spellCheckLanguages}
|
||||
onLanguagesChange={this._onSpellCheckLanguagesChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_renderDiscoverySection() {
|
||||
const SetIdServer = sdk.getComponent("views.settings.SetIdServer");
|
||||
|
||||
|
@ -381,6 +411,9 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const plaf = PlatformPeg.get();
|
||||
const supportsMultiLanguageSpellCheck = plaf.supportsMultiLanguageSpellCheck();
|
||||
|
||||
const discoWarning = this.state.requiredPolicyInfo.hasTerms
|
||||
? <img className='mx_GeneralUserSettingsTab_warningIcon'
|
||||
src={require("../../../../../../res/img/feather-customised/warning-triangle.svg")}
|
||||
|
@ -409,6 +442,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
{this._renderProfileSection()}
|
||||
{this._renderAccountSection()}
|
||||
{this._renderLanguageSection()}
|
||||
{supportsMultiLanguageSpellCheck ? this._renderSpellCheckSection() : null}
|
||||
{ discoverySection }
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
{ accountManagementSection }
|
||||
|
|
|
@ -48,6 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
|||
'showRedactions',
|
||||
'enableSyntaxHighlightLanguageDetection',
|
||||
'expandCodeByDefault',
|
||||
'scrollToBottomOnMessageSent',
|
||||
'showCodeLineNumbers',
|
||||
'showJoinLeaves',
|
||||
'showAvatarChanges',
|
||||
|
|
120
src/components/views/spaces/SpaceBasicSettings.tsx
Normal file
120
src/components/views/spaces/SpaceBasicSettings.tsx
Normal file
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2021 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, {useRef, useState} from "react";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Field from "../elements/Field";
|
||||
|
||||
interface IProps {
|
||||
avatarUrl?: string;
|
||||
avatarDisabled?: boolean;
|
||||
name?: string,
|
||||
nameDisabled?: boolean;
|
||||
topic?: string;
|
||||
topicDisabled?: boolean;
|
||||
setAvatar(avatar: File): void;
|
||||
setName(name: string): void;
|
||||
setTopic(topic: string): void;
|
||||
}
|
||||
|
||||
const SpaceBasicSettings = ({
|
||||
avatarUrl,
|
||||
avatarDisabled = false,
|
||||
setAvatar,
|
||||
name = "",
|
||||
nameDisabled = false,
|
||||
setName,
|
||||
topic = "",
|
||||
topicDisabled = false,
|
||||
setTopic,
|
||||
}: IProps) => {
|
||||
const avatarUploadRef = useRef<HTMLInputElement>();
|
||||
const [avatar, setAvatarDataUrl] = useState(avatarUrl); // avatar data url cache
|
||||
|
||||
let avatarSection;
|
||||
if (avatarDisabled) {
|
||||
if (avatar) {
|
||||
avatarSection = <img className="mx_SpaceBasicSettings_avatar" src={avatar} alt="" />;
|
||||
} else {
|
||||
avatarSection = <div className="mx_SpaceBasicSettings_avatar" />;
|
||||
}
|
||||
} else {
|
||||
if (avatar) {
|
||||
avatarSection = <React.Fragment>
|
||||
<AccessibleButton
|
||||
className="mx_SpaceBasicSettings_avatar"
|
||||
onClick={() => avatarUploadRef.current?.click()}
|
||||
element="img"
|
||||
src={avatar}
|
||||
alt=""
|
||||
/>
|
||||
<AccessibleButton onClick={() => {
|
||||
avatarUploadRef.current.value = "";
|
||||
setAvatarDataUrl(undefined);
|
||||
setAvatar(undefined);
|
||||
}} kind="link" className="mx_SpaceBasicSettings_avatar_remove">
|
||||
{ _t("Delete") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
avatarSection = <React.Fragment>
|
||||
<div className="mx_SpaceBasicSettings_avatar" onClick={() => avatarUploadRef.current?.click()} />
|
||||
<AccessibleButton onClick={() => avatarUploadRef.current?.click()} kind="link">
|
||||
{ _t("Upload") }
|
||||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="mx_SpaceBasicSettings">
|
||||
<div className="mx_SpaceBasicSettings_avatarContainer">
|
||||
{ avatarSection }
|
||||
<input type="file" ref={avatarUploadRef} onChange={(e) => {
|
||||
if (!e.target.files?.length) return;
|
||||
const file = e.target.files[0];
|
||||
setAvatar(file);
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
setAvatarDataUrl(ev.target.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}} accept="image/*" />
|
||||
</div>
|
||||
|
||||
<Field
|
||||
name="spaceName"
|
||||
label={_t("Name")}
|
||||
autoFocus={true}
|
||||
value={name}
|
||||
onChange={ev => setName(ev.target.value)}
|
||||
disabled={nameDisabled}
|
||||
/>
|
||||
|
||||
<Field
|
||||
name="spaceTopic"
|
||||
element="textarea"
|
||||
label={_t("Description")}
|
||||
value={topic}
|
||||
onChange={ev => setTopic(ev.target.value)}
|
||||
rows={3}
|
||||
disabled={topicDisabled}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpaceBasicSettings;
|
175
src/components/views/spaces/SpaceCreateMenu.tsx
Normal file
175
src/components/views/spaces/SpaceCreateMenu.tsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
Copyright 2021 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, {useContext, useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
|
||||
import FormButton from "../elements/FormButton";
|
||||
import createRoom, {IStateEvent, Preset} from "../../../createRoom";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import SpaceBasicSettings from "./SpaceBasicSettings";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import FocusLock from "react-focus-lock";
|
||||
|
||||
const SpaceCreateMenuType = ({ title, description, className, onClick }) => {
|
||||
return (
|
||||
<AccessibleButton className={classNames("mx_SpaceCreateMenuType", className)} onClick={onClick}>
|
||||
<h3>{ title }</h3>
|
||||
<span>{ description }</span>
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
enum Visibility {
|
||||
Public,
|
||||
Private,
|
||||
}
|
||||
|
||||
const SpaceCreateMenu = ({ onFinished }) => {
|
||||
const cli = useContext(MatrixClientContext);
|
||||
const [visibility, setVisibility] = useState<Visibility>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [avatar, setAvatar] = useState<File>(null);
|
||||
const [topic, setTopic] = useState<string>("");
|
||||
const [busy, setBusy] = useState<boolean>(false);
|
||||
|
||||
const onSpaceCreateClick = async () => {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
const initialState: IStateEvent[] = [
|
||||
{
|
||||
type: EventType.RoomHistoryVisibility,
|
||||
content: {
|
||||
"history_visibility": visibility === Visibility.Public ? "world_readable" : "invited",
|
||||
},
|
||||
},
|
||||
];
|
||||
if (avatar) {
|
||||
const url = await cli.uploadContent(avatar);
|
||||
|
||||
initialState.push({
|
||||
type: EventType.RoomAvatar,
|
||||
content: { url },
|
||||
});
|
||||
}
|
||||
if (topic) {
|
||||
initialState.push({
|
||||
type: EventType.RoomTopic,
|
||||
content: { topic },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await createRoom({
|
||||
createOpts: {
|
||||
preset: visibility === Visibility.Public ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
creation_content: {
|
||||
// Based on MSC1840
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: initialState,
|
||||
power_level_content_override: {
|
||||
// Only allow Admins to write to the timeline to prevent hidden sync spam
|
||||
events_default: 100,
|
||||
},
|
||||
},
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: true,
|
||||
inlineErrors: true,
|
||||
});
|
||||
|
||||
onFinished();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
let body;
|
||||
if (visibility === null) {
|
||||
body = <React.Fragment>
|
||||
<h2>{ _t("Create a space") }</h2>
|
||||
<p>{ _t("Organise rooms into spaces, for just you or anyone") }</p>
|
||||
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Public")}
|
||||
description={_t("Open space for anyone, best for communities")}
|
||||
className="mx_SpaceCreateMenuType_public"
|
||||
onClick={() => setVisibility(Visibility.Public)}
|
||||
/>
|
||||
<SpaceCreateMenuType
|
||||
title={_t("Private")}
|
||||
description={_t("Invite only space, best for yourself or teams")}
|
||||
className="mx_SpaceCreateMenuType_private"
|
||||
onClick={() => setVisibility(Visibility.Private)}
|
||||
/>
|
||||
|
||||
{/*<p>{ _t("Looking to join an existing space?") }</p>*/}
|
||||
</React.Fragment>;
|
||||
} else {
|
||||
body = <React.Fragment>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_SpaceCreateMenu_back"
|
||||
onClick={() => setVisibility(null)}
|
||||
title={_t("Go back")}
|
||||
/>
|
||||
|
||||
<h2>
|
||||
{
|
||||
visibility === Visibility.Public
|
||||
? _t("Personalise your public space")
|
||||
: _t("Personalise your private space")
|
||||
}
|
||||
</h2>
|
||||
<p>
|
||||
{
|
||||
_t("Give it a photo, name and description to help you identify it.")
|
||||
} {
|
||||
_t("You can change these at any point.")
|
||||
}
|
||||
</p>
|
||||
|
||||
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
|
||||
|
||||
<FormButton
|
||||
label={busy ? _t("Creating...") : _t("Create")}
|
||||
onClick={onSpaceCreateClick}
|
||||
disabled={!name && !busy}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <ContextMenu
|
||||
left={72}
|
||||
top={62}
|
||||
chevronOffset={0}
|
||||
chevronFace={ChevronFace.None}
|
||||
onFinished={onFinished}
|
||||
wrapperClassName="mx_SpaceCreateMenu_wrapper"
|
||||
managed={false}
|
||||
>
|
||||
<FocusLock returnFocus={true}>
|
||||
{ body }
|
||||
</FocusLock>
|
||||
</ContextMenu>;
|
||||
}
|
||||
|
||||
export default SpaceCreateMenu;
|
238
src/components/views/spaces/SpacePanel.tsx
Normal file
238
src/components/views/spaces/SpacePanel.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
Copyright 2021 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, {useState} from "react";
|
||||
import classNames from "classnames";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import {useContextMenu} from "../../structures/ContextMenu";
|
||||
import SpaceCreateMenu from "./SpaceCreateMenu";
|
||||
import {SpaceItem} from "./SpaceTreeLevel";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
import SpaceStore, {HOME_SPACE, UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../../stores/SpaceStore";
|
||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {
|
||||
RovingAccessibleButton,
|
||||
RovingAccessibleTooltipButton,
|
||||
RovingTabIndexProvider,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import {Key} from "../../../Keyboard";
|
||||
|
||||
interface IButtonProps {
|
||||
space?: Room;
|
||||
className?: string;
|
||||
selected?: boolean;
|
||||
tooltip?: string;
|
||||
notificationState?: SpaceNotificationState;
|
||||
isNarrow?: boolean;
|
||||
onClick(): void;
|
||||
}
|
||||
|
||||
const SpaceButton: React.FC<IButtonProps> = ({
|
||||
space,
|
||||
className,
|
||||
selected,
|
||||
onClick,
|
||||
tooltip,
|
||||
notificationState,
|
||||
isNarrow,
|
||||
children,
|
||||
}) => {
|
||||
const classes = classNames("mx_SpaceButton", className, {
|
||||
mx_SpaceButton_active: selected,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
});
|
||||
|
||||
let avatar = <div className="mx_SpaceButton_avatarPlaceholder"><div className="mx_SpaceButton_icon" /></div>;
|
||||
if (space) {
|
||||
avatar = <RoomAvatar width={32} height={32} room={space} />;
|
||||
}
|
||||
|
||||
let notifBadge;
|
||||
if (notificationState) {
|
||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
let button;
|
||||
if (isNarrow) {
|
||||
button = (
|
||||
<RovingAccessibleTooltipButton className={classes} title={tooltip} onClick={onClick} role="treeitem">
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
{ avatar }
|
||||
{ notifBadge }
|
||||
{ children }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<RovingAccessibleButton className={classes} onClick={onClick} role="treeitem">
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
{ avatar }
|
||||
<span className="mx_SpaceButton_name">{ tooltip }</span>
|
||||
{ notifBadge }
|
||||
{ children }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return <li className={classNames({
|
||||
"mx_SpaceItem": true,
|
||||
"collapsed": isNarrow,
|
||||
})}>
|
||||
{ button }
|
||||
</li>;
|
||||
}
|
||||
|
||||
const useSpaces = (): [Room[], Room | null] => {
|
||||
const [spaces, setSpaces] = useState<Room[]>(SpaceStore.instance.spacePanelSpaces);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_TOP_LEVEL_SPACES, setSpaces);
|
||||
const [activeSpace, setActiveSpace] = useState<Room>(SpaceStore.instance.activeSpace);
|
||||
useEventEmitter(SpaceStore.instance, UPDATE_SELECTED_SPACE, setActiveSpace);
|
||||
return [spaces, activeSpace];
|
||||
};
|
||||
|
||||
const SpacePanel = () => {
|
||||
// We don't need the handle as we position the menu in a constant location
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<void>();
|
||||
const [spaces, activeSpace] = useSpaces();
|
||||
const [isPanelCollapsed, setPanelCollapsed] = useState(true);
|
||||
|
||||
const newClasses = classNames("mx_SpaceButton_new", {
|
||||
mx_SpaceButton_newCancel: menuDisplayed,
|
||||
});
|
||||
|
||||
let contextMenu = null;
|
||||
if (menuDisplayed) {
|
||||
contextMenu = <SpaceCreateMenu onFinished={closeMenu} />;
|
||||
}
|
||||
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
let handled = true;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_UP:
|
||||
onMoveFocus(ev.target as Element, true);
|
||||
break;
|
||||
case Key.ARROW_DOWN:
|
||||
onMoveFocus(ev.target as Element, false);
|
||||
break;
|
||||
default:
|
||||
handled = false;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
// consume all other keys in context menu
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
const onMoveFocus = (element: Element, up: boolean) => {
|
||||
let descending = false; // are we currently descending or ascending through the DOM tree?
|
||||
let classes: DOMTokenList;
|
||||
|
||||
do {
|
||||
const child = up ? element.lastElementChild : element.firstElementChild;
|
||||
const sibling = up ? element.previousElementSibling : element.nextElementSibling;
|
||||
|
||||
if (descending) {
|
||||
if (child) {
|
||||
element = child;
|
||||
} else if (sibling) {
|
||||
element = sibling;
|
||||
} else {
|
||||
descending = false;
|
||||
element = element.parentElement;
|
||||
}
|
||||
} else {
|
||||
if (sibling) {
|
||||
element = sibling;
|
||||
descending = true;
|
||||
} else {
|
||||
element = element.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
if (element) {
|
||||
if (element.classList.contains("mx_ContextualMenu")) { // we hit the top
|
||||
element = up ? element.lastElementChild : element.firstElementChild;
|
||||
descending = true;
|
||||
}
|
||||
classes = element.classList;
|
||||
}
|
||||
} while (element && !classes.contains("mx_SpaceButton"));
|
||||
|
||||
if (element) {
|
||||
(element as HTMLElement).focus();
|
||||
}
|
||||
};
|
||||
|
||||
const activeSpaces = activeSpace ? [activeSpace] : [];
|
||||
const expandCollapseButtonTitle = isPanelCollapsed ? _t("Expand space panel") : _t("Collapse space panel");
|
||||
// TODO drag and drop for re-arranging order
|
||||
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
|
||||
{({onKeyDownHandler}) => (
|
||||
<ul
|
||||
className={classNames("mx_SpacePanel", { collapsed: isPanelCollapsed })}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
>
|
||||
<AutoHideScrollbar className="mx_SpacePanel_spaceTreeWrapper">
|
||||
<div className="mx_SpaceTreeLevel">
|
||||
<SpaceButton
|
||||
className="mx_SpaceButton_home"
|
||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||
selected={!activeSpace}
|
||||
tooltip={_t("Home")}
|
||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
{ spaces.map(s => <SpaceItem
|
||||
key={s.roomId}
|
||||
space={s}
|
||||
activeSpaces={activeSpaces}
|
||||
isPanelCollapsed={isPanelCollapsed}
|
||||
onExpand={() => setPanelCollapsed(false)}
|
||||
/>) }
|
||||
</div>
|
||||
<SpaceButton
|
||||
className={newClasses}
|
||||
tooltip={menuDisplayed ? _t("Cancel") : _t("Create a space")}
|
||||
onClick={menuDisplayed ? closeMenu : openMenu}
|
||||
isNarrow={isPanelCollapsed}
|
||||
/>
|
||||
</AutoHideScrollbar>
|
||||
<AccessibleTooltipButton
|
||||
className={classNames("mx_SpacePanel_toggleCollapse", {expanded: !isPanelCollapsed})}
|
||||
onClick={evt => setPanelCollapsed(!isPanelCollapsed)}
|
||||
title={expandCollapseButtonTitle}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</ul>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
};
|
||||
|
||||
export default SpacePanel;
|
65
src/components/views/spaces/SpacePublicShare.tsx
Normal file
65
src/components/views/spaces/SpacePublicShare.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
Copyright 2021 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, {useState} from "react";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {_t} from "../../../languageHandler";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import {copyPlaintext} from "../../../utils/strings";
|
||||
import {sleep} from "../../../utils/promise";
|
||||
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
|
||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||
|
||||
interface IProps {
|
||||
space: Room;
|
||||
onFinished(): void;
|
||||
}
|
||||
|
||||
const SpacePublicShare = ({ space, onFinished }: IProps) => {
|
||||
const [copiedText, setCopiedText] = useState(_t("Click to copy"));
|
||||
|
||||
return <div className="mx_SpacePublicShare">
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_shareButton"
|
||||
onClick={async () => {
|
||||
const permalinkCreator = new RoomPermalinkCreator(space);
|
||||
permalinkCreator.load();
|
||||
const success = await copyPlaintext(permalinkCreator.forRoom());
|
||||
const text = success ? _t("Copied!") : _t("Failed to copy");
|
||||
setCopiedText(text);
|
||||
await sleep(10);
|
||||
if (copiedText === text) { // if the text hasn't changed by another click then clear it after some time
|
||||
setCopiedText(_t("Click to copy"));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ _t("Share invite link") }
|
||||
<span>{ copiedText }</span>
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
className="mx_SpacePublicShare_inviteButton"
|
||||
onClick={() => {
|
||||
showRoomInviteDialog(space.roomId);
|
||||
onFinished();
|
||||
}}
|
||||
>
|
||||
{ _t("Invite by email or username") }
|
||||
</AccessibleButton>
|
||||
</div>;
|
||||
};
|
||||
|
||||
export default SpacePublicShare;
|
405
src/components/views/spaces/SpaceTreeLevel.tsx
Normal file
405
src/components/views/spaces/SpaceTreeLevel.tsx
Normal file
|
@ -0,0 +1,405 @@
|
|||
/*
|
||||
Copyright 2021 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 {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import RoomAvatar from "../avatars/RoomAvatar";
|
||||
import SpaceStore from "../../../stores/SpaceStore";
|
||||
import NotificationBadge from "../rooms/NotificationBadge";
|
||||
import {RovingAccessibleButton} from "../../../accessibility/roving/RovingAccessibleButton";
|
||||
import {RovingAccessibleTooltipButton} from "../../../accessibility/roving/RovingAccessibleTooltipButton";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {ButtonEvent} from "../elements/AccessibleButton";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import Modal from "../../../Modal";
|
||||
import SpacePublicShare from "./SpacePublicShare";
|
||||
import {Action} from "../../../dispatcher/actions";
|
||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||
import {showRoomInviteDialog} from "../../../RoomInvite";
|
||||
import InfoDialog from "../dialogs/InfoDialog";
|
||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
||||
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
|
||||
|
||||
interface IItemProps {
|
||||
space?: Room;
|
||||
activeSpaces: Room[];
|
||||
isNested?: boolean;
|
||||
isPanelCollapsed?: boolean;
|
||||
onExpand?: Function;
|
||||
}
|
||||
|
||||
interface IItemState {
|
||||
collapsed: boolean;
|
||||
contextMenuPosition: Pick<DOMRect, "right" | "top" | "height">;
|
||||
}
|
||||
|
||||
export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
collapsed: !props.isNested, // default to collapsed for root items
|
||||
contextMenuPosition: null,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleCollapse(evt) {
|
||||
if (this.props.onExpand && this.state.collapsed) {
|
||||
this.props.onExpand();
|
||||
}
|
||||
this.setState({collapsed: !this.state.collapsed});
|
||||
// don't bubble up so encapsulating button for space
|
||||
// doesn't get triggered
|
||||
evt.stopPropagation();
|
||||
}
|
||||
|
||||
private onContextMenu = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
contextMenuPosition: {
|
||||
right: ev.clientX,
|
||||
top: ev.clientY,
|
||||
height: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private onClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
SpaceStore.instance.setActiveSpace(this.props.space);
|
||||
};
|
||||
|
||||
private onMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
this.setState({contextMenuPosition: target.getBoundingClientRect()});
|
||||
};
|
||||
|
||||
private onMenuClose = () => {
|
||||
this.setState({contextMenuPosition: null});
|
||||
};
|
||||
|
||||
private onHomeClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: this.props.space.roomId,
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onInviteClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this.props.space.getJoinRule() === "public") {
|
||||
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
|
||||
title: _t("Invite members"),
|
||||
description: <React.Fragment>
|
||||
<span>{ _t("Share your public space") }</span>
|
||||
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
|
||||
</React.Fragment>,
|
||||
fixedWidth: false,
|
||||
button: false,
|
||||
className: "mx_SpacePanel_sharePublicSpace",
|
||||
hasCloseButton: true,
|
||||
});
|
||||
} else {
|
||||
showRoomInviteDialog(this.props.space.roomId);
|
||||
}
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSettingsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showSpaceSettings(this.context, this.props.space);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onLeaveClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({
|
||||
action: "leave_room",
|
||||
room_id: this.props.space.roomId,
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onNewRoomClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
showCreateNewRoom(this.context, this.props.space);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onMembersClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (!RoomViewStore.getRoomId()) {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "view_room",
|
||||
room_id: this.props.space.roomId,
|
||||
}, true);
|
||||
}
|
||||
|
||||
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.SpaceMemberList,
|
||||
refireParams: { space: this.props.space },
|
||||
});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onExploreRoomsClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, {
|
||||
space: this.props.space,
|
||||
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private renderContextMenu(): React.ReactElement {
|
||||
let contextMenu = null;
|
||||
if (this.state.contextMenuPosition) {
|
||||
const userId = this.context.getUserId();
|
||||
|
||||
let inviteOption;
|
||||
if (this.props.space.canInvite(userId)) {
|
||||
inviteOption = (
|
||||
<IconizedContextMenuOption
|
||||
className="mx_SpacePanel_contextMenu_inviteButton"
|
||||
iconClassName="mx_SpacePanel_iconInvite"
|
||||
label={_t("Invite people")}
|
||||
onClick={this.onInviteClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let settingsOption;
|
||||
let leaveSection;
|
||||
if (shouldShowSpaceSettings(this.context, this.props.space)) {
|
||||
settingsOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconSettings"
|
||||
label={_t("Settings")}
|
||||
onClick={this.onSettingsClick}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
leaveSection = <IconizedContextMenuOptionList red first>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconLeave"
|
||||
label={_t("Leave space")}
|
||||
onClick={this.onLeaveClick}
|
||||
/>
|
||||
</IconizedContextMenuOptionList>;
|
||||
}
|
||||
|
||||
let newRoomOption;
|
||||
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||
newRoomOption = (
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconPlus"
|
||||
label={_t("New room")}
|
||||
onClick={this.onNewRoomClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
contextMenu = <IconizedContextMenu
|
||||
{...toRightOf(this.state.contextMenuPosition, 0)}
|
||||
onFinished={this.onMenuClose}
|
||||
className="mx_SpacePanel_contextMenu"
|
||||
compact
|
||||
>
|
||||
<div className="mx_SpacePanel_contextMenu_header">
|
||||
{ this.props.space.name }
|
||||
</div>
|
||||
<IconizedContextMenuOptionList first>
|
||||
{ inviteOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconHome"
|
||||
label={_t("Space Home")}
|
||||
onClick={this.onHomeClick}
|
||||
/>
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconMembers"
|
||||
label={_t("Members")}
|
||||
onClick={this.onMembersClick}
|
||||
/>
|
||||
{ settingsOption }
|
||||
<IconizedContextMenuOption
|
||||
iconClassName="mx_SpacePanel_iconExplore"
|
||||
label={_t("Explore rooms")}
|
||||
onClick={this.onExploreRoomsClick}
|
||||
/>
|
||||
{ newRoomOption }
|
||||
</IconizedContextMenuOptionList>
|
||||
{ leaveSection }
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<ContextMenuTooltipButton
|
||||
className="mx_SpaceButton_menuButton"
|
||||
onClick={this.onMenuOpenClick}
|
||||
title={_t("Space options")}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
/>
|
||||
{ contextMenu }
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {space, activeSpaces, isNested} = this.props;
|
||||
|
||||
const forceCollapsed = this.props.isPanelCollapsed;
|
||||
const isNarrow = this.props.isPanelCollapsed;
|
||||
const collapsed = this.state.collapsed || forceCollapsed;
|
||||
|
||||
const childSpaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||
const isActive = activeSpaces.includes(space);
|
||||
const itemClasses = classNames({
|
||||
"mx_SpaceItem": true,
|
||||
"collapsed": collapsed,
|
||||
"hasSubSpaces": childSpaces && childSpaces.length,
|
||||
});
|
||||
const classes = classNames("mx_SpaceButton", {
|
||||
mx_SpaceButton_active: isActive,
|
||||
mx_SpaceButton_hasMenuOpen: !!this.state.contextMenuPosition,
|
||||
mx_SpaceButton_narrow: isNarrow,
|
||||
});
|
||||
const notificationState = SpaceStore.instance.getNotificationState(space.roomId);
|
||||
const childItems = childSpaces && !collapsed ? <SpaceTreeLevel
|
||||
spaces={childSpaces}
|
||||
activeSpaces={activeSpaces}
|
||||
isNested={true}
|
||||
/> : null;
|
||||
let notifBadge;
|
||||
if (notificationState) {
|
||||
notifBadge = <div className="mx_SpacePanel_badgeContainer">
|
||||
<NotificationBadge forceCount={false} notification={notificationState} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
const avatarSize = isNested ? 24 : 32;
|
||||
|
||||
const toggleCollapseButton = childSpaces && childSpaces.length ?
|
||||
<button
|
||||
className="mx_SpaceButton_toggleCollapse"
|
||||
onClick={evt => this.toggleCollapse(evt)}
|
||||
/> : null;
|
||||
|
||||
let button;
|
||||
if (isNarrow) {
|
||||
button = (
|
||||
<RovingAccessibleTooltipButton
|
||||
className={classes}
|
||||
title={space.name}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
forceHide={!!this.state.contextMenuPosition}
|
||||
role="treeitem"
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleTooltipButton>
|
||||
);
|
||||
} else {
|
||||
button = (
|
||||
<RovingAccessibleButton
|
||||
className={classes}
|
||||
onClick={this.onClick}
|
||||
onContextMenu={this.onContextMenu}
|
||||
role="treeitem"
|
||||
>
|
||||
{ toggleCollapseButton }
|
||||
<div className="mx_SpaceButton_selectionWrapper">
|
||||
<RoomAvatar width={avatarSize} height={avatarSize} room={space} />
|
||||
<span className="mx_SpaceButton_name">{ space.name }</span>
|
||||
{ notifBadge }
|
||||
{ this.renderContextMenu() }
|
||||
</div>
|
||||
</RovingAccessibleButton>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<li className={itemClasses}>
|
||||
{ button }
|
||||
{ childItems }
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITreeLevelProps {
|
||||
spaces: Room[];
|
||||
activeSpaces: Room[];
|
||||
isNested?: boolean;
|
||||
}
|
||||
|
||||
const SpaceTreeLevel: React.FC<ITreeLevelProps> = ({
|
||||
spaces,
|
||||
activeSpaces,
|
||||
isNested,
|
||||
}) => {
|
||||
return <ul className="mx_SpaceTreeLevel">
|
||||
{spaces.map(s => {
|
||||
return (<SpaceItem
|
||||
key={s.roomId}
|
||||
activeSpaces={activeSpaces}
|
||||
space={s}
|
||||
isNested={isNested}
|
||||
/>);
|
||||
})}
|
||||
</ul>;
|
||||
}
|
||||
|
||||
export default SpaceTreeLevel;
|
|
@ -518,7 +518,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
// if we're fullscreen, we don't want to set a maxHeight on the video element.
|
||||
const maxVideoHeight = getFullScreenElement() ? null : (
|
||||
const maxVideoHeight = getFullScreenElement() || !this.props.maxVideoHeight ? null : (
|
||||
this.props.maxVideoHeight - (HEADER_HEIGHT + BOTTOM_PADDING + BOTTOM_MARGIN_TOP_BOTTOM)
|
||||
);
|
||||
contentView = <div className={containerClasses}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue